Greasy Fork is available in English.

百度翻译Anki制卡助手

百度翻译XAnki快速制卡助手

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         百度翻译Anki制卡助手
// @namespace    http://juexe.cn
// @version      0.4
// @description  百度翻译XAnki快速制卡助手
// @author       Juexe
// @match        https://fanyi.baidu.com/*
// @grant        none
// @note         2020.09.07-v0.4 支持静默添加模式;Toast操作提示。
// @note         2020.05.10-v0.3 可自定义tag关键词;优化tag点击监听事件;增加记忆技巧。
// @note         2020.04.24-v0.2 添加简明释义
// @note         2020.04.13-v0.1 初始版本
// ==/UserScript==

(function() {
    'use strict';

    let config = {
        'apiAddress': 'http://localhost:8765',
        'deckName': '1.1 英语生词',
        'modelName': '英语生词',
        'frontName': '例句',
        'backName': '翻译',
        'backNoteName': '背面备注',
        'apiKey': 'juexe',
        'autoClose': false,
        'keywordStyleL': '<u>',
        'keywordStyleR': '</u>',
        'silentMode': true //静默添加卡片,否则弹出添加对话框
    };
    let anki_status = false;
    let word_brief = '';

    console.log('百度翻译Anki助手正在运行……');

    initAnki();

    /**
     * 尝试 Anki 连接
     * @desc 成功连接到anki才进行后续注入动作
     */
    function initAnki() {
        sendToAnki({
            "action": "deckNames",
            "version": 6
        }).then(function(data) {
            anki_status = true;
            console.log('连接 AnkiConnect 成功');
            injectCss();
            waitReady(2000);
        }).catch(function(err) {
            anki_status = false;
            console.log('连接 AnkiConnect 失败', err);
        })
    }

    /**
     * 等待关键位置加载完成
     * @param interval 检查频率(毫秒)
     */
    function waitReady(interval = 1000) {
        if (document.querySelector('.anki-send') != null) {
            console.log('重复操作 waitReady()');
            return;
        }
        let hd = setInterval(function() {
            let flag = document.querySelector('.double-sample ol li .sample-source');
            // console.log('find result', flag);
            if (flag != null) {
                // console.log('found!');
                clearInterval(hd);
                inject();
                injectTagButton();
                addTagItemClicker();
            }
        }, interval);
    }

    /**
     * 在关键位置注入插件按钮
     */
    function inject() {
        if (document.querySelector('.anki-send') != null) {
            console.log('重复操作 inject()');
            return;
        }
        console.log('百度翻译Anki助手已载入。');
        dealBriefInfo();
        let samples = document.querySelectorAll('.double-sample ol li');
        samples.forEach(function(sample) {
            let inDom = document.createElement('a');
            inDom.className = 'anki-send';
            inDom.setAttribute('href', 'javascript:void(0)');
            inDom.innerText = '🚀';
            inDom.onclick = function() {
                getSentence(this);
            };
            sample.querySelector('div:last-child').prepend(inDom);
        });
    }

    /**
     * 注入标签按钮
     */
    function injectTagButton() {
        if (document.querySelector('.tag-add-btn') != null) {
            console.log('重复操作 injectTagButton()');
            return;
        }
        let inDom = document.createElement('button');
        inDom.className = 'tag-add-btn';
        inDom.setAttribute('href', 'javascript:void(0)');
        inDom.innerText = '🎨';
        inDom.onclick = function(ev) {
            let key = prompt('输入关键词');
            if (key) {
                console.log('自定义关键词', key);
                let tagDom = document.createElement('span');
                tagDom.className = 'sample-tagitem';
                tagDom.innerText = key;

                document.querySelector('.sample-tagnav').append(tagDom);
            }
            ev.stopPropagation();
            return false;
        };
        document.querySelector('.section-header').prepend(inDom);
    }

    /**
     * 获取单词简明释义
     */
    function dealBriefInfo() {
        let infoLines = document.querySelectorAll('.dictionary-comment p');
        word_brief = '';
        infoLines.forEach(function(p) {
            word_brief += p.innerText.replace('\n', '') + '<br>';
        });
        let momory_skill = document.querySelector('.momory-skill');
        if (momory_skill) {
            word_brief += '[记忆] ' + momory_skill.innerText;
        }
    }

    /*
     * 按钮动作:获取句子信息
     */
    function getSentence(dom) {
        // console.log('dom', dom);

        let sentenceDom = dom.parentNode.querySelector('a~.sample-source');
        let sentence = sentenceDom.innerText;
        let keywords = sentenceDom.querySelectorAll('.high-light');
        let boldText = '';
        keywords.forEach(function(keyword) {
            boldText += keyword.innerText;
        });
        boldText = boldText.trim();
        // console.log('highlight', boldText);
        if (sentence.indexOf(boldText) > -1)
            sentence = sentence.replace(boldText, config.keywordStyleL + boldText + config.keywordStyleR);
        sentence = sentence.trim();
        if (sentence.charAt(sentence.length - 1) !== '.')
            sentence += '.';
        // console.log('sentence', sentence);

        let trans = dom.parentNode.querySelector('a~.sample-target').innerText;
        // console.log('trans', trans);

        let resource = dom.parentNode.querySelector('a~.sample-resource').innerText;
        // console.log('resource', resource);

        if(config.silentMode){
            addNoteSilent(sentence, trans, resource);
        }else{
            addCard(sentence, trans, resource);
        }
    }

    /**
     * 添加卡片
     * @param front
     * @param backend
     * @param chapter
     */
    function addCard(front, backend, chapter = 'Anki 助手') {
        sendToAnki({
            "action": "guiAddCards",
            "version": 6,
            "params": {
                "note": {
                    "deckName": config['deckName'],
                    "modelName": config['modelName'],
                    "fields": {
                        [config.frontName]: front,
                        [config.backName]: backend,
                        [config.backNoteName]: word_brief
                    },
                    "options": {
                        "closeAfterAdding": config.autoClose
                    },
                    "tags": []
                }
            }
        }).then(function(data) {}).catch(function(err) {
            anki_status = false;
            alert('连接 AnkiConnect 失败');
            console.log(err);
        })
    }


    /**
     * 添加卡片
     * @param front
     * @param backend
     * @param chapter
     */
    function addNoteSilent(front, backend, chapter = 'Anki 助手') {
        sendToAnki({
            "action": "addNote",
            "version": 6,
            "params": {
                "note": {
                    "deckName": config['deckName'],
                    "modelName": config['modelName'],
                    "fields": {
                        [config.frontName]: front,
                        [config.backName]: backend,
                        [config.backNoteName]: word_brief
                    },
                    "options": {
                        "allowDuplicate": false,
                        "duplicateScope": "deck"
                    },
                    "tags": []
                }
            }
        }).then(function(data) {
            Toast("添加成功!", 1000);
        }).catch(function(err) {
            anki_status = false;
            alert('连接 AnkiConnect 失败');
            console.log(err);
        });
    }

    /**
     * 封装 fetch 实现 post 请求
     * @param req
     * @returns {Promise<fetch>}
     */
    function sendToAnki(req) {
        req['key'] = config.apiKey;
        return new Promise((resolve, reject) => fetch(config.apiAddress, {
                method: 'POST',
                mode: 'cors',
                body: JSON.stringify(req),
            })
            .then(res => res.json())
            .then(data => {
                let erro = data['error'];
                if (erro != null) {
                    alert('Anki助手请求失败:' + erro);
                    console.log(erro);
                } else {
                    resolve(data);
                }
            })
            .catch(err => reject(err))
        )
    }

    // 监听 url 发生变化
    let refreshTimeout;
    window.addEventListener('hashchange', function() {
        if (anki_status === true) {
            // console.log('url change');
            clearTimeout(refreshTimeout);
            refreshTimeout = setTimeout(waitReady, 3000);
        }
    }, false);

    // 释义标签点击触发
    function addTagItemClicker() {
        let tagnav = document.querySelector(".sample-tagnav");
        // 补充缺失的tagnav
        if (!tagnav) {
            tagnav = document.createElement('div');
            tagnav.className = 'sample-tagnav';
            document.querySelector('.sample-wrap').prepend(tagnav);
            let tag1 = document.createElement('span');
            tag1.className = 'sample-tagitem sample-all sample-current';
            tag1.innerText = '全部';
            tagnav.prepend(tag1);
            console.log('自动补全 tagnav');
        }
        tagnav.onclick = function(ev) {
            setTimeout(inject, 1000);
        };
    }

    // css
    function injectCss() {
        var dom = document.createElement('style'),
            dom_body = document.getElementsByTagName("body")[0];
        dom.innerHTML += '.anki-send{display:block; position:absolute; left:4px;}';
        dom_body.appendChild(dom);
    }

    // 简单Toast
    function Toast(msg,duration=3000){
      var m = document.createElement('div');
      m.innerHTML = msg;
      m.style.cssText="max-width:60%;min-width: 150px;padding:0 14px;height: 40px;color: rgb(255, 255, 255);line-height: 40px;text-align: center;border-radius: 4px;position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 999999;background: rgba(0, 0, 0,.7);font-size: 16px;";
      document.body.appendChild(m);
      setTimeout(function() {
        var d = 0.5;
        m.style.webkitTransition = '-webkit-transform ' + d + 's ease-in, opacity ' + d + 's ease-in';
        m.style.opacity = '0';
        setTimeout(function() { document.body.removeChild(m) }, d * 1000);
      }, duration);
    }
})();