Merge pull request #182 from toasted-nutbread/anki-screenshot

Anki screenshot
This commit is contained in:
Alex Yatskov 2019-08-17 09:05:33 -07:00 committed by GitHub
commit 8ebac935e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 175 additions and 10 deletions

View File

@ -58,6 +58,11 @@ class AnkiConnect {
return await this.ankiInvoke('guiBrowse', {query}); return await this.ankiInvoke('guiBrowse', {query});
} }
async storeMediaFile(filename, dataBase64) {
await this.checkVersion();
return await this.ankiInvoke('storeMediaFile', {filename, data: dataBase64});
}
async checkVersion() { async checkVersion() {
if (this.remoteVersion < this.localVersion) { if (this.remoteVersion < this.localVersion) {
this.remoteVersion = await this.ankiInvoke('version'); this.remoteVersion = await this.ankiInvoke('version');

View File

@ -53,7 +53,7 @@ async function apiKanjiFind(text) {
return definitions.slice(0, options.general.maxResults); return definitions.slice(0, options.general.maxResults);
} }
async function apiDefinitionAdd(definition, mode) { async function apiDefinitionAdd(definition, mode, context) {
const options = utilBackend().options; const options = utilBackend().options;
if (mode !== 'kanji') { if (mode !== 'kanji') {
@ -64,6 +64,14 @@ async function apiDefinitionAdd(definition, mode) {
); );
} }
if (context.screenshot) {
await apiInjectScreenshot(
definition,
options.anki.terms.fields,
context.screenshot
);
}
const note = await dictNoteFormat(definition, mode, options); const note = await dictNoteFormat(definition, mode, options);
return utilBackend().anki.addNote(note); return utilBackend().anki.addNote(note);
} }
@ -139,3 +147,61 @@ async function apiCommandExec(command) {
async function apiAudioGetUrl(definition, source) { async function apiAudioGetUrl(definition, source) {
return audioBuildUrl(definition, source); return audioBuildUrl(definition, source);
} }
async function apiInjectScreenshot(definition, fields, screenshot) {
let usesScreenshot = false;
for (const name in fields) {
if (fields[name].includes('{screenshot}')) {
usesScreenshot = true;
break;
}
}
if (!usesScreenshot) {
return;
}
const dateToString = (date) => {
const year = date.getUTCFullYear();
const month = date.getUTCMonth().toString().padStart(2, '0');
const day = date.getUTCDate().toString().padStart(2, '0');
const hours = date.getUTCHours().toString().padStart(2, '0');
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
const seconds = date.getUTCSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
};
const now = new Date(Date.now());
const filename = `yomichan_browser_screenshot_${definition.reading}_${dateToString(now)}.${screenshot.format}`;
const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, '');
try {
await utilBackend().anki.storeMediaFile(filename, data);
} catch (e) {
return;
}
definition.screenshotFileName = filename;
}
function apiScreenshotGet(options, sender) {
if (!(sender && sender.tab)) {
return Promise.resolve();
}
const windowId = sender.tab.windowId;
return new Promise((resolve) => {
chrome.tabs.captureVisibleTab(windowId, options, (dataUrl) => resolve(dataUrl));
});
}
function apiForward(action, params, sender) {
if (!(sender && sender.tab)) {
return Promise.resolve();
}
const tabId = sender.tab.id;
return new Promise((resolve) => {
chrome.tabs.sendMessage(tabId, {action, params}, (response) => resolve(response));
});
}

View File

@ -94,8 +94,8 @@ class Backend {
forward(apiTermsFind(text), callback); forward(apiTermsFind(text), callback);
}, },
definitionAdd: ({definition, mode, callback}) => { definitionAdd: ({definition, mode, context, callback}) => {
forward(apiDefinitionAdd(definition, mode), callback); forward(apiDefinitionAdd(definition, mode, context), callback);
}, },
definitionsAddable: ({definitions, modes, callback}) => { definitionsAddable: ({definitions, modes, callback}) => {
@ -116,6 +116,14 @@ class Backend {
audioGetUrl: ({definition, source, callback}) => { audioGetUrl: ({definition, source, callback}) => {
forward(apiAudioGetUrl(definition, source), callback); forward(apiAudioGetUrl(definition, source), callback);
},
screenshotGet: ({options}) => {
forward(apiScreenshotGet(options, sender), callback);
},
forward: ({action, params}) => {
forward(apiForward(action, params, sender), callback);
} }
}; };

View File

@ -343,7 +343,8 @@ async function dictFieldFormat(field, definition, mode, options) {
'reading', 'reading',
'sentence', 'sentence',
'tags', 'tags',
'url' 'url',
'screenshot'
]; ];
for (const marker of markers) { for (const marker of markers) {

View File

@ -175,6 +175,10 @@ function optionsFieldTemplates() {
<a href="{{definition.url}}">{{definition.url}}</a> <a href="{{definition.url}}">{{definition.url}}</a>
{{/inline}} {{/inline}}
{{#*inline "screenshot"}}
<img src="{{definition.screenshotFileName}}" />
{{/inline}}
{{~> (lookup . "marker") ~}} {{~> (lookup . "marker") ~}}
`.trim(); `.trim();
} }
@ -220,6 +224,7 @@ function optionsSetDefaults(options) {
server: 'http://127.0.0.1:8765', server: 'http://127.0.0.1:8765',
tags: ['yomichan'], tags: ['yomichan'],
sentenceExt: 200, sentenceExt: 200,
screenshot: {format: 'png', quality: 92},
terms: {deck: '', model: '', fields: {}}, terms: {deck: '', model: '', fields: {}},
kanji: {deck: '', model: '', fields: {}}, kanji: {deck: '', model: '', fields: {}},
fieldTemplates: optionsFieldTemplates() fieldTemplates: optionsFieldTemplates()
@ -283,6 +288,11 @@ function optionsVersion(options) {
if (utilStringHashCode(options.anki.fieldTemplates) === 1285806040) { if (utilStringHashCode(options.anki.fieldTemplates) === 1285806040) {
options.anki.fieldTemplates = optionsFieldTemplates(); options.anki.fieldTemplates = optionsFieldTemplates();
} }
},
() => {
if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) {
options.anki.fieldTemplates = optionsFieldTemplates();
}
} }
]; ];

View File

@ -51,6 +51,8 @@ async function formRead() {
optionsNew.anki.tags = $('#card-tags').val().split(/[,; ]+/); optionsNew.anki.tags = $('#card-tags').val().split(/[,; ]+/);
optionsNew.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); optionsNew.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10);
optionsNew.anki.server = $('#interface-server').val(); optionsNew.anki.server = $('#interface-server').val();
optionsNew.anki.screenshot.format = $('#screenshot-format').val();
optionsNew.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10);
optionsNew.anki.fieldTemplates = $('#field-templates').val(); optionsNew.anki.fieldTemplates = $('#field-templates').val();
if (optionsOld.anki.enable && !ankiErrorShown()) { if (optionsOld.anki.enable && !ankiErrorShown()) {
@ -188,6 +190,8 @@ async function onReady() {
$('#card-tags').val(options.anki.tags.join(' ')); $('#card-tags').val(options.anki.tags.join(' '));
$('#sentence-detection-extent').val(options.anki.sentenceExt); $('#sentence-detection-extent').val(options.anki.sentenceExt);
$('#interface-server').val(options.anki.server); $('#interface-server').val(options.anki.server);
$('#screenshot-format').val(options.anki.screenshot.format);
$('#screenshot-quality').val(options.anki.screenshot.quality);
$('#field-templates').val(options.anki.fieldTemplates); $('#field-templates').val(options.anki.fieldTemplates);
$('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset)); $('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset));
$('input, select, textarea').not('.anki-model').change(utilAsync(onFormOptionsChanged)); $('input, select, textarea').not('.anki-model').change(utilAsync(onFormOptionsChanged));
@ -505,7 +509,8 @@ async function ankiFieldsPopulate(element, options) {
'reading', 'reading',
'sentence', 'sentence',
'tags', 'tags',
'url' 'url',
'screenshot'
], ],
'kanji': [ 'kanji': [
'character', 'character',

View File

@ -293,6 +293,19 @@
<input type="text" id="interface-server" class="form-control"> <input type="text" id="interface-server" class="form-control">
</div> </div>
<div class="form-group options-advanced">
<label for="screenshot-format">Screenshot format</label>
<select class="form-control" id="screenshot-format">
<option value="png">PNG</option>
<option value="jpeg">JPEG</option>
</select>
</div>
<div class="form-group options-advanced">
<label for="screenshot-quality">Screenshot quality (JPEG only)</label>
<input type="number" min="0" max="100" step="1" id="screenshot-quality" class="form-control">
</div>
<div id="anki-format"> <div id="anki-format">
<p class="help-block"> <p class="help-block">
Specify the information you would like included in your flashcards in the field editor below. Specify the information you would like included in your flashcards in the field editor below.

View File

@ -33,8 +33,8 @@ function apiKanjiFind(text) {
return utilInvoke('kanjiFind', {text}); return utilInvoke('kanjiFind', {text});
} }
function apiDefinitionAdd(definition, mode) { function apiDefinitionAdd(definition, mode, context) {
return utilInvoke('definitionAdd', {definition, mode}); return utilInvoke('definitionAdd', {definition, mode, context});
} }
function apiDefinitionsAddable(definitions, modes) { function apiDefinitionsAddable(definitions, modes) {
@ -53,6 +53,10 @@ function apiCommandExec(command) {
return utilInvoke('commandExec', {command}); return utilInvoke('commandExec', {command});
} }
function apiAudioGetUrl(definition, source) { function apiScreenshotGet(options) {
return utilInvoke('audioGetUrl', {definition, source}); return utilInvoke('screenshotGet', {options});
}
function apiForward(action, params) {
return utilInvoke('forward', {action, params});
} }

View File

@ -244,6 +244,10 @@ class Frontend {
if (!this.options.enable) { if (!this.options.enable) {
this.searchClear(); this.searchClear();
} }
},
popupSetVisible: ({visible}) => {
this.popup.setVisible(visible);
} }
}; };

