Bing Image Download Button

Add an image download button on Bing's home page.

// ==UserScript==
// @namespace   https://greasyfork.org/en/users/131965-levinit
// @author      levinit
// @name        Bing Image Download Button
// @name:zh-CN  必应图片下载按钮
// @name:zh-TW  必應圖片下載按鈕
// @name:ko     Bing 이미지 다운로드 버튼
// @name:fr     Bouton de téléchargement d'image Bing
// @name:ja     Bing画像ダウンロードボタン
// @description Add an image download button on Bing's home page.
// @description:zh-CN  在必应首页添加一个图片下载按钮。
// @description:zh-TW  在必應首頁添加一个圖片下載按鈕。
// @description:ko    빙 홈페이지에 이미지 다운로드 버튼 추가
// @description:fr    Ajouter le bouton de téléchargement d'image à la page d'accueil Bing.
// @description:ja    Bingホームページに画像ダウンロードボタンを追加する。
// @match     *://cn.bing.com/*
// @match     *://www.bing.com/*
// @run-at      document-end
// @version     1.3.10
// @homepageURL   https://github.com/levinit/bing-image-download-button
// @grant       none
// ==/UserScript==

const bingDownloadBtnConfig = {
    //下载按钮css样式
    btnStyles: {
        'color': '',
        'font-size': '1.5em',
        'padding': '0.25em',
        'border-radius': '0.25em',
        'box-shadow': '0 0 3 px rgba(125, 125, 125, 0.25)',
        'right': '20%',
        'top': '12.5%',
        'background': '#c3d1cf94',
        'position': 'fixed',
        'z-index': 9999
    },
    //下载按钮上的文字
    btnText() {
        let text = 'Download Today Bing Picture' //lang en
        switch (navigator.language.toLowerCase()) {
            case 'zh':
            case 'zh-cn':
            case 'zh-sg':
                text = '下载今日必应图片'
                break;
            case 'zh-tw':
            case 'zh-hk':
                text = '下載今日必應圖片'
                break;
            case 'ko':
            case 'ko_kr':
                text = '오늘의 빙 이미지 다운로드'
                break;
            case 'ja':
            case 'ja_jp':
                text = '今日のBing画像をダウンロードする'
                break
            case 'fr':
            case 'fr_be':
            case 'fr_ca':
            case 'fr_ch':
            case 'fr_fr':
            case 'fr_lu':
                text = 'Téléchargez les image de bing aujourd’hui'
                break
            default:
                break;
        }
        return text
    },
    //当前要下载的bing图片的信息
    imgInfo: {
        url: '',
        name: '',
        'name-rule': { //图片默认命名规则,true项的内容将写入到图片名中
            //图片名字信息来自于图片的url 一般形如 flower_12345_1920x1080 形式
            'baseName': true, //基础名字
            'imgNO': false, //数字编号
            'imgResolution': false, //分辨率
            'dateInfo': true, //日期信息(从浏览器中获取的操作系统日期信息)
            'description': true, //描述信息(bing首页右下角获取)
            'copyright': false //图片版权信息(同上)
        },
        //bing提供的图片分辨率 不设置则使用默认 默认分辨率一般和当前系统设置、显示器分辨率有关
        resolution: 'UHD' //1366x768 1280x720 1920x1080
    },
    //设置菜单
    menuInfo: {
        menuWrapStyles: {
            'position': 'fixed',
            'z-index': '9',
            'right': '1%',
            'top': '5%',
            'font-size': '1.25em',
            'display': 'none'
        },
        //设置菜单相关标签的id值
        menuWrapId: 'bing-download-settings',
        resetBtnId: 'reset-menu-settings',
        closeBtnClass: 'close-settings-menu',
        saveBtnId: 'save-menu-settings'
    },
    //本项目信息
    about: {
        github: 'https://github.com/levinit/bing-image-download-button',
        greasyfork: 'https://greasyfork.org/zh-TW/scripts/35070-bing-image-download-button'
    },
    //本地存储使用的key 用于存储菜单中设置的信息
    localStoreKey: 'bingImgDownload'
}

//当前日期偏移量 本日为0 bing可以查看前7天图片 0-7

let dateOffset = 0

