必应图片下载按钮

在必应首页添加一个图片下载按钮。

  1. // ==UserScript==
  2. // @namespace https://greasyfork.org/en/users/131965-levinit
  3. // @author levinit
  4. // @name Bing Image Download Button
  5. // @name:zh-CN 必应图片下载按钮
  6. // @name:zh-TW 必應圖片下載按鈕
  7. // @name:ko Bing 이미지 다운로드 버튼
  8. // @name:fr Bouton de téléchargement d'image Bing
  9. // @name:ja Bing画像ダウンロードボタン
  10. // @description Add an image download button on Bing's home page.
  11. // @description:zh-CN 在必应首页添加一个图片下载按钮。
  12. // @description:zh-TW 在必應首頁添加一个圖片下載按鈕。
  13. // @description:ko 빙 홈페이지에 이미지 다운로드 버튼 추가
  14. // @description:fr Ajouter le bouton de téléchargement d'image à la page d'accueil Bing.
  15. // @description:ja Bingホームページに画像ダウンロードボタンを追加する。
  16. // @match *://cn.bing.com/*
  17. // @match *://www.bing.com/*
  18. // @run-at document-end
  19. // @version 1.3.12
  20. // @homepageURL https://github.com/levinit/bing-image-download-button
  21. // @grant none
  22. // ==/UserScript==
  23.  
  24. const VERSION = '1.3.12'
  25.  
  26. const bingDownloadBtnConfig = {
  27. //下载按钮css样式
  28. btnStyles: {
  29. 'color': '',
  30. 'font-size': '1.5em',
  31. 'padding': '0.25em',
  32. 'border-radius': '0.25em',
  33. 'box-shadow': '0 0 3 px rgba(125, 125, 125, 0.25)',
  34. 'right': '20%',
  35. 'top': '12.5%',
  36. 'background': '#c3d1cf94',
  37. 'position': 'fixed',
  38. 'z-index': 9999
  39. },
  40. //下载按钮上的文字
  41. btnText() {
  42. let text = 'Download Today Bing Picture' //lang en
  43. switch (navigator.language.toLowerCase()) {
  44. case 'zh':
  45. case 'zh-cn':
  46. case 'zh-sg':
  47. text = '下载今日必应图片'
  48. break;
  49. case 'zh-tw':
  50. case 'zh-hk':
  51. text = '下載今日必應圖片'
  52. break;
  53. case 'ko':
  54. case 'ko_kr':
  55. text = '오늘의 빙 이미지 다운로드'
  56. break;
  57. case 'ja':
  58. case 'ja_jp':
  59. text = '今日のBing画像をダウンロードする'
  60. break
  61. case 'fr':
  62. case 'fr_be':
  63. case 'fr_ca':
  64. case 'fr_ch':
  65. case 'fr_fr':
  66. case 'fr_lu':
  67. text = 'Téléchargez les image de bing aujourd’hui'
  68. break
  69. default:
  70. break;
  71. }
  72. return text
  73. },
  74. //当前要下载的bing图片的信息
  75. imgInfo: {
  76. url: '',
  77. name: '',
  78. 'name-rule': { //图片默认命名规则,true项的内容将写入到图片名中
  79. //图片名字信息来自于图片的url 一般形如 flower_12345_1920x1080 形式
  80. 'baseName': true, //基础名字
  81. 'imgNO': false, //数字编号
  82. 'imgResolution': false, //分辨率
  83. 'dateInfo': true, //日期信息(从浏览器中获取的操作系统日期信息)
  84. 'description': true, //描述信息(bing首页右下角获取)
  85. 'copyright': false //图片版权信息(同上)
  86. },
  87. //bing提供的图片分辨率 不设置则使用默认 默认分辨率一般和当前系统设置、显示器分辨率有关
  88. resolution: 'UHD', //1366x768 1280x720 1920x1080
  89. separator: '_', // 默认连接符
  90. },
  91. //设置菜单
  92. menuInfo: {
  93. menuWrapStyles: {
  94. 'position': 'fixed',
  95. 'z-index': '9',
  96. 'right': '1%',
  97. 'top': '5%',
  98. 'font-size': '1.25em',
  99. 'display': 'none'
  100. },
  101. //设置菜单相关标签的id值
  102. menuWrapId: 'bing-download-settings',
  103. resetBtnId: 'reset-menu-settings',
  104. closeBtnClass: 'close-settings-menu',
  105. saveBtnId: 'save-menu-settings'
  106. },
  107. //本项目信息
  108. about: {
  109. github: 'https://github.com/levinit/bing-image-download-button',
  110. greasyfork: 'https://greasyfork.org/zh-TW/scripts/35070-bing-image-download-button',
  111. version: VERSION
  112. },
  113. //本地存储使用的key 用于存储菜单中设置的信息
  114. localStoreKey: 'bingImgDownload'
  115. }
  116.  
  117. //当前日期偏移量 本日为0 bing可以查看前7天图片 0-7
  118.  
  119. let dateOffset = 0
  120.  
  121. //从本地存储中取得设置的信息写入到bingDownloadBtn相关项中
  122. function getSavedSettings(info) {
  123. if (localStorage.getItem(info.localStoreKey)) {
  124. //本地存储的设置信息
  125. const savedSettings = JSON.parse(localStorage.getItem(info.localStoreKey))
  126. const setSettings = function (settingsObj, savedSettingsObj) {
  127. //遍历本地存储的设置信息,写入到bingDownloadBtn设置菜单的各个项中
  128. for (const item in savedSettingsObj) {
  129. if (settingsObj.hasOwnProperty(item)) {
  130. settingsObj[item] = savedSettingsObj[item]
  131. }
  132. }
  133. }
  134.  
  135. //向设置菜单中写入已经保存的图片设置项的信息(图片命名规则和分辨率)
  136. setSettings(info.imgInfo, savedSettings.imgInfo)
  137.  
  138. //绑定点击上一个/下一个图片时更新日期信息的事件
  139. getDateOffset()
  140. }
  141. }
  142.  
  143. let navEventBound = false
  144. function getDateOffset() {
  145. if (navEventBound) return
  146. navEventBound = true
  147. const leftNav = document.getElementById("leftNav")
  148. const rightNav = document.getElementById("rightNav")
  149. if (leftNav) {
  150. leftNav.addEventListener('click', function (e) {
  151. e.preventDefault()
  152. dateOffset = dateOffset === -7 ? -7 : dateOffset - 1
  153. })
  154. }
  155. if (rightNav) {
  156. rightNav.addEventListener('click', function (e) {
  157. e.preventDefault()
  158. dateOffset = dateOffset === 0 ? 0 : dateOffset + 1
  159. })
  160. }
  161. }
  162.  
  163. //-----获取图片信息(根据设置规则修改)
  164. function getImgInfo(imgInfo) {
  165. const link = document.querySelector('a.downloadLink')
  166. if (!link) return
  167. let url = link.href.split('&rf')[0]
  168. //图片地址 根据分辨率设置修改图片地址 分辨率如1920x1080 如果未设置分辨率将使用默认分辨率
  169. url = imgInfo.resolution ? url.replace(/\d{4}x\d{3,4}/, imgInfo.resolution) : url
  170. console.log("img url is: ", url)
  171. /*图片名字 根据图片地址生成图片原始名字
  172. 原始示例 AberystwythSeafront_ZH-CN9542789062_1920x1080.jpg
  173. 原始名字分成三部分 baseName imgNO resolution
  174. */
  175. //原始名字去掉前面的OHR.字样 使用_分割
  176. const match = /id=.+?\.(jpg|png)/.exec(url)
  177. if (!match) {
  178. console.warn('图片URL格式异常,无法提取图片名')
  179. return
  180. }
  181. const nameInfo = match[0].replace('id=', '').replace(/^OHR\./, '').split('_')
  182.  
  183. //图片格式
  184. const imgFormat = nameInfo[nameInfo.length - 1].split('.')[1]
  185.  
  186. //初始化图片命名相关的项
  187. let [baseName, imgNO, resolution, description, copyright, dateInfo] = ['', '', '', '', '', '']
  188.  
  189. //根据名字生成规则修改图片名字
  190. for (const rule in imgInfo['name-rule']) {
  191. const ruleValue = imgInfo['name-rule'][rule]
  192. if (ruleValue === true) {
  193. switch (rule) {
  194. case 'baseName':
  195. baseName = `${nameInfo[0]}`
  196. break;
  197. case 'imgNO':
  198. imgNO = nameInfo[1] ? nameInfo[1] : ''
  199. break;
  200. case 'imgResolution':
  201. resolution = nameInfo[2] ? nameInfo[2].split('.')[0] : ''
  202. break;
  203. case 'dateInfo':
  204. //日期 先从描述信息的日期中获取,如果没有则使用系统时间
  205. try {
  206. dateInfo = document.querySelector('.musCardCont a.title').href.match(/Date:%\d+_/)[0].slice(-9, -1)
  207. } catch (error) {
  208. console.log(error)
  209. } finally {
  210. if (dateInfo === '' || dateInfo === undefined) {
  211. const now = new Date()
  212. const imgDate = new Date(now.getTime() + dateOffset * (24 * 60 * 60 * 1000))
  213. dateInfo = `${imgDate.getFullYear()}-${imgDate.getMonth() + 1}-${imgDate.getDate()}`
  214. }
  215. }
  216. break;
  217. //图片描述
  218. case 'description':
  219. try {
  220. description = document.querySelector('.musCardCont a.title').textContent || ''
  221. } catch (e) {
  222. description = ''
  223. }
  224. break;
  225. //图片版权
  226. case 'copyright':
  227. try {
  228. copyright = document.querySelector('.musCardCont div.copyright').textContent || ''
  229. } catch (e) {
  230. copyright = ''
  231. }
  232. break;
  233. default:
  234. break;
  235. }
  236. }
  237. }
  238. // 使用自定义连接符拼接
  239. const separator = imgInfo.separator || '_'
  240. let nameArr = [baseName, imgNO, resolution, description, copyright, dateInfo].filter(Boolean)
  241. let name = nameArr.join(separator)
  242. // 清理多余连接符
  243. const sepReg = new RegExp(`[${separator === ' ' ? '\s' : separator}]{2,}`, 'g')
  244. name = name.replace(sepReg, separator).replace(new RegExp(`^[${separator === ' ' ? '\s' : separator}]+|[${separator === ' ' ? '\s' : separator}]+$`, 'g'), '')
  245. //如果图片没有名字只有后缀 强行给图片加上名字
  246. if (name === `.${imgFormat}`) {
  247. name = `${nameInfo[0]}.${imgFormat}`
  248. } else {
  249. name = `${name}.${imgFormat}`
  250. }
  251.  
  252. //存储图片url及名字
  253. bingDownloadBtnConfig.imgInfo.url = url
  254. bingDownloadBtnConfig.imgInfo.name = name
  255. }
  256.  
  257. //-------添加下载按钮
  258. function addBtn(info) {
  259. const btn = document.createElement('a')
  260. btn.appendChild(document.createTextNode(info.btnText()))
  261.  
  262.  
  263. btn.style.cssText = (function (styles) {
  264. let btnCssText = ''
  265. for (let style in styles) {
  266. btnCssText += `${style}: ${styles[style]}; `
  267. }
  268. return btnCssText
  269. })(info.btnStyles)
  270.  
  271. btn.href = info.imgInfo.url
  272. btn.download = info.imgInfo.name
  273. btn.title = `img name: ${info.imgInfo.name}
  274. 右键打开设置菜单 | Right Click this button to open settings menu`
  275. document.body.appendChild(btn)
  276.  
  277. //当光标移动到下载按钮上时立即更新图片下载信息
  278. btn.onmouseover = function () {
  279. // 注意:点击了前一天或后一天按钮后 需要刷新图片的下载地址
  280. getImgInfo(info.imgInfo)
  281. //将处理后的图片的url和name写入到下载按钮的属性中
  282. this.href = info.imgInfo.url
  283. this.download = info.imgInfo.name
  284. }
  285.  
  286. //在下载按钮上右键可打开设置菜单
  287. btn.oncontextmenu = function (e) {
  288. e.preventDefault()
  289. document.getElementById(info.menuInfo.menuWrapId).style.display = 'block'
  290. }
  291. }
  292.  
  293. //-----添加设置菜单
  294. function addMenu(info) {
  295. const menuInfo = info.menuInfo
  296. // 如果菜单已存在则不再插入
  297. if (document.getElementById(menuInfo.menuWrapId)) return
  298.  
  299. //先前已经存储的图像分辨率设置信息
  300. const savedImgResolution = info.imgInfo.resolution
  301. //先前已经存储的图像规则信息
  302. const savedImgNameRule = info.imgInfo['name-rule']
  303.  
  304. const menuContent = `
  305. <fieldset id="btn-settings">
  306. <legend>settings</legend>
  307. <div class="settings-content">
  308. <ul class="img-infos">
  309. <header>
  310. Image Info
  311. </header>
  312. <li>
  313. <header>
  314. Image Name contains:
  315. </header>
  316. <div>
  317. <label>Base Name</label>
  318. <input class="img-info" type="checkbox" name="name-rule" checked data-img-name-rule="baseName" />
  319. </div>
  320. <div>
  321. <label>NO.</label>
  322. <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="imgNO"
  323. ${savedImgNameRule.imgNO ? 'checked' : ''} />
  324. </div>
  325. <div>
  326. <label>Resolution</label>
  327. <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="imgResolution"
  328. ${savedImgNameRule.imgResolution ? 'checked' : ''} />
  329. </div>
  330. <div>
  331. <label>Description</label>
  332. <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="description"
  333. ${savedImgNameRule.description ? 'checked' : ''} />
  334. </div>
  335. <div>
  336. <label>CopyRight</label>
  337. <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="copyright"
  338. ${savedImgNameRule.copyright ? 'checked' : ''} />
  339. </div>
  340. <div>
  341. <label>Date Info</label>
  342. <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="dateInfo"
  343. ${savedImgNameRule.dateInfo ? 'checked' : ''} />
  344. </div>
  345. <div style="display: flex; align-items: center; gap: 0.5em;">
  346. <label for="separator-select">Separator</label>
  347. <select id="separator-select" class="img-info" name="separator">
  348. <option value="_" ${info.imgInfo.separator === '_' ? 'selected' : ''}>_</option>
  349. <option value="-" ${info.imgInfo.separator === '-' ? 'selected' : ''}>-</option>
  350. <option value="," ${info.imgInfo.separator === ',' ? 'selected' : ''}>,</option>
  351. <option value=" " ${info.imgInfo.separator === ' ' ? 'selected' : ''}>Space</option>
  352. <option value="." ${info.imgInfo.separator === '.' ? 'selected' : ''}>.</option>
  353. </select>
  354. </div>
  355. </li>
  356. <li>
  357. <header>
  358. Image Resolution
  359. </header>
  360. <div>
  361. <label>UHD</label>
  362. <input class="img-info" type="radio" name="resolution" data-img-resolution="UHD"
  363. ${savedImgResolution === 'UHD' ? 'checked' : ''} />
  364. </div>
  365. <div>
  366. <label>1920x1080</label>
  367. <input class="img-info" type="radio" name="resolution" data-img-resolution="1920x1080"
  368. ${savedImgResolution === '1920x1080' ? 'checked' : ''} />
  369. </div>
  370. <div>
  371. <label>1366x768</label>
  372. <input class="img-info" type="radio" name="resolution" data-img-resolution="1366x768"
  373. ${savedImgResolution === '1366x768' ? 'checked' : ''} />
  374. </div>
  375. <div>
  376. <label>1280x720</label>
  377. <input class="img-info" type="radio" name="resolution" data-img-resolution="1280x720"
  378. ${savedImgResolution === '1280x720' ? 'checked' : ''} />
  379. </div>
  380. <div>
  381. <label>Default</label>
  382. <input class="img-info" type="radio" name="resolution" data-img-resolution="" ${savedImgResolution === ''
  383. ? 'checked' : ''} />
  384. </div>
  385. </li>
  386. </ul>
  387. <div class="about">
  388. <small>V${info.about.version}</small>
  389. <a href="${info.about.github}">GitHub</a>
  390. <a href="${info.about.greasyfork}">GreasyFork</a>
  391. </div>
  392. </div>
  393. <footer>
  394. <button id="${menuInfo.resetBtnId}" class="reset-btn">reset</button>
  395. <button id="${menuInfo.saveBtnId}" class="${menuInfo.closeBtnClass}">save</button>
  396. <button class="${menuInfo.closeBtnClass}">cancel</button>
  397. </footer>
  398. </fieldset>
  399. <style>
  400. #btn-settings {
  401. width: 300px;
  402. border: 1px dashed gainsboro;
  403. border-radius: 8px;
  404. box-shadow: 0 0 10px gainsboro;
  405. background-color: aliceblue;
  406. }
  407.  
  408. #btn-settings legend {
  409. font-weight: bold;
  410. text-shadow: 0 0 2px gray;
  411. color: steelblue;
  412. }
  413.  
  414. #btn-settings ul {
  415. padding: 0;
  416. }
  417.  
  418. #btn-settings ul>header {
  419. width: 100%;
  420. border-bottom: 3px groove gainsboro;
  421. font-weight: bold;
  422. color: slategrey;
  423. text-shadow: 0 0 5px gainsboro;
  424. margin-bottom: 0.5em;
  425. }
  426.  
  427. #btn-settings li {
  428. list-style-type: none;
  429. border-bottom: 1px dashed gainsboro;
  430. padding-bottom: 0.5em;
  431. }
  432.  
  433. .img-infos li header {
  434. color: sienna;
  435. margin-bottom: 0.25em;
  436. }
  437.  
  438. .img-infos li label {
  439. width: 80%;
  440. display: inline-block;
  441. }
  442.  
  443. .img-infos .img-info {
  444. vertical-align:middle;
  445. }
  446.  
  447. #btn-settings .about {
  448. text-align: right;
  449. margin-bottom: 1em;
  450. }
  451.  
  452. #btn-settings .about a {
  453. margin-right: 1em;
  454. text-decoration: underline;
  455. }
  456.  
  457. #btn-settings footer {
  458. text-align: right;
  459. }
  460.  
  461. #btn-settings footer button {
  462. width: 88px;
  463. cursor: pointer;
  464. font-size: 1.2em;
  465. font-weight: bold;
  466. line-height: 1.25;
  467. text-align: center;
  468. padding: 0;
  469. color: teal;
  470. }
  471.  
  472. #btn-settings footer .reset-btn {
  473. margin-right: 25px;
  474. color: tomato;
  475. }
  476. </style>
  477. `
  478.  
  479. const menu = document.createElement('div')
  480. menu.innerHTML = menuContent
  481. menu.id = menuInfo.menuWrapId
  482. let cssText = ''
  483. for (const style in menuInfo.menuWrapStyles) {
  484. cssText += `${style}: ${menuInfo.menuWrapStyles[style]}; `
  485. }
  486. menu.style.cssText = cssText
  487. document.body.appendChild(menu)
  488.  
  489. // 菜单事件绑定只绑定一次
  490. menu.onclick = function (e) {
  491. if (e.target.classList.contains(menuInfo.closeBtnClass)) {
  492. menu.style.display = 'none'
  493. if (e.target.id === menuInfo.saveBtnId) {
  494. localStorage.setItem(info.localStoreKey, JSON.stringify(getUserSettings(info)))
  495. getSavedSettings(info)
  496. getImgInfo(info.imgInfo)
  497. }
  498. }
  499. if (e.target.id === menuInfo.resetBtnId) {
  500. localStorage.removeItem(info.localStoreKey)
  501. getSavedSettings(info)
  502. getImgInfo(info.imgInfo)
  503. }
  504. }
  505. }
  506.  
  507. //从本地存储获取已经保存的设置信息
  508. function getUserSettings() {
  509. //btn-styles
  510. const btnStyles = {}
  511.  
  512. for (const item of document.querySelectorAll('.btn-style')) {
  513. let value = item.value
  514. //未设置的属性 以及position设置中未选择的属性 忽略
  515. if (item.value === "" || item.previousElementSibling.type === 'radio' && item.previousElementSibling.checked === false) {
  516. continue
  517. }
  518. const property = item.getAttribute('data-property')
  519. btnStyles[property] = value
  520. }
  521.  
  522.  
  523. //img-info
  524. const imgInfo = {
  525. 'name-rule': {}
  526. }
  527.  
  528. for (const item of document.querySelectorAll('.img-info')) {
  529. switch (item.name) {
  530. //图片命名规则
  531. case 'name-rule':
  532. imgInfo['name-rule'][item.getAttribute('data-img-name-rule')] = item.checked
  533. break
  534. //分辨率
  535. case 'resolution':
  536. if (item.checked) {
  537. imgInfo.resolution = item.getAttribute('data-img-resolution')
  538. }
  539. break
  540. //文件名连接符
  541. case 'separator':
  542. imgInfo.separator = item.value
  543. break
  544. default:
  545. break
  546. }
  547. }
  548. return { btnStyles, imgInfo }
  549. }
  550.  
  551.  
  552. //+++++++++ 打开页面后的初始化 +++++++++
  553. //从本地存储读取设置信息
  554. getSavedSettings(bingDownloadBtnConfig)
  555. //设置图片信息
  556. getImgInfo(bingDownloadBtnConfig.imgInfo)
  557. //添加下载按钮
  558. addBtn(bingDownloadBtnConfig)
  559. //添加设置菜单
  560. addMenu(bingDownloadBtnConfig)
  561.