View File

@ -113,6 +113,14 @@ class Popup {
return this.injected && this.container.style.visibility !== 'hidden'; return this.injected && this.container.style.visibility !== 'hidden';
} }
setVisible(visible) {
if (visible) {
this.container.style.setProperty('display', '');
} else {
this.container.style.setProperty('display', 'none', 'important');
}
}
containsPoint(point) { containsPoint(point) {
if (!this.isVisible()) { if (!this.isVisible()) {
return false; return false;

View File

@ -435,7 +435,15 @@ class Display {
try { try {
this.spinner.show(); this.spinner.show();
const noteId = await apiDefinitionAdd(definition, mode); const context = {};
if (this.noteUsesScreenshot()) {
const screenshot = await this.getScreenshot();
if (screenshot) {
context.screenshot = screenshot;
}
}
const noteId = await apiDefinitionAdd(definition, mode, context);
if (noteId) { if (noteId) {
const index = this.definitions.indexOf(definition); const index = this.definitions.indexOf(definition);
Display.adderButtonFind(index, mode).addClass('disabled'); Display.adderButtonFind(index, mode).addClass('disabled');
@ -488,10 +496,39 @@ class Display {
} }
} }
noteUsesScreenshot() {
const fields = this.options.anki.terms.fields;
for (const name in fields) {
if (fields[name].includes('{screenshot}')) {
return true;
}
}
return false;
}
async getScreenshot() {
try {
await this.setPopupVisible(false);
await Display.delay(1); // Wait for popup to be hidden.
const {format, quality} = this.options.anki.screenshot;
const dataUrl = await apiScreenshotGet({format, quality});
if (!dataUrl || dataUrl.error) { return; }
return {dataUrl, format};
} finally {
await this.setPopupVisible(true);
}
}
get firstExpressionIndex() { get firstExpressionIndex() {
return this.options.general.resultOutputMode === 'merge' ? 0 : -1; return this.options.general.resultOutputMode === 'merge' ? 0 : -1;
} }
setPopupVisible(visible) {
return apiForward('popupSetVisible', {visible});
}
static clozeBuild(sentence, source) { static clozeBuild(sentence, source) {
const result = { const result = {
sentence: sentence.text.trim() sentence: sentence.text.trim()
@ -517,4 +554,8 @@ class Display {
static viewerButtonFind(index) { static viewerButtonFind(index) {
return $('.entry').eq(index).find('.action-view-note'); return $('.entry').eq(index).find('.action-view-note');
} }
static delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
} }