// ==UserScript== // @name Anki_Search // @namespace https://github.com/yekingyan/anki_search_on_web/ // @version 1.0.8 // @description 同步搜索Anki上的内容,支持google、bing、yahoo、百度。依赖AnkiConnect(插件:2055492159) // @author Yekingyan // @run-at document-start // @include *://www.google.com/* // @include *://www.google.com.*/* // @include *://www.google.co.*/* // @include *://mijisou.com/* // @include *://*.bing.com/* // @include *://search.yahoo.com/* // @include *://www.baidu.com/* // @include *://ankiweb.net/* // @grant unsafeWindow // @downloadURL https://update.greasyfork.icu/scripts/379860/Anki_Search.user.js // @updateURL https://update.greasyfork.icu/scripts/379860/Anki_Search.meta.js // ==/UserScript== /** * version change * - fix replace target width */ const URL = "http://127.0.0.1:8765" const SEARCH_FROM = "-deck:English" const MAX_CARDS = 37 // set card size const MIN_CARD_WIDTH = 30 const MAX_CARD_WIDTH = 40 const MAX_CARD_HEIGHT = 70 const MAX_IMG_WIDTH = MAX_CARD_WIDTH - 3 // adaptor const HOST_MAP = new Map([ ["local", ["#anki-q", "#anki-card"]], ["google", ["#APjFqb", "#rhs"]], ["bing", ["#sb_form_q", "#b_context"]], ["yahoo", ["#yschsp", "#right"]], ["baidu", ["#kw", "#content_right"]], ["anki", [".form-control", "#content_right"]], ["mijisou", ["#q", "#sidebar_results"]], // ["duckduckgo", ["#search_form_input", ".results--sidebar"]], ]) const INPUT_WAIT_MS = 700 // utils function log() { console.log.apply(console, arguments) } function* counter() { /** * 计数器,统计请求次数 */ let val = 0 let skip = 0 while (true) { skip = yield val val = val + 1 + (skip === undefined ? 0 : skip) } } let g_counterReqText = counter() let g_counterReqSrc = counter() g_counterReqText.next() g_counterReqSrc.next() class Singleton { constructor() { const instance = this.constructor.instance if (instance) { return instance } this.constructor.instance = this } } // request and data class Api{ static _commonData(action, params) { /** * 请求表单的共同数据结构 * action: str findNotes notesInfo * params: dict * return: dict */ return { "action": action, "version": 6, "params": params } } static async _searchByText(searchText) { /** * 通过文本查卡片ID */ let query = `${SEARCH_FROM} ${searchText}` let data = this._commonData("findNotes", { "query": query }) try { let response = await fetch(URL, { method: "POST", body: JSON.stringify(data) }) g_counterReqText.next() return await response.json() } catch (error) { console.log("Request searchByText Failed", error) } } static async _searchByID(ids) { /** * 通过卡片ID获取卡片内容 */ let data = this._commonData("notesInfo", { "notes": ids }) try { let response = await fetch(URL, { method: "POST", body: JSON.stringify(data) }) g_counterReqText.next() return await response.json() } catch (error) { console.log("Request searchByID Failed", error) } } static async searchImg(filename) { /** * 搜索文件名 返回 资源的base64编码 * return base64 code */ let data = this._commonData("retrieveMediaFile", { "filename": filename }) try { let response = await fetch(URL, { method: "POST", body: JSON.stringify(data) }) let res = await response.json() g_counterReqSrc.next() return res.result } catch (error) { log("Request searchImg Failed", error, filename) } } static formatBase64Img(base64) { let src = `data:image/png;base64,${base64}` return src } static async searchImgBase64(filename) { let res = await this.searchImg(filename) let base64Img = this.formatBase64Img(res) return base64Img } static async search(searchText) { /** * 结合两次请求, 一次完整的搜索 * searchValue: 搜索框的内容 */ if (searchText.length === 0) { return [] } try { let idRes = await this._searchByText(searchText) let ids = idRes.result ids.length >= MAX_CARDS ? ids.length = MAX_CARDS : null let cardRes = await this._searchByID(ids) let cards = cardRes.result return cards } catch (error) { log("Request search Failed", error, searchText) } } } class Card { constructor(id, index, frontCardContent, backCardData, parent) { this.id = id this.index = index this.isfirstChild = index === 1 this.frontCardContent = frontCardContent // strContent this.backCardData = backCardData // [order, field, content] this.backCardData.sort((i, j) => i > j ? 1 : -1) this.parent = parent this._cardHTML = null this._title = null this.isExtend = null this.bodyDom = null this.titleDom = null } get title() { let title = "" let parseTitle = this.frontCardContent.split(//) let blankHead = parseTitle[0].split(/\s+/) //有div的情况 if (this.frontCardContent.includes("")) { // 第一个div之前不是全部都是空白,就是标题 if (!/^\s+$/.test(blankHead[0]) && blankHead[0] !== "") { title = blankHead } else { // 标题是第一个div标签的内容 title = parseTitle[1].split("")[0] } } else { //没有div的情况 title = this.frontCardContent } this._title = title title = this.index + "、" + title return title } get forntCard() { if (this._title === this.frontCardContent) { let arrow = `` let arrows = "" for (let index = 0; index < 4; index++) { arrows = arrows + arrow } return `
↓${arrows}
` } return this.frontCardContent } get backCard() { let back = "" if (this.backCardData.length <= 1) { back += this.backCardData[0][2] } else { this.backCardData.forEach(item => { let order, field, content [order, field, content] = item if (content.length > 0) { back += `
${field}
${content}

