diff options
Diffstat (limited to 'ext/mixed/js')
| -rw-r--r-- | ext/mixed/js/display.js | 400 | ||||
| -rw-r--r-- | ext/mixed/js/japanese.js | 40 | ||||
| -rw-r--r-- | ext/mixed/js/util.js | 150 | 
3 files changed, 263 insertions, 327 deletions
| diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 7982c69f..47efd195 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -27,190 +27,67 @@ class Display {          this.sequence = 0;          this.index = 0;          this.audioCache = {}; -        this.responseCache = {};          $(document).keydown(this.onKeyDown.bind(this));      } -    definitionAdd(definition, mode) { +    onError(error) {          throw 'override me';      } -    definitionsAddable(definitions, modes) { +    onSearchClear() {          throw 'override me';      } -    templateRender(template, data) { -        throw 'override me'; -    } - -    kanjiFind(character) { -        throw 'override me'; -    } - -    handleError(error) { -        throw 'override me'; -    } - -    clearSearch() { -        throw 'override me'; -    } - -    showTermDefs(definitions, options, context) { -        window.focus(); - -        this.spinner.hide(); -        this.definitions = definitions; -        this.options = options; -        this.context = context; - -        const sequence = ++this.sequence; -        const params = { -            definitions, -            addable: options.anki.enable, -            grouped: options.general.groupResults, -            playback: options.general.audioSource !== 'disabled', -            debug: options.general.debugInfo -        }; - -        if (context) { -            for (const definition of definitions) { -                if (context.sentence) { -                    definition.cloze = clozeBuild(context.sentence, definition.source); -                } - -                definition.url = context.url; -            } -        } - -        this.templateRender('terms.html', params).then(content => { -            this.container.html(content); -            this.entryScroll(context && context.index || 0); - -            $('.action-add-note').click(this.onAddNote.bind(this)); -            $('.action-play-audio').click(this.onPlayAudio.bind(this)); -            $('.kanji-link').click(this.onKanjiLookup.bind(this)); - -            return this.adderButtonsUpdate(['term-kanji', 'term-kana'], sequence); -        }).catch(this.handleError.bind(this)); -    } - -    showKanjiDefs(definitions, options, context) { -        window.focus(); - -        this.spinner.hide(); -        this.definitions = definitions; -        this.options = options; -        this.context = context; - -        const sequence = ++this.sequence; -        const params = { -            definitions, -            source: context && context.source, -            addable: options.anki.enable, -            debug: options.general.debugInfo -        }; - -        if (context) { -            for (const definition of definitions) { -                if (context.sentence) { -                    definition.cloze = clozeBuild(context.sentence); -                } - -                definition.url = context.url; -            } -        } - -        this.templateRender('kanji.html', params).then(content => { -            this.container.html(content); -            this.entryScroll(context && context.index || 0); - -            $('.action-add-note').click(this.onAddNote.bind(this)); -            $('.source-term').click(this.onSourceTerm.bind(this)); - -            return this.adderButtonsUpdate(['kanji'], sequence); -        }).catch(this.handleError.bind(this)); +    onSourceTermView(e) { +        e.preventDefault(); +        this.sourceTermView();      } -    adderButtonsUpdate(modes, sequence) { -        return this.definitionsAddable(this.definitions, modes).then(states => { -            if (states === null || sequence !== this.sequence) { -                return; -            } - -            states.forEach((state, index) => { -                for (const mode in state) { -                    const button = Display.adderButtonFind(index, mode); -                    if (state[mode]) { -                        button.removeClass('disabled'); -                    } else { -                        button.addClass('disabled'); -                    } +    async onKanjiLookup(e) { +        try { +            e.preventDefault(); -                    button.removeClass('pending'); +            const link = $(e.target); +            const context = { +                source: { +                    definitions: this.definitions, +                    index: Display.entryIndexFind(link)                  } -            }); -        }); -    } - -    entryScroll(index, smooth) { -        index = Math.min(index, this.definitions.length - 1); -        index = Math.max(index, 0); - -        $('.current').hide().eq(index).show(); - -        const container = $('html,body').stop(); -        const entry = $('.entry').eq(index); -        const target = index === 0 ? 0 : entry.offset().top; - -        if (smooth) { -            container.animate({scrollTop: target}, 200); -        } else { -            container.scrollTop(target); -        } - -        this.index = index; -    } - -    onSourceTerm(e) { -        e.preventDefault(); -        this.sourceBack(); -    } - -    onKanjiLookup(e) { -        e.preventDefault(); +            }; -        const link = $(e.target); -        const context = { -            source: { -                definitions: this.definitions, -                index: Display.entryIndexFind(link) +            if (this.context) { +                context.sentence = this.context.sentence; +                context.url = this.context.url;              } -        }; -        if (this.context) { -            context.sentence = this.context.sentence; -            context.url = this.context.url; +            const kanjiDefs = await apiKanjiFind(link.text()); +            this.kanjiShow(kanjiDefs, this.options, context); +        } catch (e) { +            this.onError(e);          } - -        this.kanjiFind(link.text()).then(kanjiDefs => { -            this.showKanjiDefs(kanjiDefs, this.options, context); -        }).catch(this.handleError.bind(this));      } -    onPlayAudio(e) { +    onAudioPlay(e) {          e.preventDefault();          const index = Display.entryIndexFind($(e.currentTarget));          this.audioPlay(this.definitions[index]);      } -    onAddNote(e) { +    onNoteAdd(e) {          e.preventDefault();          const link = $(e.currentTarget);          const index = Display.entryIndexFind(link);          this.noteAdd(this.definitions[index], link.data('mode'));      } +    onNoteView(e) { +        e.preventDefault(); +        const link = $(e.currentTarget); +        const index = Display.entryIndexFind(link); +        apiNoteView(link.data('noteId')); +    } +      onKeyDown(e) {          const noteTryAdd = mode => {              const button = Display.adderButtonFind(this.index, mode); @@ -219,57 +96,64 @@ class Display {              }          }; +        const noteTryView = mode => { +            const button = Display.viewerButtonFind(this.index); +            if (button.length !== 0 && !button.hasClass('disabled')) { +                apiNoteView(button.data('noteId')); +            } +        }; +          const handlers = {              27: /* escape */ () => { -                this.clearSearch(); +                this.onSearchClear();                  return true;              },              33: /* page up */ () => {                  if (e.altKey) { -                    this.entryScroll(this.index - 3, true); +                    this.entryScrollIntoView(this.index - 3, true);                      return true;                  }              },              34: /* page down */ () => {                  if (e.altKey) { -                    this.entryScroll(this.index + 3, true); +                    this.entryScrollIntoView(this.index + 3, true);                      return true;                  }              },              35: /* end */ () => {                  if (e.altKey) { -                    this.entryScroll(this.definitions.length - 1, true); +                    this.entryScrollIntoView(this.definitions.length - 1, true);                      return true;                  }              },              36: /* home */ () => {                  if (e.altKey) { -                    this.entryScroll(0, true); +                    this.entryScrollIntoView(0, true);                      return true;                  }              },              38: /* up */ () => {                  if (e.altKey) { -                    this.entryScroll(this.index - 1, true); +                    this.entryScrollIntoView(this.index - 1, true);                      return true;                  }              },              40: /* down */ () => {                  if (e.altKey) { -                    this.entryScroll(this.index + 1, true); +                    this.entryScrollIntoView(this.index + 1, true);                      return true;                  }              },              66: /* b */ () => {                  if (e.altKey) { -                    this.sourceBack(); +                    this.sourceTermView();                      return true;                  }              }, @@ -303,6 +187,12 @@ class Display {                      return true;                  } +            }, + +            86: /* v */ () => { +                if (e.altKey) { +                    noteTryView(); +                }              }          }; @@ -312,7 +202,133 @@ class Display {          }      } -    sourceBack() { +    async termsShow(definitions, options, context) { +        try { +            window.focus(); + +            this.definitions = definitions; +            this.options = options; +            this.context = context; + +            const sequence = ++this.sequence; +            const params = { +                definitions, +                addable: options.anki.enable, +                grouped: options.general.groupResults, +                playback: options.general.audioSource !== 'disabled', +                debug: options.general.debugInfo +            }; + +            if (context) { +                for (const definition of definitions) { +                    if (context.sentence) { +                        definition.cloze = Display.clozeBuild(context.sentence, definition.source); +                    } + +                    definition.url = context.url; +                } +            } + +            const content = await apiTemplateRender('terms.html', params); +            this.container.html(content); +            this.entryScrollIntoView(context && context.index || 0); + +            $('.action-add-note').click(this.onNoteAdd.bind(this)); +            $('.action-view-note').click(this.onNoteView.bind(this)); +            $('.action-play-audio').click(this.onAudioPlay.bind(this)); +            $('.kanji-link').click(this.onKanjiLookup.bind(this)); + +            await this.adderButtonUpdate(['term-kanji', 'term-kana'], sequence); +        } catch (e) { +            this.onError(e); +        } +    } + +    async kanjiShow(definitions, options, context) { +        try { +            window.focus(); + +            this.definitions = definitions; +            this.options = options; +            this.context = context; + +            const sequence = ++this.sequence; +            const params = { +                definitions, +                source: context && context.source, +                addable: options.anki.enable, +                debug: options.general.debugInfo +            }; + +            if (context) { +                for (const definition of definitions) { +                    if (context.sentence) { +                        definition.cloze = Display.clozeBuild(context.sentence); +                    } + +                    definition.url = context.url; +                } +            } + +            const content = await apiTemplateRender('kanji.html', params); +            this.container.html(content); +            this.entryScrollIntoView(context && context.index || 0); + +            $('.action-add-note').click(this.onNoteAdd.bind(this)); +            $('.action-view-note').click(this.onNoteView.bind(this)); +            $('.source-term').click(this.onSourceTermView.bind(this)); + +            await this.adderButtonUpdate(['kanji'], sequence); +        } catch (e) { +            this.onError(e); +        } +    } + +    async adderButtonUpdate(modes, sequence) { +        try { +            const states = await apiDefinitionsAddable(this.definitions, modes); +            if (!states || sequence !== this.sequence) { +                return; +            } + +            for (let i = 0; i < states.length; ++i) { +                const state = states[i]; +                for (const mode in state) { +                    const button = Display.adderButtonFind(i, mode); +                    if (state[mode]) { +                        button.removeClass('disabled'); +                    } else { +                        button.addClass('disabled'); +                    } + +                    button.removeClass('pending'); +                } +            } +        } catch (e) { +            this.onError(e); +        } +    } + +    entryScrollIntoView(index, smooth) { +        index = Math.min(index, this.definitions.length - 1); +        index = Math.max(index, 0); + +        $('.current').hide().eq(index).show(); + +        const container = $('html,body').stop(); +        const entry = $('.entry').eq(index); +        const target = index === 0 ? 0 : entry.offset().top; + +        if (smooth) { +            container.animate({scrollTop: target}, 200); +        } else { +            container.scrollTop(target); +        } + +        this.index = index; +    } + +    sourceTermView() {          if (this.context && this.context.source) {              const context = {                  url: this.context.source.url, @@ -320,34 +336,42 @@ class Display {                  index: this.context.source.index              }; -            this.showTermDefs(this.context.source.definitions, this.options, context); +            this.termsShow(this.context.source.definitions, this.options, context);          }      } -    noteAdd(definition, mode) { -        this.spinner.show(); -        return this.definitionAdd(definition, mode).then(success => { -            if (success) { +    async noteAdd(definition, mode) { +        try { +            this.spinner.show(); + +            const noteId = await apiDefinitionAdd(definition, mode); +            if (noteId) {                  const index = this.definitions.indexOf(definition);                  Display.adderButtonFind(index, mode).addClass('disabled'); +                Display.viewerButtonFind(index).removeClass('pending disabled').data('noteId', noteId);              } else { -                this.handleError('note could not be added'); +                throw 'note could note be added';              } -        }).catch(this.handleError.bind(this)).then(() => this.spinner.hide()); +        } catch (e) { +            this.onError(e); +        } finally { +            this.spinner.hide(); +        }      } -    audioPlay(definition) { -        this.spinner.show(); +    async audioPlay(definition) { +        try { +            this.spinner.show(); -        for (const key in this.audioCache) { -            this.audioCache[key].pause(); -        } - -        audioBuildUrl(definition, this.options.general.audioSource, this.responseCache).then(url => { +            let url = await apiAudioGetUrl(definition, this.options.general.audioSource);              if (!url) {                  url = '/mixed/mp3/button.mp3';              } +            for (const key in this.audioCache) { +                this.audioCache[key].pause(); +            } +              let audio = this.audioCache[url];              if (audio) {                  audio.currentTime = 0; @@ -365,7 +389,25 @@ class Display {                      audio.play();                  };              } -        }).catch(this.handleError.bind(this)).then(() => this.spinner.hide()); +        } catch (e) { +            this.onError(e); +        } finally { +            this.spinner.hide(); +        } +    } + +    static clozeBuild(sentence, source) { +        const result = { +            sentence: sentence.text.trim() +        }; + +        if (source) { +            result.prefix = sentence.text.substring(0, sentence.offset).trim(); +            result.body = source.trim(); +            result.suffix = sentence.text.substring(sentence.offset + source.length).trim(); +        } + +        return result;      }      static entryIndexFind(element) { @@ -375,4 +417,8 @@ class Display {      static adderButtonFind(index, mode) {          return $('.entry').eq(index).find(`.action-add-note[data-mode="${mode}"]`);      } + +    static viewerButtonFind(index) { +        return $('.entry').eq(index).find('.action-view-note'); +    }  } diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js new file mode 100644 index 00000000..c11e955b --- /dev/null +++ b/ext/mixed/js/japanese.js @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + + +function jpIsKanji(c) { +    const code = c.charCodeAt(0); +    return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0; +} + +function jpIsKana(c) { +    return wanakana.isKana(c); +} + +function jpKatakanaToHiragana(text) { +    let result = ''; +    for (const c of text) { +        if (wanakana.isKatakana(c)) { +            result += wanakana.toHiragana(c); +        } else { +            result += c; +        } +    } + +    return result; +} diff --git a/ext/mixed/js/util.js b/ext/mixed/js/util.js deleted file mode 100644 index 5cf62000..00000000 --- a/ext/mixed/js/util.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2017  Alex Yatskov <alex@foosoft.net> - * Author: Alex Yatskov <alex@foosoft.net> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <http://www.gnu.org/licenses/>. - */ - - -/* - * Cloze - */ - -function clozeBuild(sentence, source) { -    const result = { -        sentence: sentence.text.trim() -    }; - -    if (source) { -        result.prefix = sentence.text.substring(0, sentence.offset).trim(); -        result.body = source.trim(); -        result.suffix = sentence.text.substring(sentence.offset + source.length).trim(); -    } - -    return result; -} - - -/* - * Audio - */ - -function audioBuildUrl(definition, mode, cache={}) { -    if (mode === 'jpod101') { -        let kana = definition.reading; -        let kanji = definition.expression; - -        if (!kana && wanakana.isHiragana(kanji)) { -            kana = kanji; -            kanji = null; -        } - -        const params = []; -        if (kanji) { -            params.push(`kanji=${encodeURIComponent(kanji)}`); -        } -        if (kana) { -            params.push(`kana=${encodeURIComponent(kana)}`); -        } - -        const url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`; -        return Promise.resolve(url); -    } else if (mode === 'jpod101-alternate') { -        return new Promise((resolve, reject) => { -            const response = cache[definition.expression]; -            if (response) { -                resolve(response); -            } else { -                const data = { -                    post: 'dictionary_reference', -                    match_type: 'exact', -                    search_query: definition.expression -                }; - -                const params = []; -                for (const key in data) { -                    params.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`); -                } - -                const xhr = new XMLHttpRequest(); -                xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'); -                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); -                xhr.addEventListener('error', () => reject('failed to scrape audio data')); -                xhr.addEventListener('load', () => { -                    cache[definition.expression] = xhr.responseText; -                    resolve(xhr.responseText); -                }); - -                xhr.send(params.join('&')); -            } -        }).then(response => { -            const dom = new DOMParser().parseFromString(response, 'text/html'); -            for (const row of dom.getElementsByClassName('dc-result-row')) { -                try { -                    const url = row.getElementsByClassName('ill-onebuttonplayer').item(0).getAttribute('data-url'); -                    const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText; -                    if (url && reading && (!definition.reading || definition.reading === reading)) { -                        return url; -                    } -                } catch (e) { -                    // NOP -                } -            } -        }); -    } else { -        return Promise.reject('unsupported audio source'); -    } -} - -function audioBuildFilename(definition) { -    if (definition.reading || definition.expression) { -        let filename = 'yomichan'; -        if (definition.reading) { -            filename += `_${definition.reading}`; -        } -        if (definition.expression) { -            filename += `_${definition.expression}`; -        } - -        return filename += '.mp3'; -    } -} - -function audioInject(definition, fields, mode) { -    if (mode === 'disabled') { -        return Promise.resolve(true); -    } - -    const filename = audioBuildFilename(definition); -    if (!filename) { -        return Promise.resolve(true); -    } - -    let usesAudio = false; -    for (const name in fields) { -        if (fields[name].includes('{audio}')) { -            usesAudio = true; -            break; -        } -    } - -    if (!usesAudio) { -        return Promise.resolve(true); -    } - -    return audioBuildUrl(definition, mode).then(url => { -        definition.audio = {url, filename}; -        return true; -    }).catch(() => false); -} |