//从本地存储中取得设置的信息写入到bingDownloadBtn相关项中
function getSavedSettings(info) {
    if (localStorage.getItem(info.localStoreKey)) {

        //本地存储的设置信息
        const savedSettings = JSON.parse(localStorage.getItem(bingDownloadBtnConfig.localStoreKey))
        const setSettings = function (settingsObj, savedSettingsObj) {
            //遍历本地存储的设置信息,写入到bingDownloadBtn设置菜单的各个项中
            for (const item in savedSettingsObj) {
                if (settingsObj.hasOwnProperty(item)) {
                    settingsObj[item] = savedSettingsObj[item]
                }
            }
        }

        //向设置菜单中写入已经保存的图片设置项的信息(图片命名规则和分辨率)
        setSettings(info.imgInfo, savedSettings.imgInfo)

        //绑定点击上一个/下一个图片时更新日期信息的事件
        getDateOffset()
    }
}

function getDateOffset() {
    //前一天
    document.getElementById("leftNav").addEventListener('click', function (e) {
        e.preventDefault()
        dateOffset = dateOffset === -7 ? -7 : dateOffset - 1
    })
    //后一天
    document.getElementById("rightNav").addEventListener('click', function (e) {
        e.preventDefault()
        dateOffset = dateOffset === 0 ? 0 : dateOffset + 1
    })
}

//-----获取图片信息(根据设置规则修改)
function getImgInfo(imgInfo) {
    let url = document.querySelector('a.downloadLink').href.split('&rf')[0]
    //图片地址  根据分辨率设置修改图片地址 分辨率如1920x1080 如果未设置分辨率将使用默认分辨率
    url = imgInfo.resolution ? url.replace(/\d{4}x\d{3,4}/, imgInfo.resolution) : url
    console.log("img url is: ", url)
    /*图片名字  根据图片地址生成图片原始名字
    原始示例 AberystwythSeafront_ZH-CN9542789062_1920x1080.jpg
    原始名字分成三部分 baseName imgNO resolution
    */
    //原始名字去掉前面的OHR.字样 使用_分割
    const nameInfo = /id=.+?\.(jpg|png)/.exec(url)[0].replace('id=', '').replace(/^OHR\./, '').split('_')

    //图片格式
    const imgFormat =nameInfo[nameInfo.length - 1].split('.')[1]

    //初始化图片命名相关的项
    let [baseName, imgNO, resolution, description, copyright, dateInfo] = ['', '', '', '', '', '']

    //根据名字生成规则修改图片名字
    for (const rule in imgInfo['name-rule']) {
        const ruleValue = imgInfo['name-rule'][rule]
        if (ruleValue === true) {
            switch (rule) {
                case 'baseName':
                    baseName = `${nameInfo[0]}`
                    break;
                case 'imgNO':
                    imgNO = `_${nameInfo[1]}`
                    break;
                case 'imgResolution':
                    resolution = `_${nameInfo[2]}`.split('.')[0]
                    break;
                case 'dateInfo':
                    //日期 先从描述信息的日期中获取,如果没有则使用系统时间
                    try {
                        dateInfo = document.querySelector('.musCardCont a.title').href.match(/Date:%\d+_/)[0].slice(-9, -1)
                    } catch (error) {
                        console.log(error)
                    } finally {
                        if (dateInfo === '' || dateInfo === undefined) {
                            const now = new Date()
                            const imgDate = new Date(now.getTime() + dateOffset * (24 * 60 * 60 * 1000))
                            dateInfo = `_${imgDate.getFullYear()}-${imgDate.getMonth() + 1}-${imgDate.getDate()}`
                        }
                    }
                    break;
                //图片描述
                case 'description':
                    description = `_${document.querySelector('.musCardCont a.title').textContent
                    }`
                    break;
                //图片版权
                case 'copyright':
                    copyright = document.querySelector('.musCardCont div.copyright').textContent
                    break;
                default:
                    break;
            }
        }
    }

    //拼接图片名字 去掉前后可能出现的_
    let name = `${baseName}${imgNO}${resolution}${description}${copyright}${dateInfo}`.replace(/^_/, '').replace(/_$/, '')

    //如果图片没有名字只有后缀 强行给图片加上名字
    if (name === `.${imgFormat}`) {
        name = `${nameInfo[0]}.${imgFormat}`
    } else {
        name = `${name}.${imgFormat}`
    }

    //存储图片url及名字
    bingDownloadBtnConfig.imgInfo.url = url
    bingDownloadBtnConfig.imgInfo.name = name
}

