anki

make card

Version vom 05.05.2025. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.ip-ddns.com/scripts/534395/1583577/anki.js

;const {
    addAnki, getAnkiFormValue,
    anki, ankiSave, showAnkiCard,
    queryAnki,
    PushAnkiBeforeSaveHook, PushAnkiAfterSaveHook,
    PushExpandAnkiRichButton,
    PushExpandAnkiInputButton,
    PushHookAnkiStyle, PushHookAnkiHtml, PushHookAnkiClose, PushHookAnkiDidRender, PushShowFn, PushHookAnkiChange
} = (() => {
    let ankiHost = GM_getValue('ankiHost', 'http://127.0.0.1:8765');
    let richTexts = [];
    let existsNoteId = 0;
    const setExistsNoteId = (id) => {
        existsNoteId = id;
        const update = document.querySelector('#force-update');
        if (id > 0) {
            update.parentElement.style.display = 'block';
        } else {
            update.parentElement.style.display = 'none';
            update.checked = false;
        }
    }
    const spellIconsTtf = GM_getResourceURL('spell-icons-ttf');
    const spellIconsWoff = GM_getResourceURL('spell-icons-woff');
    const spellCss = GM_getResourceText("spell-css")
        .replace('chrome-extension://__MSG_@@extension_id__/fg/font/spell-icons.ttf', spellIconsTtf)
        .replace('chrome-extension://__MSG_@@extension_id__/fg/font/spell-icons.woff', spellIconsWoff);
    const select2Css = GM_getResourceText("select2-css");
    const frameCss = GM_getResourceText("frame-css");
    const diagStyle = GM_getResourceText('diag-style');
    const beforeSaveHookFns = [], afterSaveHookFns = [];

    function PushAnkiBeforeSaveHook(...call) {
        beforeSaveHookFns.push(...call);
    }

    function PushAnkiAfterSaveHook(...call) {
        afterSaveHookFns.push(...call);
    }

    PushIconAction && PushIconAction({
        name: 'anki',
        icon: 'icon-anki',
        image: GM_getResourceURL('icon-anki'),
        trigger: (t) => {
            addAnki(getSelectionElement(), tapKeyboard).catch(res => console.log(res));
        }
    });

    async function queryAnki(expression) {
        let {result, error} = await anki('findNotes', {
            query: expression
        })
        if (error) {
            throw error;
        }
        if (result.length < 1) {
            return null
        }
        const res = await anki('notesInfo', {
            notes: result
        })
        if (res.error) {
            throw res.error;
        }
        return res.result;
    }

    function getSearchType(ev, type = null) {
        const value = ev.target.parentElement.previousElementSibling.value.trim();
        const field = ev.target.parentElement.parentElement.querySelector('.field-name').value;
        const deck = document.querySelector('#deckName').value;
        const sel = document.createElement('select');
        const inputs = ev.target.parentElement.previousElementSibling;
        sel.name = inputs.name;
        sel.className = inputs.className;
        const precision = `deck:${deck} "${field}:${value}"`;
        const str = value.split(' ');
        const vague = str.length > 1 ? str.map(v => `${field}:*${v}*`).join(' ') : `${field}:*${value}*`;
        const deckVague = `deck:${deck} ` + vague;
        if (type !== null) {
            return [vague, deckVague, precision, value][type];
        }
        const searchType = GM_getValue('searchType', 0);
        const m = {};
        const nbsp = '&nbsp;'.repeat(5);
        const options = [
            [vague, `模糊不指定组牌查询:   ${nbsp}${vague}`],
            [deckVague, `模糊指定组牌查询:    ${nbsp}${deckVague}`],
            [precision, `精确查询:    ${nbsp}${precision}`],
            [value, `自定义查询:    ${nbsp}${value}`],
        ].map((v, i) => {
            if (i === searchType) {
                const vv = v[1].split(':')[0];
                v[1] = v[1].replace(vv, vv + ' (默认)');
            }
            v[0] = htmlSpecial(v[0]);
            m[v[0]] = i;
            return v;
        });
        return {options, m}
    }

    const contextMenuFns = {
        'anki-search': async (ev) => {
            ev.preventDefault();
            const sel = document.createElement('select');
            const inputs = ev.target.parentElement.previousElementSibling;
            sel.name = inputs.name;
            sel.className = inputs.className;
            const {options, m} = getSearchType(ev);
            sel.innerHTML = buildOption(options, m[GM_getValue('searchType', 0)], 0, 1);
            inputs.parentElement.replaceChild(sel, inputs);
            sel.focus();
            const fn = () => {
                GM_setValue('searchType', m[htmlSpecial(sel.value)]);
                searchAnki(ev, sel.value, inputs, sel);
                sel.removeEventListener('blur', fn);
                sel.removeEventListener('change', fn);
            };
            sel.addEventListener('blur', fn)
            sel.addEventListener('change', fn)
        },
        'action-copy': async (ev) => {
            ev.preventDefault();
            const ele = ev.target.parentElement.previousElementSibling.querySelector('.spell-content');
            const item = new ClipboardItem({
                'text/html': new Blob([ele.innerHTML], {type: 'text/html'}),
                'text/plain': new Blob([ele.innerHTML], {type: 'text/plain'}),
            })
            await navigator.clipboard.write([item]).catch(console.log)
        }
    }

    function focusEle(ele, offset = 0) {
        const s = window.getSelection();
        const r = document.createRange();
        r.setStart(ele, offset);
        r.collapse(true);
        s.removeAllRanges();
        s.addRange(r);
        ele.focus();
    }

    const br = (() => {
        const div = document.createElement('div');
        div.innerHTML = '<br>';
        return div
    })();
    const clickFns = {
        'card-delete': async () => {
            if (confirm('确定删除么?')) {
                const {error} = await anki('deleteNotes', {notes: [existsNoteId]});
                if (error) {
                    Swal.showValidationMessage(error);
                    return
                }
                setExistsNoteId(0);
            }
        },
        'anki-search': (ev) => {
            const express = getSearchType(ev, GM_getValue('searchType', 0));
            const inputs = ev.target.parentElement.previousElementSibling;
            searchAnki(ev, express, inputs);
        },
        'word-wrap-first': (ev) => {
            const b = br.cloneNode(true);
            ev.target.parentElement.previousElementSibling.querySelector('.spell-content').insertAdjacentElement('afterbegin', b);
            focusEle(b);
        },
        'word-wrap-last': (ev) => {
            const b = br.cloneNode(true);
            ev.target.parentElement.previousElementSibling.querySelector('.spell-content').insertAdjacentElement('beforeend', b);
            focusEle(b);
        },
        'upperlowercase': (ev) => {
            const input = ev.target.parentElement.previousElementSibling;
            if (input.value === '') {
                return
            }
            const stats = input.dataset.stats;
            switch (stats) {
                case 'upper':
                    input.value = input.dataset.value;
                    input.dataset.stats = '';
                    break
                case 'lower':
                    input.value = input.value.toUpperCase();
                    input.dataset.stats = 'upper';
                    break
                default:
                    input.dataset.value = input.value;
                    input.value = input.value.toLowerCase();
                    input.dataset.stats = 'lower';
                    break
            }
        },
        'lemmatizer': (ev) => {
            const inputs = ev.target.parentElement.previousElementSibling;
            const words = inputs.value.split(' ');
            const word = inputs.value.split(' ')[0].toLowerCase();
            if (word === '') {
                return
            }
            const origin = lemmatizer.only_lemmas_withPos(word);
            if (origin.length < 1) {
                return
            }
            const last = words.length > 1 ? (' ' + words.slice(1).join(' ')) : '';
            if (origin.length === 1) {
                inputs.value = origin[0][0] + last;
                return
            }
            let wait = origin[0][0];
            [...origin].splice(1).map(v => wait = v[0] === origin[0][0] ? wait : v[0]);
            if (wait === origin[0][0]) {
                inputs.value = origin[0][0] + last
                return;
            }
            const all = origin.map(v => v[0] + last).join(' ');
            const ops = [...origin.map(v => [v[0] + last, `${v[1]}:${v[0]} ${last}`]), [all, all]];
            const options = buildOption(ops, '', 0, 1);
            const sel = document.createElement('select');
            sel.name = inputs.name;
            sel.className = inputs.className;
            sel.innerHTML = options;
            inputs.parentElement.replaceChild(sel, inputs);
            sel.focus();
            sel.onblur = () => {
                inputs.value = sel.value;
                sel.parentElement.replaceChild(inputs, sel);
            }
        },
        'text-clean': (ev) => {
            ev.target.parentElement.previousElementSibling.querySelector('.spell-content').innerHTML = '';
        },
        'paste-html': async (ev) => {
            ev.target.parentElement.previousElementSibling.querySelector('.spell-content').focus();
            await tapKeyboard('ctrl v');
        },
        'action-switch-text': (ev) => {
            const el = ev.target.parentElement.previousElementSibling.querySelector('.spell-content');
            if (el.tagName === 'DIV') {
                const text = el.innerHTML
                el.outerHTML = `<textarea class="${el.className}">${text}</textarea>`;
                ev.target.title = '切换为富文本'
            } else {
                const text = el.value
                el.outerHTML = `<div class="${el.className}" contenteditable="true">${text}</div>`;
                ev.target.title = '切换为textarea'
            }
        },
        'minus': (ev) => {
            ev.target.parentElement.parentElement.parentElement.removeChild(ev.target.parentElement.parentElement);
        },
        "action-copy": async (ev) => {
            const ele = ev.target.parentElement.previousElementSibling.querySelector('.spell-content');
            const html = await checkAndStoreMedia(ele.innerHTML);
            const item = new ClipboardItem({
                'text/html': new Blob([html], {type: 'text/html'}),
                'text/plain': new Blob([html], {type: 'text/plain'}),
            })
            await navigator.clipboard.write([item]).catch(console.log)
        },
    };

    async function searchAnki(ev, queryStr, inputs, sels = null) {
        const field = ev.target.parentElement.parentElement.querySelector('.field-name').value;
        let result;
        try {
            result = await queryAnki(queryStr);
            if (!result) {
                setExistsNoteId(0);
                sels && sels.parentElement.replaceChild(inputs, sels);
                return
            }
        } catch (e) {
            Swal.showValidationMessage(e);
            return
        }
        if (result.length === 1) {
            if (sels && sels.parentElement) {
                sels.parentElement.replaceChild(inputs, sels);
            }
            await showAnkiCard(result[0]);
            return
        }
        const sel = document.createElement('select');
        sel.name = inputs.name;
        sel.className = inputs.className;
        const values = {};
        const options = result.map(v => {
            values[v.fields[field].value] = v;
            return [v.fields[field].value, v.fields[field].value];
        });
        sel.innerHTML = buildOption(options, '', 0, 1);
        const ele = (sels && sels.parentElement) ? sels : inputs;
        if (!ele || !ele.parentElement) {
            return
        }
        ele.parentElement.replaceChild(sel, ele);
        sel.focus();
        const changeFn = async () => {
            inputs.value = sel.value;
            await showAnkiCard(values[sel.value]);
        }
        const blurFn = async () => {
            sel.removeEventListener('change', changeFn);
            inputs.value = sel.value;
            sel.parentElement.replaceChild(inputs, sel);
            await showAnkiCard(values[sel.value]);
        };
        sel.addEventListener('change', changeFn);
        sel.addEventListener('blur', blurFn);
        await showAnkiCard(result[0]);
    }

    const showFns = [];

    function PushShowFn(...fns) {
        showFns.push(...fns);
    }

    async function showAnkiCard(result) {
        setExistsNoteId(result.noteId);
        $('#tags').val(result.tags).trigger('change');
        const res = await anki('cardsInfo', {cards: [result.cards[0]]});
        if (res.error) {
            console.log(res.error);
        }
        if (res.result.length > 0) {
            document.querySelector('#deckName').value = res.result[0].deckName;
        }
        document.querySelector('#model').value = result.modelName;
        const sentenceInput = document.querySelector('#sentence_field');
        const sentence = sentenceInput.value;
        const fields = {
            [sentence]: sentenceInput,
        };
        [...document.querySelectorAll('#shadowFields input.field-name')].map(input => fields[input.value] = input);

        for (const k of Object.keys(result.fields)) {
            if (!fields.hasOwnProperty(k)) {
                continue;
            }
            const v = result.fields[k].value;
            if (fields[k].nextElementSibling.tagName === 'SELECT') {
                continue;
            }
            if (fields[k].nextElementSibling.tagName === 'INPUT') {
                fields[k].nextElementSibling.value = v;
                continue;
            }
            const div = document.createElement('div');
            div.innerHTML = v;
            for (const img of [...div.querySelectorAll('img')]) {
                if (!img.src) {
                    continue;
                }
                const srcs = (new URL(img.src)).pathname.split('/');
                const src = srcs[srcs.length - 1];
                let suffix = 'png';
                const name = src.split('.');
                suffix = name.length > 1 ? name[1] : suffix;
                const {result, error} = await anki('retrieveMediaFile', {'filename': src});
                if (error) {
                    console.log(error);
                    continue
                }
                if (!result) {
                    continue;
                }
                img.dataset.fileName = src;
                img.src = `data:image/${suffix};base64,` + result;
            }
            fields[k].parentElement.querySelector('.spell-content').innerHTML = div.innerHTML;
        }
        showFns.forEach(fn => fn(result, res));
    }

    function buildInput(rawStr = false, field = '', value = '', checked = false) {
        const li = document.createElement('div');
        const checkeds = checked ? 'checked' : '';
        li.className = 'form-item'
        li.innerHTML = createHtml(`
            <input name="shadow-form-field[]" placeholder="字段名" value="${field}" class="swal2-input field-name">
            <input name="shadow-form-value[]" value="${value}" placeholder="字段值" class="swal2-input field-value"> 
            <div class="field-operate">
                <button class="minus">➖</button>
                <input type="radio" title="选中赋值" ${checkeds} name="shadow-form-defaut[]">
                <button class="lemmatizer" title="lemmatize查找单词原型">📟</button>
                <button class="anki-search" title="search anki 左健搜索 右键选择搜索模式">🔍</button>
                <button class="upperlowercase" title="大小写转换">🔡</button>
                ${inputButtons.join('\n')} ${inputButtonFields[field] ? inputButtonFields[field].join('\n') : ''}

            </div>
        `);
        if (rawStr) {
            return li.outerHTML
        }
        document.querySelector('#shadowFields ol').appendChild(li)
    }

    const inputButtons = [], inputButtonFields = {}, buttonFields = {}, buttons = [];

    function PushButtonFn(type, className, button, clickFn, field = '', contextMenuFn = null) {
        if (!className) {
            return
        }
        const fields = type === 'input' ? inputButtonFields : buttonFields;
        const pushButtons = type === 'input' ? inputButtons : buttons;
        if (field) {
            fields[field] ? fields[field].push(button) : fields[field] = [button];
        } else {
            button && pushButtons.push(button);
        }

        if (clickFn) {
            const fn = clickFns[className];
            clickFns[className] = fn ? (ev) => clickFn(ev, fn) : clickFn;
        }
        if (contextMenuFn) {
            const fn = contextMenuFns[className];
            contextMenuFns[className] = fn ? (ev) => contextMenuFn(ev, fn) : contextMenuFn;
        }
    }

    function PushExpandAnkiInputButton(className, button, clickFn, field = '', contextMenuFn = null) {
        PushButtonFn('input', className, button, clickFn, field, contextMenuFn)
    }

    function PushExpandAnkiRichButton(className, button, clickFn, field = '', contextMenuFn = null) {
        PushButtonFn('rich', className, button, clickFn, field, contextMenuFn)
    }

    function buildTextarea(rawStr = false, field = '', value = '', checked = false) {
        const li = document.createElement('div');
        const checkeds = checked ? 'checked' : '';
        const richText = spell();
        li.className = 'form-item'
        li.innerHTML = createHtml(`
            <input name="shadow-form-field[]" placeholder="字段名" value="${field}" class="swal2-input field-name">
            <div class="wait-replace"></div>            
            <div class="field-operate">
                <button class="minus">➖</button>
                <input type="radio" title="选中赋值" ${checkeds} name="shadow-form-defaut[]">
                <button class="paste-html" title="粘贴">✍️</button>
                <button class="text-clean" title="清空">🧹</button>
                <button class="action-copy" title="复制innerHTML 左键处理图片 右键不处理">⭕</button>
                <button class="action-switch-text" title="切换为textrea">🖺</button>
                <button class="word-wrap-first" title="在首行换行">🔼</button>
                <button class="word-wrap-last" title="在最后换行">🔽</button>
                ${buttons.join('\n')} ${buttonFields[field] ? buttonFields[field].join('\n') : ''}
            </div>
        `);
        const editor = richText.querySelector('.spell-content');

        if (rawStr) {
            richTexts.push((ele) => {
                editor.innerHTML = value;
                enableImageResizeInDiv(editor);

                ele.parentElement.replaceChild(richText, ele);
            })
            return li.outerHTML
        }
        li.removeChild(li.querySelector('.wait-replace'));
        enableImageResizeInDiv(editor);
        editor.innerHTML = value;
        li.insertBefore(richText, li.querySelector('.field-operate'));
        document.querySelector('#shadowFields ol').appendChild(li);
    }

    const base64Reg = /(data:(.*?)\/(.*?);base64,(.*?)?)[^0-9a-zA-Z=\/+]/i;

    async function fetchImg(html) {
        const div = document.createElement('div');
        div.innerHTML = html;
        for (const img of div.querySelectorAll('img')) {
            if (img.dataset.hasOwnProperty('fileName') && img.dataset.fileName) {
                img.src = img.dataset.fileName;
                continue;
            }
            const prefix = GM_getValue('proxyPrefix', '')
            if (img.src.indexOf('http') === 0) {
                const name = img.src.split('/').pop().split('&')[0];
                const {error: err} = await anki('storeMediaFile', {
                    filename: name,
                    url: prefix ? (prefix + encodeURIComponent(img.src)) : img.src,
                    deleteExisting: false,
                })
                if (err) {
                    throw err
                }
                img.src = name
            }
        }
        return div.innerHTML
    }

    async function checkAndStoreMedia(text) {
        text = await fetchImg(text);
        while (true) {
            const r = base64Reg.exec(text);
            if (!r) {
                break
            }
            const sha = sha1(base64ToUint8Array(r[4]));
            const file = 'paste-' + sha + '.' + r[3];
            const {error: err} = await anki("storeMediaFile", {
                    filename: file,
                    data: r[4],
                    deleteExisting: false,
                }
            )
            if (err) {
                throw err;
            }
            text = text.replace(r[1], file);
        }
        return text
    }

    function anki(action, params = {}) {
        return new Promise(async (resolve, reject) => {
            await GM_xmlhttpRequest({
                method: 'POST',
                url: ankiHost,
                data: JSON.stringify({action, params, version: 6}),
                headers: {
                    "Content-Type": "application/json"
                },
                onload: (res) => {
                    resolve(JSON.parse(res.responseText));
                },
                onerror: reject,
            })
        })
    }

    let enableSentence, sentenceNum, sentenceBackup;
    const styles = [], htmls = [], closeFns = [], didRenderFns = [], changeFns = {
        ".sentence-format-setting": (ev) => {
            document.querySelector('.sentence-format').style.display = ev.target.checked ? 'block' : 'none';
        },
        "#auto-sentence": (ev) => {
            document.querySelector('.sample-sentence').style.display = ev.target.checked ? 'grid' : 'none';
            enableSentence = ev.target.checked
        },
        "#sentence_num": (ev) => {
            const {wordFormat, sentenceFormat} = sentenceFormatFn();
            const {sentence, offset, word,} = sentenceBackup;
            const num = parseInt(ev.target.value);
            document.querySelector('.sample-sentence .spell-content').innerHTML = cutSentence(word, offset, sentence, num, wordFormat, sentenceFormat);
            sentenceNum = num
        }
    };

    function PushHookAnkiClose(fn) {
        fn && closeFns.push(fn)
    }

    function PushHookAnkiDidRender(fn) {
        fn && didRenderFns.push(fn)
    }

    function PushHookAnkiChange(selector, fn) {
        if (!selector || !fn) {
            return;
        }
        const fnn = changeFns[selector];
        changeFns[selector] = fnn ? (ev) => {
            fn(ev, fnn)
        } : fn;
    }

    function PushHookAnkiStyle(style) {
        style && styles.push(style)
    }

    function PushHookAnkiHtml(htmlFn) {
        htmlFn && htmls.push(htmlFn)
    }

    function sentenceFormatFn() {
        let wordFormat = decodeHtmlSpecial(document.querySelector('.sentence_bold').value);
        if (!wordFormat) {
            wordFormat = '<b>{$bold}</b>';
        }
        let sentenceFormat = decodeHtmlSpecial(document.querySelector('.sentence_format').value);
        if (!sentenceFormat) {
            sentenceFormat = '<div>{$sentence}</div>'
        }
        return {
            wordFormat, sentenceFormat
        }
    }

    async function addAnki(value = '') {
        sentenceBackup = calSentence();
        let deckNames, models;
        existsNoteId = 0;
        if (typeof value === 'string') {
            value = value.trim();
        }
        try {
            const {result: deck} = await anki('deckNames');
            const {result: modelss} = await anki('modelNames');
            deckNames = deck;
            models = modelss;
        } catch (e) {
            console.log(e);
            deckNames = [];
            models = [];
            setTimeout(() => {
                Swal.showValidationMessage('无法获取anki的数据,请检查ankiconnect是否启动或者重新设置地址再点🔨');
            }, 1000);
        }
        const model = GM_getValue('model', '问答题');
        let modelFields = GM_getValue('modelFields-' + model, [[1, '正面', true], [2, '背面', false]]);
        const deckName = GM_getValue('deckName', '');
        enableSentence = GM_getValue('enableSentence', true);
        const sentenceField = GM_getValue('sentenceField', '句子');
        sentenceNum = GM_getValue('sentenceNum', 1);
        const lastValues = {ankiHost, model, deckName,}
        const deckNameOptions = buildOption(deckNames, deckName);
        const modelOptions = buildOption(models, model);

        const sentenceHtml = `<div class="wait-replace"></div>            
            <div class="field-operate">
                <button class="paste-html" title="粘贴">✍️</button>
                <button class="text-clean" title="清空">🧹</button>
                <button class="action-copy" title="复制innerHTML">⭕</button>
                <button class="action-switch-text" title="切换为textrea">🖺</button>
                ${buttons.join('\n')} ${buttonFields[sentenceField].join('\n')}
            </div>`
        const fieldFn = ['', buildInput, buildTextarea];
        const changeFn = ev => {
            for (const selector of Object.keys(changeFns)) {
                if (ev.target.matches(selector)) {
                    changeFns[selector](ev);
                    return;
                }
            }
            if (ev.target.id !== 'model' && ev.target.id !== 'ankiHost') {
                return
            }
            const filed = ev.target.id === 'model' ? ev.target.value : ev.target.parentElement.nextElementSibling.nextElementSibling.querySelector('#model').value;
            if (filed === '') {
                return;
            }
            const modelField = GM_getValue('modelFields-' + filed, [[1, '正面', false], [2, '背面', false]]);
            document.querySelector('#shadowFields ol').innerHTML = '';
            if (modelField.length > 0) {
                modelField.forEach(v => {
                    let t = value
                    if (value instanceof HTMLElement) {
                        t = v[0] === 2 ? value.innerHTML : htmlSpecial(value.innerText.trim());
                    }
                    fieldFn[v[0]](false, v[1], v[2] ? t : '', v[2]);
                })
            }
        }
        document.addEventListener('change', changeFn);
        const clickFn = async ev => {
            if (ev.target.id === 'shadowAddField') {
                const type = parseInt(document.getElementById('shadowField').value);
                fieldFn[type]()
                return
            }
            const className = ev.target.className;
            if (className === 'hammer') {
                ankiHost = ev.target.parentElement.previousElementSibling.value;
                GM_setValue('ankiHost', ankiHost);
                try {
                    const {result: deck} = await anki('deckNames');
                    const {result: modelss} = await anki('modelNames');
                    deckNames = deck;
                    models = modelss;
                    ev.target.parentElement.parentElement.nextElementSibling.querySelector('#deckName').innerHTML = buildOption(deckNames, deckName);
                    ev.target.parentElement.parentElement.nextElementSibling.nextElementSibling.querySelector('#model').innerHTML = buildOption(models, model);
                    Swal.resetValidationMessage();
                } catch (e) {
                    Swal.showValidationMessage('无法获取anki的数据,请检查ankiconnect是否启动或者重新设置地址再点🔨');
                    console.log(e);
                }
                return
            }
            clickFns.hasOwnProperty(className) && clickFns[className] && clickFns[className](ev);
        }
        document.addEventListener('click', clickFn);
        const contextMenuFn = (ev) => {
            contextMenuFns.hasOwnProperty(ev.target.className) && contextMenuFns[ev.target.className](ev);
        };
        document.addEventListener('contextmenu', contextMenuFn);
        const sentenceBold = GM_getValue('sentence_bold', '');
        const sentenceFormat = GM_getValue('sentence_format', '')
        let ol = '';
        if (modelFields.length > 0) {
            ol = modelFields.map(v => {
                let t = value
                if (value instanceof HTMLElement) {
                    t = v[0] === 2 ? value.innerHTML : htmlSpecial(value.innerText.trim());
                }
                return fieldFn[v[0]](true, v[1], v[2] ? t : '', v[2])
            }).join('\n')
        }
        const hookStyles = styles.length > 0 ? `<style>${styles.filter(v => v !== '').join('\n')}</style>` : '';

        const style = `<style>${select2Css} ${frameCss} ${spellCss} ${diagStyle} </style> ${hookStyles}`;
        const ankiHtml = `${style} 
    <div class="form-item">
        <label for="ankiHost" class="form-label">ankiConnect监听地址</label>
        <input id="ankiHost" value="${ankiHost}" placeholder="ankiConnector监听地址" class="swal2-input">
        <div class="field-operate">
                <button class="hammer">🔨</button>
            </div>
    </div>
    <div class="form-item">
        <label for="deckName" class="form-label">牌组</label>
        <select id="deckName" class="swal2-select">${deckNameOptions}</select>
    </div>
    <div class="form-item">
        <label for="model" class="form-label">模板</label>
        <select id="model" class="swal2-select">${modelOptions}</select>
    </div>
    
    <div class="form-item">
        <label for="tags" class="form-label">标签</label>
        <select class="swal2-select js-example-basic-multiple js-states form-control" id="tags"></select>
    </div>
    
    <div class="form-item">
        <label for="auto-sentence" class="form-label">自动提取句子</label>
        <input type="checkbox" ${enableSentence ? 'checked' : ''} class="swal2-checkbox" name="auto-sentence" id="auto-sentence">
    </div>
    
    <div class="form-item">
        <label for="shadowField" class="form-label">字段格式</label>
        <select id="shadowField" class="swal2-select">
            <option value="1">文本</option>
            <option value="2">富文本</option>
        </select>
        <button class="btn-add-field" id="shadowAddField">➕</button>
    </div>
    
    <div class="form-item" id="shadowFields">
        <ol>${ol}</ol>
    </div>
    <div class="form-item sample-sentence">
        <label class="form-label">句子</label>
        <div class="sentence_setting">   
            <label for="sentence_field" class="form-label">字段</label>
            <input type="text" value="${sentenceField}" id="sentence_field" placeholder="句子字段" class="swal2-input sentence_field" name="sentence_field" >       
            <label class="form-label" for="sentence_num">句子数量</label>
            <input type="number" min="0" id="sentence_num" value="${sentenceNum}" class="swal2-input sentence_field" placeholder="提取的句子数量">
            <input type="checkbox" class="sentence-format-setting swal2-checkbox" title="设置句子加粗和整句格式">
            <dd class="sentence-format">
                <input type="text" name="sentence_bold" value="${htmlSpecial(sentenceBold)}" class="sentence_bold sentence-format-input" title="加粗格式,默认: <b>{$bold}</b}" placeholder="加粗格式,默认: <b>{$bold}</b}">
                <input type="text" value="${htmlSpecial(sentenceFormat)}" name="sentence_format" class="sentence_format sentence-format-input" title="整句格式,默认: <div>{$sentence}</div>" placeholder="整句格式,默认: <div>{$sentence}</div>">
            </dd>
            ${sentenceHtml}
        </div>
    </div>
    
    <div class="form-item" style="display: none">
        <label for="force-update" class="form-label">更新</label>
        <input type="checkbox" class="swal2-checkbox" name="update" id="force-update">
        <input type="button" class="card-delete" value="删除">
    </div>`;
        const ankiContainer = document.createElement('div');
        ankiContainer.className = 'anki-container';
        ankiContainer.innerHTML = createHtml(ankiHtml);
        if (htmls.length > 0) {
            htmls.map(fn => fn(ankiContainer));
        }
        await Swal.fire({
            didRender: async () => {
                const eles = document.querySelectorAll('.wait-replace');
                if (eles.length > 0) {
                    richTexts.forEach((fn, index) => fn(eles[index]))
                }
                const se = document.querySelector('.sentence_setting .wait-replace');
                if (se) {
                    const editor = spell();
                    const {wordFormat, sentenceFormat} = sentenceFormatFn();
                    const {sentence, offset, word,} = sentenceBackup;
                    editor.querySelector('.spell-content').innerHTML = cutSentence(word, offset, sentence, sentenceNum, wordFormat, sentenceFormat);
                    se.parentElement.replaceChild(editor, se);
                    enableImageResizeInDiv(editor.querySelector('.spell-content'))
                }
                if (!enableSentence) {
                    document.querySelector('.sample-sentence').style.display = 'none';
                }
                let {result: tags} = await anki('getTags');
                tags = tags.map(v => {
                    return {id: v, text: v}
                });
                $('#tags').select2({
                    tags: true,
                    placeholder: '选择或输入标签',
                    data: tags,
                    tokenSeparators: [',', ' '],
                    multiple: true,
                });
                didRenderFns.length > 0 && didRenderFns.forEach(fn => fn());
            },
            title: "anki制卡",
            showCancelButton: true,
            width: '55rem',
            html: ankiContainer,
            focusConfirm: false,
            didDestroy: () => {
                richTexts = [];
                document.removeEventListener('click', clickFn);
                document.removeEventListener('change', changeFn);
                document.removeEventListener('contextmenu', contextMenuFn);
                closeFns.length > 0 && closeFns.map(fn => fn());
            },
            preConfirm: async () => {
                let r;
                try {
                    r = await ankiSave();
                } catch (e) {
                    Swal.showValidationMessage('发生出错:' + e);
                    return
                }
                const {res, modelField, form, params} = r;
                console.log(form, params, res);
                if (res.error !== null) {
                    Swal.showValidationMessage('发生出错:' + res.error);
                    return
                }
                Object.keys(lastValues).forEach(k => {
                    if (lastValues[k] !== form[k]) {
                        GM_setValue(k, form[k])
                    }
                });
                const {wordFormat, sentenceFormat} = sentenceFormatFn();
                [
                    [enableSentence, 'enableSentence'],
                    //[sentenceNum, 'sentenceNum'],
                    [document.querySelector('#sentence_field').value, 'sentenceField'],
                    [wordFormat, 'sentence_bold'],
                    [sentenceFormat, 'sentence_format'],
                ].forEach(v => {
                    if (v[0] !== GM_getValue(v[1])) {
                        GM_setValue(v[1], v[0])
                    }
                })
                if (modelField.length !== modelFields.length || !modelField.every((v, i) => v === modelFields[i])) {
                    GM_setValue('modelFields-' + form.model, modelField)
                }
                Swal.fire({
                    html: "操作成功",
                    timer: 500,
                });
            }
        });
    }

    async function getAnkiFormValue(formFields) {
        const form = {}, fields = {}, modelField = [];
        formFields.forEach(field => {
            form[field] = document.getElementById(field).value;
        });
        for (const div of [...document.querySelectorAll('#shadowFields > ol > div')]) {
            const name = div.children[0].value;
            if (name === '') {
                continue;
            }
            modelField.push([
                div.children[1].tagName === 'INPUT' ? 1 : 2,
                name,
                div.children[2].children[1].checked
            ]);
            if (div.children[1].tagName === 'INPUT') {
                fields[name] = decodeHtmlSpecial(div.children[1].value);
            } else {
                const el = div.querySelector('.spell-content');
                fields[name] = await checkAndStoreMedia(el.tagName === 'DIV' ? el.innerHTML : el.value)
            }
        }

        if (Object.values(form).map(v => v === '' ? 0 : 1).reduce((p, c) => p + c, 0) < Object.keys(form).length) {
            throw '还有参数为空!请检查!';
        }
        let tags = $('#tags').val();

        if (enableSentence) {
            const el = document.querySelector('.sentence_setting .spell-content');
            fields[document.querySelector('#sentence_field').value] = await checkAndStoreMedia(el.tagName === 'DIV' ? el.innerHTML : el.value);
        }
        const params = {
            "note": {
                "deckName": form.deckName,
                "modelName": form.model,
                "fields": fields,
                "tags": tags,
            }
        }
        return {
            params,
            modelField,
            form,
        }
    }

    async function ankiSave(fields = ['ankiHost', 'model', 'deckName'], update = 'updateNote') {
        const {params, modelField, form} = await getAnkiFormValue(fields);
        let res;
        if (existsNoteId > 0 && document.querySelector('#force-update').checked) {
            params.note.id = existsNoteId;
            beforeSaveHookFns.forEach(fn => {
                const note = fn(true, params.note);
                params.note = note ? note : params.note;
            });
            res = await anki(update, params)
        } else {
            beforeSaveHookFns.forEach(fn => {
                const note = fn(false, params.note);
                params.note = note ? note : params.note;
            });
            res = await anki('addNote', params);
        }
        afterSaveHookFns.forEach(fn => fn(res, params));
        if (res.error) {
            throw res.error;
        }
        return {
            res, modelField, form, params
        }
    }

    return {
        addAnki, getAnkiFormValue, ankiSave,
        anki, queryAnki, showAnkiCard,
        PushAnkiBeforeSaveHook, PushAnkiAfterSaveHook, PushExpandAnkiRichButton, PushExpandAnkiInputButton,
        PushHookAnkiStyle, PushHookAnkiHtml, PushHookAnkiClose, PushHookAnkiDidRender, PushShowFn, PushHookAnkiChange
    };

})();