您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
同步搜索Anki上的内容,支持google、bing、yahoo、百度。依赖AnkiConnect(插件:2055492159)
当前为
// ==UserScript== // @name Anki_Search // @namespace https://github.com/yekingyan/anki_search_on_web/ // @version 0.6 // @description 同步搜索Anki上的内容,支持google、bing、yahoo、百度。依赖AnkiConnect(插件:2055492159) // @author Yekingyan // @run-at document-start // @require https://code.jquery.com/jquery-3.3.1.min.js // @include https://www.google.com/* // @include https://www.google.com.*/* // @include https://www.google.co.*/* // @include https://www.bing.com/* // @include http://www.bing.com/* // @include https://cn.bing.com/* // @include https://search.yahoo.com/* // @include https://www.baidu.com/* // @include http://www.baidu.com/* // @include https://ankiweb.net/* // @grant unsafeWindow // ==/UserScript== (function () { 'use strict' //--------------------------------------------------------------------------------- // AnkiConnect(插件:2055492159)的接口 const local_url = 'http://127.0.0.1:8765' // 搜索范围设置 // 只搜English牌组,'deck:English' // 排除English牌组,'-deck:English' const SEARCH_FROM = '' // 卡页类型,正反面的名称 // 默认supermemo不用设置 // 目前只持支取两个字段 const FRONT_CARCD_FILES = '' const BACK_CARK_FILES = '' // 依赖 let requiredScript = [ ` <script src="https://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script> `, ` <style> /*card*/ .anki-mb-1 { margin-bottom: .25rem!important; } .anki-mb-1, .my-1 { margin-bottom: .25rem!important; } .anki-rounded { border-radius: .25rem!important; } /* .anki-border-success { border-color: #dfe1e5!important; } */ .anki-card { position: relative; display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; min-width: 0; word-wrap: break-word; background-color: #fff; background-clip: border-box; border: 1.5px solid #dfe1e5; border-radius: .25rem; } /* cardheader */ .anki-card-header:first-child { border-radius: calc(.25rem - 1px) calc(.25rem - 1px) 0 0; } .anki-font-weight-bold { font-weight: 700!important; } .bg-title { background-color: #c6e1e4!important; } .anki-card-header { padding: .75rem 1.25rem; margin-bottom: 0; background-color: rgba(0,0,0,.03); border-bottom: 1px solid rgba(0,0,0,.125); } /*card body*/ .text-success { color: #28a745!important; } .anki-card-body { -ms-flex: 1 1 auto; flex: 1 1 auto; padding: .75rem 1.25rem; border-bottom: solid 1px; } /*card footer*/ .card-footer:last-child { border-radius: 0 0 calc(.25rem - 1px) calc(.25rem - 1px); } .anki-bg-transparent { background-color: transparent!important; } .anki-card-footer { padding: .75rem 1.25rem; background-color: rgba(0,0,0,.03); border-top: 1px solid rgba(0,0,0,.125); } </style> ` ] // 图片等资源的路径, 注意windows要将 反斜杠\ 换成 斜杠/ // 需开启 web服务器 // const media_path = `C:/Users/y/AppData/Roaming/Anki2/用户1/collection.media/` // const media_url = 'file:///' + media_path let WHERE = '' const getHostSearchInputAndTarget = () => { /** * 获取当前网站的搜索输入框 与 需要插入的位置 * */ let host = window.location.host let searchInput = [undefined] // 搜索框 let targetDom = [undefined] // 左边栏的父节点 let HOST_MAP = new Map([ ['google', ['.gLFyf', '#rhs']], ['bing', ['#sb_form_q', '#b_context']], ['yahoo', ['#yschsp', '#right']], ['baidu', ['#kw', '#content_right']], ['anki', ['.form-control', '#content_right']], // ['duckduckgo', ['#search_form_input', '.results--sidebar']], ]) for (let [key, value] of HOST_MAP) { if (host.includes(key)) { WHERE = key searchInput = $(value[0], window.top.document) targetDom = $(value[1], window.top.document) break } } return [searchInput, targetDom] } const mylog = function () { console.log.apply(console, arguments) } const commonData = (action, params) => { /** * 请求的共同数据结构 * action: str findNotes notesInfo * params: dict * return: dict */ return { "action": action, "version": 6, "params": params } } const searchByFileName = (filename) => { /** * 搜索文件名 返回 资源的base64编码 * return base64 code */ // gif 太大了,不要 // if (filename.includes('.gif')) { // return // } let data = commonData('retrieveMediaFile', { 'filename': filename }) data = JSON.stringify(data) const _searchByFileName = new Promise((resolve, reject) => { $.post(local_url, data) .done((res) => { resolve([filename, res.result]) }) .fail((err) => { reject(err) }) }) srcCount = getSrcCount.next().value return _searchByFileName } const searchByTest = (searchText) => { /** * 搜索文字返回含卡片ID的数组 * searchText: str 要搜索的内容 * from: str 搜索特定的牌组 * callback: array noteIds */ let from = '-deck:English' if (SEARCH_FROM) { from = SEARCH_FROM } let query = `${from} ${searchText}` let data = commonData('findNotes', { 'query': query }) data = JSON.stringify(data) const _searchByTest = new Promise((resolve, reject) => { $.post(local_url, data) .done((res) => { // 只要前37个结果 res.result.length >= 37 ? res.result.length = 37 : null resolve(res.result) }) .fail((err) => { reject(err) }) }) idCount = getIdCount.next().value return _searchByTest } const searchByIds = (ids) => { /** * 通过卡片Id获取卡片列表 * callback: cards array */ let notes = ids let data = commonData('notesInfo', { 'notes': notes }) data = JSON.stringify(data) const _searchByIds = new Promise((resolve, reject) => { $.post(local_url, data) .done((res) => { resolve(res.result) }) .fail((err) => { reject(err) }) }) detailCount = getDetailCount.next().value return _searchByIds } const search = (searchValue, callback) => { /** * 结合两次请求, 一次完整的搜索 * searchByTest() --> searchByIds() * searchValue: 搜索框的内容 * callback: cards: array */ if (!searchValue.length) { return } searchByTest(searchValue).then((ids) => { searchByIds(ids).then((cards) => { callback(cards) console.log( '总请求次数:', idCount + detailCount + srcCount, '\n', 'src请求次数:', srcCount + '\n', 'detail请求次数', detailCount + '\n', 'id请求次数', idCount + '\n' , ) }) }) } function* next_id() { /** * 计数器,统计每个接口请求的次数 */ let current_id = 0 while (true) { current_id++ yield current_id } } let lastCount = next_id() let getIdCount = next_id() let getDetailCount = next_id() let getSrcCount = next_id() let idCount = 0 let detailCount = 0 let srcCount = 0 let templateItem = (id, title, frontCard, backCard, show = 'show') => { let template = ` <div class="anki-card anki-border-success anki-mb-1 anki-rounded" style="min-width: 35rem; max-width: 50rem; width:fit-content; width:-webkit-fit-content; width:-moz-fit-content;"> <div class="anki-card-header bg-title anki-font-weight-bold collapsed" id="heading${id}" data-toggle="collapse" aria-expanded="false" data-target="#collapse${id}" aria-controls="collapse${id}"> ${title} </div> <div class="collapse ${show}" id="collapse${id}" aria-labelledby="heading${id}" data-parent="#accordionCard"> <div class="anki-card-body text-success anki-border-success" >${frontCard}</div> <div class="anki-card-footer anki-bg-transparent anki-border-success">${backCard}</div> </div> </div> ` return template } // 容器 const container = `<div id="accordionCard"><div>` const insertContainet = (targetDom) => { /** * 插入容器到页面 * */ let containerDiv = $.parseHTML(container) let father = $('#accordionCard', window.top.document) if (Object.keys(father).length <= 2) { // 根据不同网站加入容器 targetDom[0].prepend(containerDiv[0]) } } const insertCards = (domsArray) => { /** * 将节点插入到页面中 * domsArray: array dom节点列表 * targetDom: 要在页面中依附的元素 */ // 多次搜索清空旧结果 let father = $('#accordionCard', window.top.document) father.empty() // 添加搜索结果到容器内 let str, imageDom let fit = 'width:fit-content; width:-webkit-fit-content; width:-moz-fit-content;' domsArray.forEach((item, index) => { father.append(item) // 卡片样式在各站点的适配 switch (WHERE) { // case 'google': $(item).attr('style', 'min-width: 40rem; max-width: 45rem;' + fit) // break case 'bing': $(item).attr('style', 'min-width: 28rem; max-width: 45rem;' + fit) break case 'baidu': // 如果想适配百度,需把css的rem 换算,百度的rem 有毒 $(item).attr('style', 'min-width: 400px; max-width: 600px;' + fit) $(item).find('.anki-card-footer').attr('style', 'padding: 9.75px 16.25px;') $(item).find('.anki-card-body').attr('style', 'padding: 9.75px 16.25px;') $(item).find('.anki-card-header').attr('style', 'padding: 9.75px 16.25px;') break // default: $(item).attr('style', 'min-width: 35rem; max-width: 45rem;' + fit) // break } // 卡片加入时只显示标题 let collapse = $(item).find('.collapse') if (index !== 0) { collapse.hide() } else { lastClick = collapse } // 获取卡片的str, 用于更替src资源 str = $(item[1]).html() collectSrc(str, (filename, data) => { imageDom = $(`img[src="${filename}"]`, window.top.document) // dom 更替src属性 imageDom.attr('src', data) //样式 限制图片宽度 imageDom.attr('style', ' max-width: 520px;') }) }) } const getTittleFromFrontCard = (index, frontCard) => { /** * 通过FrontCard生成简短的标题, * 并根据标题重新定义frontCard */ let title = '' let parseTitle = frontCard.split(/<div.*?>/) let blanckHead = parseTitle[0].split(/\s+/) //有div的情况 if (frontCard.includes('</div>')) { // 第一个div之前不是全部都是空白,就是标题 if (!/^\s+$/.test(blanckHead[0]) && blanckHead[0] !== '') { title = blanckHead } else { // 标题是第一个div标签的内容 title = parseTitle[1].split('</div>')[0] } } else { //没有div的情况 title = frontCard let arrows = `<span style="padding-left: 4.5em">↓</span>` frontCard = arrows + arrows + arrows + arrows } title = index + '、' + title return [frontCard, title] } const src_base64 = (base64) => { let src = ` data:image/png;base64,${base64} ` return src } // const replaceDivSrc = (str, s_b_map) => { // /** // * 更替div中的src属性 // */ // let tempStr = str // for (let i of s_b_map) { // tempStr = tempStr.replace(i[0], i[i]) // } // return tempStr // } const collectSrc = (str, callback) => { /** * 将str资源文件名,提取出来, 并对应base64资源 * callback filename, base64 */ let src, base64, data // 找出 src="**.jpg" let re_src = /src="(.*?)"/gm let srcsList = str.match(re_src) if (!srcsList) { // 没有资源的卡片 return str } // 找出**.jpg let filename srcsList.forEach(item => { filename = item.split('"')[1].split('"')[0] // 查文件询名对应的资源 searchByFileName(filename).then(results => { // src -> data base64 = results[1] data = src_base64(base64) // data replace src callback(results[0], data) }) }) } const resolveCars = (cards, targetDom) => { /** * 处理卡片信息 * cards: array * */ let id, title, frontCard, backCard, show, isFirst, itemDivs, fileds_f, fileds_b FRONT_CARCD_FILES ? fileds_f = FRONT_CARCD_FILES : fileds_f = '正面' BACK_CARK_FILES ? fileds_b = BACK_CARK_FILES : fileds_b = '背面' isFirst = true itemDivs = [] cards.forEach((item, index) => { if (isFirst) { // 是否展开,展开第一个 show = 'show' isFirst = false } else { show = '' } id = item.noteId frontCard = item.fields[fileds_f]['value'] backCard = item.fields[fileds_b]['value'] ; ([frontCard, title] = getTittleFromFrontCard(index + 1, frontCard)) let strDiv = templateItem(id, title, frontCard, backCard, show) let itemDiv = $.parseHTML(strDiv) itemDivs.push(itemDiv) }) // 处理收集的itemDivs,插入到页面中 insertCards(itemDivs) } const seacrhAddEventListener = (searchInput) => { /** * 搜索框有内容变化触发更新 */ let lastSearchText = '' // 最新一次请求的搜索的参数 let lastTime // 最后触发事件的时间 searchInput.on({ // 有输入行为 input: function (e) { // 相同的内容就不请求了 if (lastSearchText !== searchInput.val()) { // 0.7秒内没有新的事件触发,才发一次结合请求 lastTime = e.timeStamp setTimeout(() => { if (lastTime - e.timeStamp == 0) { lastSearchText = searchInput.val() search(lastSearchText, (cards) => { console.log('inputEven request tiems', lastCount.next().value, searchInput.val(), cards) resolveCars(cards) }) } }, 700) } }, // 失焦触发请求 change: function () { console.log('change request', searchInput.val()) if (lastSearchText !== searchInput.val()) { search(searchInput.val(), (cards) => { resolveCars(cards) }) } }, }) } let lastClick // 记录最后一次显示或隐藏的卡片 const switchCardAddEventListener = () => { /** * 控制卡片风手琴 */ $('#accordionCard', window.top.document).on('click', '.collapsed', function (event) { let cardTitle = $(event.target) let targetId = cardTitle.data('target') let collapse = $(targetId, window.top.document) //目标元素的显示与隐藏 collapse.toggle(500) collapse.toggleClass('show') // 上一个元素的隐藏,如果是自身则不操作 if (lastClick && lastClick.attr('id') !== collapse.attr('id')) { // 如果有 show的class 则去掉并隐藏 lastClick.hide(500) lastClick.removeClass('show') } // 如果目标卡片是打开状态,标志 if (collapse.hasClass('show')) { lastClick = collapse } }) } $(window.top.document).ready(() => { // 注入脚本 if (location.host === "www.baidu.com") { // 不注入jquery requiredScript = requiredScript.slice(1) } let html = $.parseHTML(requiredScript.join('\n'), window.top.document, true) $('body', window.top.document).append(html) }) $(document).ready(() => { // 获取输入框 与 搜索值 let [searchInput, targetDom] = getHostSearchInputAndTarget() // 终止搜索 if (!searchInput[0]) { console.log('在页面没有找到搜索框', searchInput) return } if (!targetDom[0]) { console.log('在页面没有找到可依附的元素', targetDom) return } // 插入容器到页面 insertContainet(targetDom) // 刷新,搜索一次 search(searchInput.val(), (cards) => { resolveCars(cards) }) // 输入框的搜索事件监听 seacrhAddEventListener(searchInput) // 控制卡片风手琴的事件监听 switchCardAddEventListener() // 最右 }) /** * div class="med" id="res" 左侧搜索结果 * 左边 rhs_block * * * <iframe src="chrome-extension://pioclpoplcdbaefihamjohnefbikjilc/SimSearchFrame.html" id="simSearchFrame" style="width: 454px; height: 265px; border: none;"></iframe> * */ const test = (condition, e) => { if (!condition) { console.log(e) } } //----------------------------------------------------------------------- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址