//-------添加下载按钮
function addBtn(info) {
    const btn = document.createElement('a')
    btn.appendChild(document.createTextNode(info.btnText()))


    btn.style.cssText = (function (styles) {
        let btnCssText = ''
        for (let style in styles) {
            btnCssText += `${style}: ${styles[style]}; `
        }
        return btnCssText
    })(info.btnStyles)

    btn.href = info.imgInfo.url
    btn.download = info.imgInfo.name
    btn.title = `img name: ${info.imgInfo.name}
          右键打开设置菜单 | Right Click this button to open settings menu`
    document.body.appendChild(btn)

    //当光标移动到下载按钮上时立即更新图片下载信息
    btn.onmouseover = function () {
        // 注意:点击了前一天或后一天按钮后 需要刷新图片的下载地址
        getImgInfo(info.imgInfo)
        //将处理后的图片的url和name写入到下载按钮的属性中
        this.href = info.imgInfo.url
        this.download = info.imgInfo.name
    }

    //在下载按钮上右键可打开设置菜单
    btn.oncontextmenu = function (e) {
        e.preventDefault()
        document.getElementById(info.menuInfo.menuWrapId).style.display = 'block'
    }
}

//-----添加设置菜单
function addMenu(info) {
    const menuInfo = info.menuInfo

    //先前已经存储的图像分辨率设置信息
    const savedImgResolution = info.imgInfo.resolution
    //先前已经存储的图像规则信息
    const savedImgNameRule = info.imgInfo['name-rule']

    const menuContent = `
  <fieldset id="btn-settings">
    <legend>settings</legend>
    <div class="settings-content">
      <ul class="img-infos">
        <header>
          Image Info
        </header>
        <li>
          <header>
            Image Name contains:
          </header>
          <div>
            <label>Base-Name</label>
            <input class="img-info" type="checkbox" name="name-rule" checked data-img-name-rule="baseName" />
          </div>
          <div>
            <label>NO.</label>
            <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="imgNO"
              ${savedImgNameRule.imgNO ? 'checked' : ''} />
          </div>
          <div>
            <label>Resolution</label>
            <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="imgResolution"
              ${savedImgNameRule.imgResolution ? 'checked' : ''} />
          </div>
          <div>
            <label>Description</label>
            <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="description"
              ${savedImgNameRule.description ? 'checked' : ''} />
          </div>
          <div>
            <label>CopyRight</label>
            <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="copyright"
              ${savedImgNameRule.copyright ? 'checked' : ''} />
          </div>
          <div>
            <label>Date-Info</label>
            <input class="img-info" type="checkbox" name="name-rule" data-img-name-rule="dateInfo"
              ${savedImgNameRule.dateInfo ? 'checked' : ''} />
          </div>
        </li>
        <li>
          <header>
            Image Resolution
          </header>
          <div>
            <label>UHD</label>
            <input class="img-info" type="radio" name="resolution" data-img-resolution="UHD"
              ${savedImgResolution === 'UHD' ? 'checked' : ''} />
          </div>
          <div>
            <label>1920x1080</label>
            <input class="img-info" type="radio" name="resolution" data-img-resolution="1920x1080"
              ${savedImgResolution === '1920x1080' ? 'checked' : ''} />
          </div>
          <div>
            <label>1366x768</label>
            <input class="img-info" type="radio" name="resolution" data-img-resolution="1366x768"
              ${savedImgResolution === '1366x768' ? 'checked' : ''} />
          </div>
          <div>
            <label>1280x720</label>
            <input class="img-info" type="radio" name="resolution" data-img-resolution="1280x720"
              ${savedImgResolution === '1280x720' ? 'checked' : ''} />
          </div>
          <div>
            <label>Default</label>
            <input class="img-info" type="radio" name="resolution" data-img-resolution="" ${savedImgResolution === ''
        ? 'checked' : ''} />
          </div>
        </li>
      </ul>
      <div class="about">
        About:
        <a href="${info.about.github}">GitHub</a>
        <a href="${info.about.greasyfork}">GreasyFork</a>
      </div>
    </div>
    <footer>
      <button id="${menuInfo.resetBtnId}" class="reset-btn">reset</button>
      <button id="${menuInfo.saveBtnId}" class="${menuInfo.closeBtnClass}">save</button>
      <button class="${menuInfo.closeBtnClass}">cancel</button>
    </footer>
  </fieldset>
  <style>
    #btn-settings {
      width: 300px;
      border: 1px dashed gainsboro;
      border-radius: 8px;
      box-shadow: 0 0 10px gainsboro;
      background-color: aliceblue;
    }

    #btn-settings legend {
      font-weight: bold;
      text-shadow: 0 0 2px gray;
      color: steelblue;
    }

    #btn-settings ul {
      padding: 0;
    }

    #btn-settings ul>header {
      width: 100%;
      border-bottom: 3px groove gainsboro;
      font-weight: bold;
      color: slategrey;
      text-shadow: 0 0 5px gainsboro;
      margin-bottom: 0.5em;
    }

    #btn-settings li {
      list-style-type: none;
      border-bottom: 1px dashed gainsboro;
      padding-bottom: 0.5em;
    }

    .img-infos li header {
      color: sienna;
      margin-bottom: 0.25em;
    }

    .img-infos li label {
      width: 80%;
      display: inline-block;
    }

    .img-infos .img-info {
      vertical-align:middle;
    }

    #btn-settings .about {
      text-align: right;
      margin-bottom: 1em;
    }

    #btn-settings .about a {
      margin-right: 1em;
      text-decoration: underline;
    }

    #btn-settings footer {
      text-align: right;
    }

    #btn-settings footer button {
      width: 88px;
      cursor: pointer;
      font-size: 1.2em;
      font-weight: bold;
      line-height: 1.25;
      text-align: center;
      padding: 0;
      color: teal;
    }

    #btn-settings footer .reset-btn {
      margin-right: 25px;
      color: tomato;
    }
  </style>
          `

    const menu = document.createElement('div')
    menu.innerHTML = menuContent
    menu.id = info.menuInfo.menuWrapId
    let cssText = ''

    //设置菜单样式
    for (const style in menuInfo.menuWrapStyles) {
        cssText += `${style}: ${menuInfo.menuWrapStyles[style]}; `
    }
    menu.style.cssText = cssText
    document.body.appendChild(menu)

    //菜单的事件绑定:保存 重置 和 取消
    menu.onclick = function (e) {
        if (e.target.classList.contains(menuInfo.closeBtnClass)) {
            //如果点击的保存或取消按钮 关闭设置菜单
            menu.style.display = 'none'

            //如果点击的是保存按钮 存储设置的信息
            if (e.target.id === menuInfo.saveBtnId) {
                localStorage.setItem(info.localStoreKey, JSON.stringify(getUserSettings(info)))
                getSavedSettings(info)
                getImgInfo(info.imgInfo) //刷新图片相关信息
            }
        }

        //如果点击的是重置按钮 清空设置
        if (e.target.id === menuInfo.resetBtnId) {
            localStorage.removeItem(info.localStoreKey)
            getSavedSettings(info)
            getImgInfo(info.imgInfo) //刷新图片相关信息
        }

    }
}