` } }) } return back } get templateCard() { let template = `
${this.title}
${this.forntCard}
${this.backCard}
` return template } get cardHTML() { if (!this._cardHTML) { throw "pls requestCardSrc first" } return this._cardHTML } set cardHTML(cardHTML) { this._cardHTML = cardHTML } async replaceImg(templateCard) { let reSrc = /src="(.*?)"/g let reFilename = /src="(?.*?)"/ let srcsList = templateCard.match(reSrc) let temp = templateCard if (!srcsList) { return temp } await Promise.all(srcsList.map(async (i) => { let filename = i.match(reFilename).groups.filename let base64Img = await Api.searchImgBase64(filename) let orgImg = ` this.onClick()) this.bodyDom = window.top.document.getElementById(`body-${this.id}`) this.bodyDom.addEventListener("animationend", () => this.onAniEnd()) } onClick() { this.parent.onCardClick(this) let show = !this.isExtend this.setExtend(show) } onAniEnd() { if (this.isExtend) { window.scroll(window.outerWidth, window.pageYOffset) } } onInsert() { this.listenEvent() this.tryCollapse() } } class CardMgr extends Singleton { constructor () { super() this.cards = [] } formatCardsData(cardsData) { /** turn cardData 2 cardObj */ let cards = [] cardsData.forEach((item, index) => { let id = item.noteId let frontCard = [] let backCards = [] for (const [k, v] of Object.entries(item.fields)) { if (v.order === 0) { frontCard = v.value continue } backCards.push([v.order, k, v.value]) } let card = new Card(id, index+1, frontCard, backCards, this) cards.push(card) }) return cards } insertCardsDom(cards) { if (!DomOper.getContainer()) { return } DomOper.clearContainer() cards.forEach(card => { DomOper.getContainer().insertAdjacentHTML("beforeend", card.cardHTML) card.onInsert() }) } async searchAndInsertCard(searchValue) { DomOper.insertContainerOnce() if (!DomOper.getContainer()) { return } let cardsData = await Api.search(searchValue) let cards = this.formatCardsData(cardsData) this.cards = cards await Promise.all(cards.map(async (card) => await card.requestCardSrc())) this.insertCardsDom(cards) log( `total req: ${g_counterReqText.next(-1).value + g_counterReqSrc.next(-1).value}\n`, `req searchText: ${g_counterReqText.next(-1).value}\n`, `req searchSrc: ${g_counterReqSrc.next(-1).value}\n`, ) } onCardClick(curCard) { this.cards.forEach( card => { if (card !== curCard) { card.setExtend(false) } }) } } // dom const REPLACE_TARGET_ID = "anki-replace-target" const REPLACE_TARGET = `
` const CONTAINER_ID = "anki-container" const CONTAINER = `
` class DomOper { static getHostSearchInputAndTarget() { /** * 获取当前网站的搜索输入框 与 需要插入的位置 * */ let host = window.location.host || "local" let searchInput = null // 搜索框 let targetDom = null // 左边栏的父节点 this.removeReplaceTargetDom() for (let [key, value] of HOST_MAP) { if (host.includes(key)) { searchInput = window.top.document.querySelector(value[0]) targetDom = window.top.document.querySelector(value[1]) break } } if (!targetDom) { targetDom = this.getOrCreateReplaceTargetDom() } return [searchInput, targetDom] } // listen input static addInputEventListener(searchInput) { function onSearchTextInput(event) { lastInputTs = event.timeStamp searchText = event.srcElement.value setTimeout(() => { if (event.timeStamp === lastInputTs) { new CardMgr().searchAndInsertCard(searchText) } }, INPUT_WAIT_MS) } let lastInputTs, searchText searchInput.addEventListener("input", onSearchTextInput) } static getReplaceTargetDom() { return window.top.document.getElementById(REPLACE_TARGET_ID) } static createReplaceTargetDom() { let targetDomParent = window.top.document.getElementById("rcnt") if (targetDomParent) { targetDomParent.insertAdjacentHTML("beforeend", REPLACE_TARGET) } } static getOrCreateReplaceTargetDom() { if (!this.getReplaceTargetDom()) { this.createReplaceTargetDom() } return this.getReplaceTargetDom() } static removeReplaceTargetDom () { if (!this.getReplaceTargetDom()) { return } this.getReplaceTargetDom().remove() } static insertCssStyle() { let headDom = window.top.document.getElementsByTagName("HEAD")[0] headDom.insertAdjacentHTML("beforeend", style) } static insertContainerOnce(targetDom) { if (this.getContainer()) { return } targetDom = targetDom ? targetDom : this.getHostSearchInputAndTarget()[1] if (!targetDom) { log("AKS can't insert cards container", targetDom) return } targetDom.insertAdjacentHTML("afterbegin", CONTAINER) this.insertCssStyle() } static getContainer() { return window.top.document.getElementById(CONTAINER_ID) } static clearContainer() { this.getContainer().innerHTML = "" } static replaceImgHTML(html, filename, base64Img) { let orgImg = ` /*card*/ .anki-card-size { min-width: ${MIN_CARD_WIDTH}em; max-width: ${MAX_CARD_WIDTH}em; max-height: ${MAX_CARD_HEIGHT}em; } .anki-img-width { max-width: ${MAX_IMG_WIDTH}em; } .anki-card { position: relative; display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; word-wrap: break-word; width:fit-content; width:-webkit-fit-content; width:-moz-fit-content; margin-bottom: .25em; border: .1em solid #69928f; // border-radius: calc(.7em - 1px); border-radius: .7em; } .anki-body { overflow-x: visible; overflow-y: auto; } /* card title */ .anki-title { padding: .75em; margin: 0em; font-weight: 700; color: black; background-color: #e0f6f9; // border-radius: calc(.5em - 1px); border-radius: .7em; transition-property: all; transition-duration: 1.5s; transition-timing-function: ease-out; } .anki-title-sel { animation-name: select-title; animation-duration: 5s; animation-iteration-count: infinite; animation-direction: alternate; } .anki-title:hover{ // background-color: #9791b1; background-color: #d2e4f9; } .anki-sub-title { color: #5F9EA0; } .anki-front-card { padding: .75em; border-bottom: solid .3em #c6e1e4; } .anki-back-card { padding: .75em .75em; } .anki-collapsed { overflow: hidden; animation-name: collapsed; animation-duration: .3s; animation-timing-function: ease-out; animation-fill-mode:forwards; animation-direction: normal; } .anki-extend { overflow-x: visible; animation-name: extend; animation-duration: .3s; animation-timing-function: ease-in; animation-fill-mode:forwards; animation-direction: normal; } div#anki-container ul { margin-bottom: 1em; margin-left: 2em; } div#anki-container ol { margin-bottom: 1em; margin-left: 2em; } div#anki-container ul li{ list-style-type: disc; } div#anki-container ul ul li{ list-style-type: circle; } div#anki-container ul ul ul li{ list-style-type: square; } div#anki-container ul ul ul ul li{ list-style-type: circle; } div#anki-replace-target { margin-left: 2em; width: ${MIN_CARD_WIDTH}em; max-width: ${MAX_CARD_WIDTH}em; float: right; display: block; position: relative; } @keyframes collapsed { 0% {max-height: ${MAX_CARD_HEIGHT}em; max-width: ${MAX_CARD_WIDTH}em;} 100% {max-height: 0em; max-width: 30em;} } @keyframes extend { 0% {max-height: 0em; max-width: ${MIN_CARD_WIDTH}em;} 100% {max-height: ${MAX_CARD_WIDTH}em; max-width: ${MAX_CARD_WIDTH}em;} } @keyframes select-title { 0% {background: #e0f6f9;} 50% {background: #e1ddf3;} 100% {background: #d2e4f9;} } /** * hljs css */ pre code.hljs { display: block; overflow-x: auto; padding: 1em } code.hljs { padding: 3px 5px } .hljs { color: #e0e2e4; background: #282b2e } .hljs-keyword, .hljs-literal, .hljs-selector-id, .hljs-selector-tag { color: #93c763 } .hljs-number { color: #ffcd22 } .hljs-attribute { color: #668bb0 } .hljs-link, .hljs-regexp { color: #d39745 } .hljs-meta { color: #557182 } .hljs-addition, .hljs-built_in, .hljs-bullet, .hljs-emphasis, .hljs-name, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-subst, .hljs-tag, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable { color: #8cbbad } .hljs-string, .hljs-symbol { color: #ec7600 } .hljs-comment, .hljs-deletion, .hljs-quote { color: #818e96 } .hljs-selector-class { color: #a082bd } .hljs-doctag, .hljs-keyword, .hljs-literal, .hljs-name, .hljs-section, .hljs-selector-tag, .hljs-strong, .hljs-title, .hljs-type { font-weight: 700 } .hljs-class .hljs-title, .hljs-code, .hljs-section, .hljs-title.class_ { color: #fff } `