diff --git a/background-script.js b/background-script.js index c23835f..42a9f88 100644 --- a/background-script.js +++ b/background-script.js @@ -1731,6 +1731,20 @@ browser.runtime.onMessage.addListener((req, sender, sendResponse) => { refresh_epwing(); } + else if (req.type == "ankiconnect_mine") + { + console.log(req.command); + let xhr = new XMLHttpRequest(); + xhr.open("POST", req.host, true); + //xhr.setRequestHeader('Content-type','application/json; charset=utf-8'); + xhr.addEventListener('load', () => + { + console.log(xhr.responseText); + let response = JSON.parse(xhr.responseText); + console.log(response); + }); + xhr.send(req.command); + } else { return; diff --git a/livemining.html b/livemining.html new file mode 100644 index 0000000..8a744d0 --- /dev/null +++ b/livemining.html @@ -0,0 +1,87 @@ + + + + + +Nazeka: Configuration for Live Mining + + + +

Configuration for Live Mining

+

Live Mining requires Anki and the Ankiconnect plugin.

+ +

If Live Mining is enabled, then when you mine notes to the mining storage, Nazeka will also attempt to connect to Ankiconnect and tell it to insert a new note.

+ +

Nazeka will NOT show any error indications if Ankiconnect fails to insert the new note (such as if Anki or Ankiconnect isn't running, or the card model/template changed since configuration).

+ +

Deck and Model

+
+
+
+
+ +

Fields

+
+
+ +
+(Unpopulated) +
+ + + + + +

Fields Help

+ +

Behavior notes:

+ + + +

About formatting identities:

+ +

nothing - Nothing. + +found_spelling - The spelling you clicked on to mine the word (e.g. "行く") (this is always the "found spelling" described above) +readings - Readings (this is the list of readings you see in the mining UI when mining the word) + +definitions - Definition (HTML; has spans with class names, newlines are <br> tags) +definitions_raw - Definition (plaintext, linebreaks are LF characters; only for templates with special CSS or JS) + +found_text - Text the word was found as (e.g. "行きます"). Also known as "found text". +context - The sentence you found the word in. Sentence boundaries are a guess, so it might be cut off funny and need manual editing inside of anki. +context_left - The left half of the sentence you found the word in, until the found text. +context_right - The right half of the sentence you found the word in, after the found text. + +audio_anki - anki audio tag (e.g. "[sound:nazeka_audio_ねにもつ_根に持つ.mp3]"), downloaded by Nazeka, sent to anki with Ankiconnect's storeMediaFile command; added even if the storeMediaFile command fails; not all words have audio, may be blank +audio_kanji - spelling portion of audio file's name, does no downloading of files. if there are no spellings, it's identical to audio_reading. may be blank. +audio_reading - reading portion of audio file's name, does no downloading of files. may be blank. + +If you use the "audio" field, then the audio file is downloaded from jpod101's servers when you mine the card, and sent to Anki's media folder with Ankiconnect's storeMediaFile action. This only works when you're connected to the internet. + +The "audio_kanji" and "audio_reading" fields are for people with the JDIC audio pack, or recovering the audio manually if you mined while your internet was down, or for people who want to play the audio file from jpod101's servers directly so they don't need to store it as a media file. (Not sure whether Anki currently supports that, but it should be possible in the future.) + +jmdict - JMDict numeric ID +url - URL of the page the card was mined from (if it was the reader page, it will look like jibberish) +time_unix - Time when card was mined (unix timestamp, milliseconds, can be converted to a localized date within Anki by using javascript in the card model/template) +time_local - Time when card was mined (localized date, depends on browser and OS) + +readings_raw - The unfiltered list of readings associated with the given word. Restriction information, like さくや not being a reading of 夕べ, is stripped. +spellings_raw - The unfiltered list of spellings associated with the given word. Restriction information is stripped. +

+ +

Advanced

+
+
+ + diff --git a/livemining.js b/livemining.js new file mode 100644 index 0000000..d693afb --- /dev/null +++ b/livemining.js @@ -0,0 +1,151 @@ +'use strict'; + +function reset_fields() +{ + document.getElementById("containfields").innerHTML = ""; +} + +function add_option(input, text, selected=false) +{ + let option = document.createElement("option"); + option.value = text; + option.innerText = text; + option.selected = selected; + input.appendChild(option); +} + +function add_options(input, preselect="") +{ + let list = [ + "nothing", + + "found_spelling", + "readings", + + "definitions", + "definitions_raw", + + "found_text", + "context", + "context_left", + "context_right", + + "audio_anki", + "audio_kanji", + "audio_reading", + + "jmdict_id", + "url", + "time_unix", + "time_local", + + "readings_raw", + "spellings_raw", + ]; + + for (let ident of list) + { + add_option(input, ident, (ident == preselect)); + } +} + +function add_field(fieldname, preselect="") +{ + let div = document.createElement("div"); + let input = document.createElement("select"); + input.id = fieldname; + add_options(input, preselect); + let label = document.createElement("label"); + label.for = fieldname; + label.innerText = fieldname; + div.appendChild(input); + div.appendChild(label); + document.getElementById("containfields").appendChild(div); +} + +function saveConfig() +{ + let mappings = new Array(); + for(let element of document.querySelectorAll("#containfields > div")) + { + console.log(element); + mappings.push([element.querySelector("label").innerText, element.querySelector("select").value]); + } + console.log(mappings); + let setstuff = + { + "livemining_deckname": document.querySelector("#deckname").value, + "livemining_modelname": document.querySelector("#modelname").value, + "livemining_fields": mappings, + "livemining_host": document.querySelector("#host").value + }; + console.log(setstuff); + browser.storage.local.set(setstuff); +} + +function loadConfig() +{ + let promise = browser.storage.local.get(["livemining_deckname", "livemining_modelname", "livemining_fields", "livemining_host"]); + promise.then((storage) => + { + if(storage["livemining_deckname"] !== undefined) + document.querySelector("#deckname").value = storage["livemining_deckname"]; + if(storage["livemining_modelname"] !== undefined) + document.querySelector("#modelname").value = storage["livemining_modelname"]; + if(storage["livemining_host"] !== undefined) + document.querySelector("#host").value = storage["livemining_host"]; + reset_fields(); + for(let pair of storage["livemining_fields"]) + add_field(pair[0], pair[1]); + }, (e) => {console.log(e);}); +} + + +function buildpage() +{ + document.getElementById("autofields").onclick = function(e) + { + let host = document.getElementById("host").value; + + let request = JSON.parse(` + {"action":"modelFieldNames", + "version":6, + "params": {"modelName":"Basic"} + }`); + request["params"]["modelName"] = document.getElementById("modelname").value; + + let json = JSON.stringify(request); + console.log(json); + + let xhr = new XMLHttpRequest(); + xhr.open("POST", host, true); + //xhr.setRequestHeader('Content-type','application/json; charset=utf-8'); + xhr.addEventListener('load', () => + { + let response = JSON.parse(xhr.responseText); + if (response["result"]) + { + reset_fields(); + for(let fieldname of response["result"]) + add_field(fieldname); + } + }); + xhr.send(json); + + e.preventDefault(); + }; + + document.getElementById("applyfields").onclick = saveConfig; + + loadConfig(); + + document.getElementById("loadfields").onclick = loadConfig; +} +if (document.readyState == "complete") +{ + buildpage(); +} +else +{ + document.addEventListener("DOMContentLoaded", buildpage); +} diff --git a/manifest.json b/manifest.json index dac9cc0..7a7182e 100644 --- a/manifest.json +++ b/manifest.json @@ -17,7 +17,8 @@ "contextMenus", "storage", "tabs", - "clipboardRead" + "clipboardRead", + "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?*" ], "background": { @@ -44,6 +45,8 @@ "reader.html", "mining.js", "mining.html", + "livemining.js", + "livemining.html", "jmdictabbreviations.html" ], diff --git a/options.html b/options.html index 0690f3d..c8181bd 100644 --- a/options.html +++ b/options.html @@ -17,11 +17,15 @@ +
+

+
+

About

This extension uses JMdict, which is Copyright © 2018 The Electronic Dictionary Research and Development Group and licensed under the CC BY-SA 3.0 license.

This extension is otherwise licensed under the Apache License, version 2.0.

-

View JMDict abbreviation definitions

+

diff --git a/options.js b/options.js index c508a50..0382215 100644 --- a/options.js +++ b/options.js @@ -80,6 +80,12 @@ function defaults() default: false, label: "Ignore hard line breaks" }); + settings.push({ + id: "live_mining", + kind: "checkbox", + default: false, + label: "Enable live mining (configured via the button below under \"Other\")" + }); // display settings.push({ @@ -161,7 +167,7 @@ function defaults() kind: "combobox", options: ["Flip", "Push"], default: 0, - label: "How to keep the popup on the screen vertically (at least one of these two options should be \"Push\")" + label: "How to keep the popup on the screen vertically (at least one of these two options should be \"Flip\")" }); // theme @@ -430,6 +436,11 @@ function buildpage() optionsection.appendChild(container); } + let livemininglink = document.getElementById("livemininglink"); + livemininglink.onclick = function(e){browser.windows.create({url:browser.extension.getURL('livemining.html'),type:'popup'}); e.preventDefault();}; + + optionsection.appendChild(document.createElement("hr")); + let file = document.createElement("input"); file.type = "file"; file.id = "epwing_file"; diff --git a/texthook.js b/texthook.js index 22a1be1..75ac9c9 100644 --- a/texthook.js +++ b/texthook.js @@ -55,6 +55,7 @@ hotkey_audio: "p", hotkey_nudge_left: "ArrowLeft", hotkey_nudge_right: "ArrowRight", volume: 0.2, +live_mining: false }; let platform = "win"; @@ -526,6 +527,7 @@ function build_div_inner(text, result, moreText, index, first_of_many = false) else continue; // shouldn't happen, but again, just in case of broken data + if(term.has_audio.length > 0) { for(let audio of term.has_audio) @@ -538,6 +540,29 @@ function build_div_inner(text, result, moreText, index, first_of_many = false) } } + // for mining + + let reading_data_holder = document.createElement("div"); + reading_data_holder.className = "nazeka_mining_readings"; + reading_data_holder.style.display = "none"; + let reading_data_list = new Array(); + for(let r of term.r_ele) + reading_data_list.push(r.reb); + reading_data_holder.innerText = reading_data_list.join("、"); + container.appendChild(reading_data_holder); + + let spelling_data_holder = document.createElement("div"); + spelling_data_holder.className = "nazeka_mining_spellings"; + spelling_data_holder.style.display = "none"; + let spelling_data_list = new Array(); + if(term.k_ele) + for(let k of term.k_ele) + spelling_data_list.push(k.keb); + spelling_data_holder.innerText = spelling_data_list.join("、"); + container.appendChild(spelling_data_holder); + + // end of "for mining" + let temptag = document.createElement("span"); temptag.className = "nazeka_word"; let original_kana = ""; @@ -995,6 +1020,7 @@ async function settings_init() getvar("hotkey_nudge_left", "ArrowLeft"); getvar("hotkey_nudge_right", "ArrowRight"); getvar("volume", 0.2); + getvar("live_mining", false); if(!settings.enabled && exists_div()) delete_div(); @@ -1575,32 +1601,160 @@ async function mine_to_storage(object) browser.storage.local.set({cards:cards}); } -async function try_to_play_audio(object) +function get_audio_text(element) // [reading, spelling] { - if(!exists_div()) - return; - let mydiv = get_div().querySelector(".nazeka_audioref"); + let mydiv = element.querySelector(".nazeka_audioref"); if(mydiv) { let text = mydiv.innerText; - let url; if(text.includes(";")) { let fields = text.split(";"); - url = "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kana=" + fields[0] + "&kanji=" + fields[1]; + return [fields[0], fields[1]]; } else - url = "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kana=" + text + "&kanji=" + text; + return [text, text]; + } + return undefined; +} + +async function try_to_play_audio() +{ + if(!exists_div()) + return; + let fields = get_audio_text(get_div()); + if(fields) + { + let url = "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kana=" + fields[0] + "&kanji=" + fields[1]; let audio = new Audio(url); audio.volume = settings.volume; audio.play(); } } +async function try_to_play_audio() +{ + if(!exists_div()) + return; + let fields = get_audio_text(get_div()); + if(fields) + { + let url = "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kana=" + fields[0] + "&kanji=" + fields[1]; + let audio = new Audio(url); + audio.volume = settings.volume; + audio.play(); + } +} + +function send_ankiconnect_command(host, command) +{ + let json = JSON.stringify(command); + + browser.runtime.sendMessage({type:"ankiconnect_mine", host:host, command:json}); +} + +async function mine_to_ankiconnect_with_base64_audio(object, audiofname, audiodata) +{ + let object_keys = Object.keys(object); + let promise = browser.storage.local.get(["livemining_deckname", "livemining_modelname", "livemining_fields", "livemining_host"]); + promise.then((storage) => + { + if(storage["livemining_host"] === undefined) + storage["livemining_host"] = "http://localhost:8765/"; + if(storage["livemining_deckname"] === undefined) + storage["livemining_deckname"] = "Default"; + if(storage["livemining_modelname"] === undefined) + storage["livemining_modelname"] = "Basic"; + if(storage["livemining_fields"] === undefined) + storage["livemining_fields"] = [["Front", "found_spelling"], ["Back", "readings"]]; + + console.log("attempting to mine card"); + + let note = { + "action": "addNote", + "version": 6, + "params": { + "note": { + "deckName": storage["livemining_deckname"], + "modelName": storage["livemining_modelname"], + "fields": {}, + "tags": [ + "nazeka" + ] + } + } + }; + let audioadded = false; + let fields = {}; + console.log(storage["livemining_fields"]); + console.log("starting loop"); + for(let pair of storage["livemining_fields"]) + { + console.log("start of iteration"); + console.log(pair); + if(pair[1] != "audio_anki" && object_keys.includes(pair[1])) + fields[pair[0]] = String(object[pair[1]]); + if(pair[1] == "audio_anki") + { + if(audiofname != "" && audiodata != "") + { + if(!audioadded) + { + let mediafilecommand = { + "action": "storeMediaFile", + "version": 6, + "params": { + "filename": audiofname, + "data": audiodata + } + }; + console.log("starting command"); + send_ankiconnect_command(storage["livemining_host"], mediafilecommand); + audioadded = true; + } + // for some reason not even anki 2.0 supports this + //if(pair[1] == "audio_html") + // fields[pair[0]] = ""; + //else + fields[pair[0]] = "[sound:" + audiofname + "]"; + } + else + fields[pair[0]] = ""; + } + console.log("end of iteration"); + } + console.log("ending loop"); + note["params"]["note"]["fields"] = fields; + send_ankiconnect_command(storage["livemining_host"], note); + console.log("test"); + }, (e) => {console.log(e);}); +} + +async function mine_to_ankiconnect(object) +{ + console.log(object); + let audio_base64 = ""; + if(object.audio !== undefined) + { + let url = "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kana=" + object.audio[0] + "&kanji=" + object.audio[1]; + console.log(url); + let reader = new FileReader(); + reader.onload = () => + { + let fname = "nazeka_audio_" + object.audio[0] + "_" + object.audio[1] + ".mp3"; + let result = reader.result.replace(/[^,]*,/, ""); + mine_to_ankiconnect_with_base64_audio(object, fname, result); + }; + fetch(url) + .then((response) => response.blob()) + .then((myBlob) => reader.readAsDataURL(myBlob)); + } + else + mine_to_ankiconnect_with_base64_audio(object, "", ""); +} + function mine(highlight) { - // TODO mine audio information - let front = highlight.textContent; let word = highlight.parentElement.parentElement; let readings = ""; @@ -1614,6 +1768,56 @@ function mine(highlight) let seq = word.getAttribute("nazeka_seq"); mine_to_storage({front: front, readings: readings, definitions: definitions, lookup: lookup.textContent, sentence: sentence.textContent, index: index.textContent, seq: seq}); + + if(settings.live_mining) + { + let now = new Date(); + + let readings_raw = word.querySelector(".nazeka_mining_readings").innerText; + let spellings_raw = word.querySelector(".nazeka_mining_spellings").innerText; + + let audiostuff = get_audio_text(word); + let audio_kanji = ""; + let audio_reading = ""; + + if(readings.length >= 2) + readings = readings.substring(1, readings.length-1); + + console.log(audiostuff); + + if(audiostuff !== undefined) + { + audio_kanji = audiostuff[1]; + audio_reading = audiostuff[0]; + } + mine_to_ankiconnect( + { + nothing:"", + + found_spelling:front, + readings:readings, + + definitions:definitions.innerHTML, + definitions_raw:definitions.innerText, + + found_text: lookup.textContent, + context: sentence.textContent, + context_left: sentence.textContent.substring(0, index.textContent), + context_right: sentence.textContent.substring(parseInt(index.textContent) + lookup.textContent.length), + + audio: audiostuff, + audio_kanji: audio_kanji, + audio_reading: audio_reading, + + jmdict_id: seq, + url: get_doc().URL, + time_unix: now.getTime(), + time_local: now.toLocaleString(), + + readings_raw: readings_raw, + spellings_raw: spellings_raw, + }); + } } function keytest(event)