//从本地存储获取已经保存的设置信息
function getUserSettings() {
    //btn-styles
    const btnStyles = {}

    for (const item of document.querySelectorAll('.btn-style')) {
        let value = item.value
        //未设置的属性 以及position设置中未选择的属性 忽略
        if (item.value === "" || item.previousElementSibling.type === 'radio' && item.previousElementSibling.checked === false) {
            continue
        }
        const property = item.getAttribute('data-property')
        btnStyles[property] = value
    }


    //img-info
    const imgInfo = {
        'name-rule': {}
    }

    for (const item of document.querySelectorAll('.img-info')) {
        switch (item.name) {
            //图片命名规则
            case 'name-rule':
                imgInfo['name-rule'][item.getAttribute('data-img-name-rule')] = item.checked
                break
            //分辨率
            case 'resolution':
                if (item.checked) {
                    imgInfo.resolution = item.getAttribute('data-img-resolution')
                }
                break
            default:
                break
        }
    }
    return {btnStyles, imgInfo}
}


//+++++++++ 打开页面后的初始化 +++++++++
//从本地存储读取设置信息
getSavedSettings(bingDownloadBtnConfig)
//设置图片信息
getImgInfo(bingDownloadBtnConfig.imgInfo)
//添加下载按钮
addBtn(bingDownloadBtnConfig)
//添加设置菜单
addMenu(bingDownloadBtnConfig)