Anki_Search

同步搜索Anki上的内容,支持google、bing、yahoo、百度。依赖AnkiConnect(插件:2055492159)

  1. // ==UserScript==
  2. // @name Anki_Search
  3. // @namespace https://github.com/yekingyan/anki_search_on_web/
  4. // @version 1.0.8
  5. // @description 同步搜索Anki上的内容,支持google、bing、yahoo、百度。依赖AnkiConnect(插件:2055492159)
  6. // @author Yekingyan
  7. // @run-at document-start
  8. // @include *://www.google.com/*
  9. // @include *://www.google.com.*/*
  10. // @include *://www.google.co.*/*
  11. // @include *://mijisou.com/*
  12. // @include *://*.bing.com/*
  13. // @include *://search.yahoo.com/*
  14. // @include *://www.baidu.com/*
  15. // @include *://ankiweb.net/*
  16. // @grant unsafeWindow
  17. // ==/UserScript==
  18.  
  19. /**
  20. * version change
  21. * - fix replace target width
  22. */
  23.  
  24. const URL = "http://127.0.0.1:8765"
  25. const SEARCH_FROM = "-deck:English"
  26. const MAX_CARDS = 37
  27.  
  28. // set card size
  29. const MIN_CARD_WIDTH = 30
  30. const MAX_CARD_WIDTH = 40
  31. const MAX_CARD_HEIGHT = 70
  32. const MAX_IMG_WIDTH = MAX_CARD_WIDTH - 3
  33.  
  34. // adaptor
  35. const HOST_MAP = new Map([
  36. ["local", ["#anki-q", "#anki-card"]],
  37. ["google", ["#APjFqb", "#rhs"]],
  38. ["bing", ["#sb_form_q", "#b_context"]],
  39. ["yahoo", ["#yschsp", "#right"]],
  40. ["baidu", ["#kw", "#content_right"]],
  41. ["anki", [".form-control", "#content_right"]],
  42. ["mijisou", ["#q", "#sidebar_results"]],
  43. // ["duckduckgo", ["#search_form_input", ".results--sidebar"]],
  44. ])
  45.  
  46. const INPUT_WAIT_MS = 700
  47.  
  48.  
  49. // utils
  50. function log() {
  51. console.log.apply(console, arguments)
  52. }
  53.  
  54.  
  55. function* counter() {
  56. /**
  57. * 计数器,统计请求次数
  58. */
  59. let val = 0
  60. let skip = 0
  61. while (true) {
  62. skip = yield val
  63. val = val + 1 + (skip === undefined ? 0 : skip)
  64. }
  65. }
  66. let g_counterReqText = counter()
  67. let g_counterReqSrc = counter()
  68. g_counterReqText.next()
  69. g_counterReqSrc.next()
  70.  
  71.  
  72. class Singleton {
  73. constructor() {
  74. const instance = this.constructor.instance
  75. if (instance) {
  76. return instance
  77. }
  78. this.constructor.instance = this
  79. }
  80. }
  81.  
  82.  
  83. // request and data
  84. class Api{
  85. static _commonData(action, params) {
  86. /**
  87. * 请求表单的共同数据结构
  88. * action: str findNotes notesInfo
  89. * params: dict
  90. * return: dict
  91. */
  92. return {
  93. "action": action,
  94. "version": 6,
  95. "params": params
  96. }
  97. }
  98.  
  99. static async _searchByText(searchText) {
  100. /**
  101. * 通过文本查卡片ID
  102. */
  103. let query = `${SEARCH_FROM} ${searchText}`
  104. let data = this._commonData("findNotes", { "query": query })
  105. try {
  106. let response = await fetch(URL, {
  107. method: "POST",
  108. body: JSON.stringify(data)
  109. })
  110. g_counterReqText.next()
  111. return await response.json()
  112. } catch (error) {
  113. console.log("Request searchByText Failed", error)
  114. }
  115. }
  116.  
  117. static async _searchByID(ids) {
  118. /**
  119. * 通过卡片ID获取卡片内容
  120. */
  121. let data = this._commonData("notesInfo", { "notes": ids })
  122. try {
  123. let response = await fetch(URL, {
  124. method: "POST",
  125. body: JSON.stringify(data)
  126. })
  127. g_counterReqText.next()
  128. return await response.json()
  129. } catch (error) {
  130. console.log("Request searchByID Failed", error)
  131. }
  132. }
  133.  
  134. static async searchImg(filename) {
  135. /**
  136. * 搜索文件名 返回 资源的base64编码
  137. * return base64 code
  138. */
  139. let data = this._commonData("retrieveMediaFile", { "filename": filename })
  140. try {
  141. let response = await fetch(URL, {
  142. method: "POST",
  143. body: JSON.stringify(data)
  144. })
  145. let res = await response.json()
  146. g_counterReqSrc.next()
  147. return res.result
  148. } catch (error) {
  149. log("Request searchImg Failed", error, filename)
  150. }
  151. }
  152.  
  153. static formatBase64Img(base64) {
  154. let src = `data:image/png;base64,${base64}`
  155. return src
  156. }
  157.  
  158. static async searchImgBase64(filename) {
  159. let res = await this.searchImg(filename)
  160. let base64Img = this.formatBase64Img(res)
  161. return base64Img
  162. }
  163.  
  164. static async search(searchText) {
  165. /**
  166. * 结合两次请求, 一次完整的搜索
  167. * searchValue: 搜索框的内容
  168. */
  169. if (searchText.length === 0) {
  170. return []
  171. }
  172. try {
  173. let idRes = await this._searchByText(searchText)
  174. let ids = idRes.result
  175. ids.length >= MAX_CARDS ? ids.length = MAX_CARDS : null
  176. let cardRes = await this._searchByID(ids)
  177. let cards = cardRes.result
  178. return cards
  179. } catch (error) {
  180. log("Request search Failed", error, searchText)
  181. }
  182. }
  183. }
  184.  
  185.  
  186. class Card {
  187. constructor(id, index, frontCardContent, backCardData, parent) {
  188. this.id = id
  189. this.index = index
  190. this.isfirstChild = index === 1
  191. this.frontCardContent = frontCardContent // strContent
  192. this.backCardData = backCardData // [order, field, content]
  193. this.backCardData.sort((i, j) => i > j ? 1 : -1)
  194. this.parent = parent
  195.  
  196. this._cardHTML = null
  197. this._title = null
  198. this.isExtend = null
  199. this.bodyDom = null
  200. this.titleDom = null
  201. }
  202.  
  203. get title() {
  204. let title = ""
  205. let parseTitle = this.frontCardContent.split(/<div.*?>/)
  206. let blankHead = parseTitle[0].split(/\s+/)
  207. //有div的情况
  208. if (this.frontCardContent.includes("</div>")) {
  209. // 第一个div之前不是全部都是空白,就是标题
  210. if (!/^\s+$/.test(blankHead[0]) && blankHead[0] !== "") {
  211. title = blankHead
  212. } else {
  213. // 标题是第一个div标签的内容
  214. title = parseTitle[1].split("</div>")[0]
  215. }
  216. } else {
  217. //没有div的情况
  218. title = this.frontCardContent
  219. }
  220. this._title = title
  221. title = this.index + "、" + title
  222. return title
  223. }
  224.  
  225. get forntCard() {
  226. if (this._title === this.frontCardContent) {
  227. let arrow = `<span style="padding-left: 4.5em;">↓</span>`
  228. let arrows = ""
  229. for (let index = 0; index < 4; index++) {
  230. arrows = arrows + arrow
  231. }
  232. return `<div style="text-align: center;">↓${arrows}</div>`
  233. }
  234. return this.frontCardContent
  235. }
  236.  
  237. get backCard() {
  238. let back = ""
  239. if (this.backCardData.length <= 1) {
  240. back += this.backCardData[0][2]
  241. } else {
  242. this.backCardData.forEach(item => {
  243. let order, field, content
  244. [order, field, content] = item
  245. if (content.length > 0) {
  246. back += `<div class="anki-sub-title"><em>${field}</em></div>
  247. <div calss="anki-sub-back-card">${content}</div><br>`
  248. }
  249. })
  250. }
  251. return back
  252. }
  253.  
  254. get templateCard() {
  255. let template = `
  256. <div class="anki-card anki-card-size">
  257. <div class="anki-title" id="title-${this.id}">${this.title}</div>
  258. <div class="anki-body" id="body-${this.id}">
  259. <div class="anki-front-card">${this.forntCard}</div>
  260. <div class="anki-back-card">${this.backCard}</div>
  261. </div>
  262. </div>
  263. `
  264. return template
  265. }
  266.  
  267. get cardHTML() {
  268. if (!this._cardHTML) {
  269. throw "pls requestCardSrc first"
  270. }
  271. return this._cardHTML
  272. }
  273.  
  274. set cardHTML(cardHTML) {
  275. this._cardHTML = cardHTML
  276. }
  277.  
  278. async replaceImg(templateCard) {
  279. let reSrc = /src="(.*?)"/g
  280. let reFilename = /src="(?<filename>.*?)"/
  281. let srcsList = templateCard.match(reSrc)
  282. let temp = templateCard
  283.  
  284. if (!srcsList) {
  285. return temp
  286. }
  287.  
  288. await Promise.all(srcsList.map(async (i) => {
  289. let filename = i.match(reFilename).groups.filename
  290. let base64Img = await Api.searchImgBase64(filename)
  291. let orgImg = `<img src="${filename}"`
  292. let replaceImg = `<img class="anki-img-width" src="${base64Img}"`
  293. temp = temp.replace(orgImg, replaceImg)
  294. }))
  295.  
  296. return temp
  297. }
  298.  
  299. async requestCardSrc() {
  300. let templateCard = await this.replaceImg(this.templateCard)
  301. this.cardHTML = templateCard
  302. return templateCard
  303. }
  304.  
  305. showSelTitleClass(show) {
  306. let selTitleClass = "anki-title-sel"
  307. show
  308. ? this.titleDom.classList.add(selTitleClass)
  309. : this.titleDom.classList.remove(selTitleClass)
  310. }
  311.  
  312. setExtend(show) {
  313. if (this.isExtend === show) {
  314. return
  315. } else {
  316. let hideClass = "anki-collapsed"
  317. let showClass = "anki-extend"
  318. if (show) {
  319. this.bodyDom.classList.add(showClass)
  320. this.bodyDom.classList.remove(hideClass)
  321. } else {
  322. this.bodyDom.classList.add(hideClass)
  323. this.bodyDom.classList.remove(showClass)
  324. }
  325.  
  326. this.isExtend = show
  327. this.showSelTitleClass(show)
  328. }
  329. }
  330.  
  331. tryCollapse() {
  332. if (!this.isfirstChild) {
  333. this.setExtend(false)
  334. return
  335. }
  336. this.isExtend = true
  337. this.showSelTitleClass(true)
  338. }
  339.  
  340. listenEvent() {
  341. this.titleDom = window.top.document.getElementById(`title-${this.id}`)
  342. this.titleDom.addEventListener("click", () => this.onClick())
  343.  
  344. this.bodyDom = window.top.document.getElementById(`body-${this.id}`)
  345. this.bodyDom.addEventListener("animationend", () => this.onAniEnd())
  346. }
  347.  
  348. onClick() {
  349. this.parent.onCardClick(this)
  350. let show = !this.isExtend
  351. this.setExtend(show)
  352. }
  353.  
  354. onAniEnd() {
  355. if (this.isExtend) {
  356. window.scroll(window.outerWidth, window.pageYOffset)
  357. }
  358. }
  359.  
  360. onInsert() {
  361. this.listenEvent()
  362. this.tryCollapse()
  363. }
  364.  
  365. }
  366.  
  367.  
  368. class CardMgr extends Singleton {
  369. constructor () {
  370. super()
  371. this.cards = []
  372. }
  373.  
  374. formatCardsData(cardsData) {
  375. /** turn cardData 2 cardObj */
  376. let cards = []
  377. cardsData.forEach((item, index) => {
  378. let id = item.noteId
  379. let frontCard = []
  380. let backCards = []
  381. for (const [k, v] of Object.entries(item.fields)) {
  382. if (v.order === 0) {
  383. frontCard = v.value
  384. continue
  385. }
  386. backCards.push([v.order, k, v.value])
  387. }
  388. let card = new Card(id, index+1, frontCard, backCards, this)
  389. cards.push(card)
  390. })
  391. return cards
  392. }
  393.  
  394. insertCardsDom(cards) {
  395. if (!DomOper.getContainer()) {
  396. return
  397. }
  398. DomOper.clearContainer()
  399. cards.forEach(card => {
  400. DomOper.getContainer().insertAdjacentHTML("beforeend", card.cardHTML)
  401. card.onInsert()
  402. })
  403. }
  404.  
  405. async searchAndInsertCard(searchValue) {
  406. DomOper.insertContainerOnce()
  407. if (!DomOper.getContainer()) {
  408. return
  409. }
  410. let cardsData = await Api.search(searchValue)
  411. let cards = this.formatCardsData(cardsData)
  412. this.cards = cards
  413. await Promise.all(cards.map(async (card) => await card.requestCardSrc()))
  414. this.insertCardsDom(cards)
  415. log(
  416. `total req: ${g_counterReqText.next(-1).value + g_counterReqSrc.next(-1).value}\n`,
  417. `req searchText: ${g_counterReqText.next(-1).value}\n`,
  418. `req searchSrc: ${g_counterReqSrc.next(-1).value}\n`,
  419. )
  420. }
  421.  
  422. onCardClick(curCard) {
  423. this.cards.forEach( card => {
  424. if (card !== curCard) {
  425. card.setExtend(false)
  426. }
  427. })
  428. }
  429.  
  430. }
  431.  
  432.  
  433. // dom
  434. const REPLACE_TARGET_ID = "anki-replace-target"
  435. const REPLACE_TARGET = `<div id="${REPLACE_TARGET_ID}"><div>`
  436.  
  437. const CONTAINER_ID = "anki-container"
  438. const CONTAINER = `<div id="${CONTAINER_ID}"><div>`
  439.  
  440. class DomOper {
  441. static getHostSearchInputAndTarget() {
  442. /**
  443. * 获取当前网站的搜索输入框 与 需要插入的位置
  444. * */
  445. let host = window.location.host || "local"
  446. let searchInput = null // 搜索框
  447. let targetDom = null // 左边栏的父节点
  448. this.removeReplaceTargetDom()
  449.  
  450. for (let [key, value] of HOST_MAP) {
  451. if (host.includes(key)) {
  452. searchInput = window.top.document.querySelector(value[0])
  453. targetDom = window.top.document.querySelector(value[1])
  454. break
  455. }
  456. }
  457. if (!targetDom) {
  458. targetDom = this.getOrCreateReplaceTargetDom()
  459. }
  460.  
  461. return [searchInput, targetDom]
  462. }
  463.  
  464. // listen input
  465. static addInputEventListener(searchInput) {
  466. function onSearchTextInput(event) {
  467. lastInputTs = event.timeStamp
  468. searchText = event.srcElement.value
  469. setTimeout(() => {
  470. if (event.timeStamp === lastInputTs) {
  471. new CardMgr().searchAndInsertCard(searchText)
  472. }
  473. }, INPUT_WAIT_MS)
  474. }
  475. let lastInputTs, searchText
  476. searchInput.addEventListener("input", onSearchTextInput)
  477. }
  478.  
  479. static getReplaceTargetDom() {
  480. return window.top.document.getElementById(REPLACE_TARGET_ID)
  481. }
  482.  
  483. static createReplaceTargetDom() {
  484. let targetDomParent = window.top.document.getElementById("rcnt")
  485. if (targetDomParent) {
  486. targetDomParent.insertAdjacentHTML("beforeend", REPLACE_TARGET)
  487. }
  488. }
  489.  
  490. static getOrCreateReplaceTargetDom() {
  491. if (!this.getReplaceTargetDom()) {
  492. this.createReplaceTargetDom()
  493. }
  494. return this.getReplaceTargetDom()
  495. }
  496.  
  497. static removeReplaceTargetDom () {
  498. if (!this.getReplaceTargetDom()) {
  499. return
  500. }
  501. this.getReplaceTargetDom().remove()
  502. }
  503.  
  504. static insertCssStyle() {
  505. let headDom = window.top.document.getElementsByTagName("HEAD")[0]
  506. headDom.insertAdjacentHTML("beforeend", style)
  507. }
  508.  
  509. static insertContainerOnce(targetDom) {
  510. if (this.getContainer()) {
  511. return
  512. }
  513. targetDom = targetDom ? targetDom : this.getHostSearchInputAndTarget()[1]
  514. if (!targetDom) {
  515. log("AKS can't insert cards container", targetDom)
  516. return
  517. }
  518. targetDom.insertAdjacentHTML("afterbegin", CONTAINER)
  519. this.insertCssStyle()
  520. }
  521.  
  522. static getContainer() {
  523. return window.top.document.getElementById(CONTAINER_ID)
  524. }
  525.  
  526. static clearContainer() {
  527. this.getContainer().innerHTML = ""
  528. }
  529.  
  530. static replaceImgHTML(html, filename, base64Img) {
  531. let orgImg = `<img src="${filename}"`
  532. let replaceImg = `<img class="anki-img-width" src="${base64Img}"`
  533. html = html.replace(orgImg, replaceImg)
  534. return html
  535. }
  536.  
  537. }
  538.  
  539.  
  540. async function main() {
  541. log("Anki Serarch Launching")
  542. let [searchInput, targetDom] = DomOper.getHostSearchInputAndTarget()
  543. if (!searchInput) {
  544. log("AKS can't find search input", searchInput)
  545. return
  546. }
  547. DomOper.addInputEventListener(searchInput)
  548. DomOper.insertContainerOnce(targetDom)
  549.  
  550. let searchText = searchInput.value
  551. new CardMgr().searchAndInsertCard(searchText)
  552. }
  553.  
  554.  
  555. window.onload = main
  556.  
  557.  
  558. const style = `
  559. <style>
  560. /*card*/
  561. .anki-card-size {
  562. min-width: ${MIN_CARD_WIDTH}em;
  563. max-width: ${MAX_CARD_WIDTH}em;
  564. max-height: ${MAX_CARD_HEIGHT}em;
  565. }
  566.  
  567. .anki-img-width {
  568. max-width: ${MAX_IMG_WIDTH}em;
  569. }
  570. .anki-card {
  571. position: relative;
  572. display: -ms-flexbox;
  573. display: flex;
  574. -ms-flex-direction: column;
  575. flex-direction: column;
  576. word-wrap: break-word;
  577. width:fit-content;
  578. width:-webkit-fit-content;
  579. width:-moz-fit-content;
  580. margin-bottom: .25em;
  581. border: .1em solid #69928f;
  582. // border-radius: calc(.7em - 1px);
  583. border-radius: .7em;
  584. }
  585.  
  586. .anki-body {
  587. overflow-x: visible;
  588. overflow-y: auto;
  589. }
  590.  
  591. /* card title */
  592. .anki-title {
  593. padding: .75em;
  594. margin: 0em;
  595. font-weight: 700;
  596. color: black;
  597. background-color: #e0f6f9;
  598. // border-radius: calc(.5em - 1px);
  599. border-radius: .7em;
  600.  
  601. transition-property: all;
  602. transition-duration: 1.5s;
  603. transition-timing-function: ease-out;
  604. }
  605.  
  606. .anki-title-sel {
  607. animation-name: select-title;
  608. animation-duration: 5s;
  609. animation-iteration-count: infinite;
  610. animation-direction: alternate;
  611. }
  612.  
  613. .anki-title:hover{
  614. // background-color: #9791b1;
  615. background-color: #d2e4f9;
  616. }
  617.  
  618. .anki-sub-title {
  619. color: #5F9EA0;
  620. }
  621.  
  622. .anki-front-card {
  623. padding: .75em;
  624. border-bottom: solid .3em #c6e1e4;
  625. }
  626.  
  627. .anki-back-card {
  628. padding: .75em .75em;
  629. }
  630.  
  631. .anki-collapsed {
  632. overflow: hidden;
  633. animation-name: collapsed;
  634. animation-duration: .3s;
  635. animation-timing-function: ease-out;
  636. animation-fill-mode:forwards;
  637. animation-direction: normal;
  638. }
  639.  
  640. .anki-extend {
  641. overflow-x: visible;
  642. animation-name: extend;
  643. animation-duration: .3s;
  644. animation-timing-function: ease-in;
  645. animation-fill-mode:forwards;
  646. animation-direction: normal;
  647. }
  648.  
  649. div#anki-container ul {
  650. margin-bottom: 1em;
  651. margin-left: 2em;
  652. }
  653.  
  654. div#anki-container ol {
  655. margin-bottom: 1em;
  656. margin-left: 2em;
  657. }
  658.  
  659. div#anki-container ul li{
  660. list-style-type: disc;
  661. }
  662.  
  663. div#anki-container ul ul li{
  664. list-style-type: circle;
  665. }
  666.  
  667. div#anki-container ul ul ul li{
  668. list-style-type: square;
  669. }
  670.  
  671. div#anki-container ul ul ul ul li{
  672. list-style-type: circle;
  673. }
  674.  
  675. div#anki-replace-target {
  676. margin-left: 2em;
  677. width: ${MIN_CARD_WIDTH}em;
  678. max-width: ${MAX_CARD_WIDTH}em;
  679. float: right;
  680. display: block;
  681. position: relative;
  682. }
  683.  
  684. @keyframes collapsed
  685. {
  686. 0% {max-height: ${MAX_CARD_HEIGHT}em; max-width: ${MAX_CARD_WIDTH}em;}
  687. 100% {max-height: 0em; max-width: 30em;}
  688. }
  689.  
  690. @keyframes extend
  691. {
  692. 0% {max-height: 0em; max-width: ${MIN_CARD_WIDTH}em;}
  693. 100% {max-height: ${MAX_CARD_WIDTH}em; max-width: ${MAX_CARD_WIDTH}em;}
  694. }
  695.  
  696. @keyframes select-title
  697. {
  698. 0% {background: #e0f6f9;}
  699. 50% {background: #e1ddf3;}
  700. 100% {background: #d2e4f9;}
  701. }
  702.  
  703. /**
  704. * hljs css
  705. */
  706. pre code.hljs {
  707. display: block;
  708. overflow-x: auto;
  709. padding: 1em
  710. }
  711. code.hljs {
  712. padding: 3px 5px
  713. }
  714. .hljs {
  715. color: #e0e2e4;
  716. background: #282b2e
  717. }
  718. .hljs-keyword, .hljs-literal, .hljs-selector-id, .hljs-selector-tag {
  719. color: #93c763
  720. }
  721. .hljs-number {
  722. color: #ffcd22
  723. }
  724. .hljs-attribute {
  725. color: #668bb0
  726. }
  727. .hljs-link, .hljs-regexp {
  728. color: #d39745
  729. }
  730. .hljs-meta {
  731. color: #557182
  732. }
  733. .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 {
  734. color: #8cbbad
  735. }
  736. .hljs-string, .hljs-symbol {
  737. color: #ec7600
  738. }
  739. .hljs-comment, .hljs-deletion, .hljs-quote {
  740. color: #818e96
  741. }
  742. .hljs-selector-class {
  743. color: #a082bd
  744. }
  745. .hljs-doctag, .hljs-keyword, .hljs-literal, .hljs-name, .hljs-section, .hljs-selector-tag, .hljs-strong, .hljs-title, .hljs-type {
  746. font-weight: 700
  747. }
  748. .hljs-class .hljs-title, .hljs-code, .hljs-section, .hljs-title.class_ {
  749. color: #fff
  750. }
  751. </style>
  752. `

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址