您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 osu! 谱面下载页面上添加额外的按钮,可以唤醒下载器自动下载并导入谱面。
- // ==UserScript==
- // @name SimDevices Osu Beatmap Downloader Plugin
- // @name:zh-CN SimDevices Osu 谱面下载器插件
- // @include http*://osu.ppy.sh/*
- // @copyright 2020, Handle
- // @version 0.5.2
- // @description Add extra download buttons on beatmap page for SimDevices Beatmap Downloader on osu.ppy.sh
- // @description:zh-CN 在 osu! 谱面下载页面上添加额外的按钮,可以唤醒下载器自动下载并导入谱面。
- // @author Handle
- // @namespace https://github.com/SimDevices-Project
- // @supportURL https://github.com/SimDevices-Project/beatmap-downloader-user-script/issues
- // @grant none
- // ==/UserScript==
- ;(function () {
- 'use strict'
- const styleClassName = '_beatmap_downloader_style_'
- const beapmapPageDownloadBtnClassName = '_beatmap_downloader_btn_'
- const searchPageBtnClassName = `_beatmap_downloader_quick_btn_`
- const searchPageBtnText = 'Launch Downloader'
- const searchPageDownloadTipClassName = `_beatmap_downloader_qtip_`
- const insertStyle = () => {
- const styleDOM = document.createElement('style')
- styleDOM.classList.add(styleClassName)
- styleDOM.innerHTML = `
- .${searchPageBtnClassName}:hover {
- text-decoration: none!important;
- }
- .${searchPageDownloadTipClassName}:after{
- display:block;
- content:'';
- border-width:8px 5px 8px 5px;
- border-style:solid;
- border-color:hsl(var(--base-hue),10%,10%) transparent transparent transparent;
- position:absolute;
- left:calc(50% - 2px);
- top:100%;
- }
- `
- if (!document.querySelector(`.${styleClassName}`)) {
- document.head.append(styleDOM)
- }
- }
- const formatURL = () => {
- const URL = document.URL
- const [domain, argument] = URL.substr(URL.indexOf('//') + 2).split('#')
- const pathname = window.location.pathname.substr(1)
- const [type, sid] = pathname.split('/')
- switch (type) {
- case 'beatmapsets':
- if (sid && sid.length) {
- const [mode, bid] = argument || []
- return {
- id: sid,
- type: 'beatmapset',
- }
- } else {
- return {
- id: 0,
- type: 'beatmapsearch',
- }
- }
- break
- default:
- return { id: 0, type: type }
- }
- }
- const insertBeatmapPageDownloadBtn = (id = 1011011, type = 's') => {
- const htmlText = `
- <a href="beatmap-downloader://${type}/${id}" class="btn-osu-big btn-osu-big--beatmapset-header ${beapmapPageDownloadBtnClassName}">
- <span class="btn-osu-big__content">
- <span class="btn-osu-big__left">
- <span class="btn-osu-big__text-top">启动</span>
- <span class="btn-osu-hint btn-osu-big__text-bottom">Beatmap Downloader</span>
- </span>
- <span class="btn-osu-big__icon">
- <span class="fa-fw">
- <i class="fas fa-download"></i>
- </span>
- </span>
- </span>
- </a>`
- const htmlDOM = document.createRange().createContextualFragment(htmlText)
- const btnContainer = document.querySelector('.beatmapset-header__buttons')
- const downloaderBtnQueryWith = `.${beapmapPageDownloadBtnClassName}`
- const downloaderBtnDOM = document.querySelector(downloaderBtnQueryWith)
- if (!downloaderBtnDOM) {
- btnContainer.insertBefore(htmlDOM, btnContainer.lastElementChild)
- }
- }
- const getSearchPageDownloadBtn = (id = 1011011, type = 's') => {
- const htmlText = `
- <a href="beatmap-downloader://${type}/${id}" class="beatmapset-panel__icon ${searchPageBtnClassName}" data-orig-title="Launch Downloader" aria-describedby="qtip-downloader">
- <i class="fas fa-lg fa-gamepad"></i>
- </a>`
- /**
- * @type {HTMLAnchorElement}
- */
- const htmlDOM = document.createRange().createContextualFragment(htmlText)
- return htmlDOM
- }
- const insertSearchPageDownloadTip = () => {
- const insertHTML = `
- <div
- id="qtip-downloader"
- class="qtip qtip-default tooltip-default qtip-pos-bc ${searchPageDownloadTipClassName}"
- tracking="false"
- role="alert"
- aria-live="polite"
- aria-atomic="false"
- aria-describedby="qtip-downloader-content"
- aria-hidden="true"
- data-qtip-id="downloader"
- style="z-index: 15003;"
- >
- <div class="qtip-content" id="qtip-downloader-content" aria-atomic="true">
- <span style="display: block; visibility: visible;">${searchPageBtnText}</span>
- </div>
- </div>`
- /**
- * @type {HTMLDivElement}
- */
- const htmlDOM = document.createRange().createContextualFragment(insertHTML)
- const qTipQueryWith = `.${searchPageDownloadTipClassName}`
- const qTipDOM = document.querySelector(qTipQueryWith)
- if (!qTipDOM) {
- document.body.appendChild(htmlDOM)
- }
- }
- /**
- * 获取DOM元素绝对坐标
- * @param {HTMLElement} element 要获取坐标的元素
- */
- const getAbsolutePostion = (element) => {
- const rect = element.getBoundingClientRect()
- const X = rect.left + document.documentElement.scrollLeft
- const Y = rect.top + document.documentElement.scrollTop
- const width = rect.width
- const height = rect.height
- return {
- x: X,
- y: Y,
- width,
- height,
- }
- }
- const setSearchPageDownloadTipPosition = ({ x = 0, y = 0, show = true } = {}) => {
- const qTipQueryWith = `.${searchPageDownloadTipClassName}`
- /**
- * @type {HTMLDivElement}
- */
- const qTipDOM = document.querySelector(qTipQueryWith)
- if (!qTipDOM) {
- return
- }
- if (!show) {
- qTipDOM.style.display = 'none'
- qTipDOM.style.opacity = 0
- } else {
- if (qTipDOM.style.display === 'block') {
- } else {
- qTipDOM.style.display = 'block'
- let opacitySet = 0
- if (qTipDOM.dataset.timer) {
- clearInterval(qTipDOM.dataset.timer)
- }
- qTipDOM.style.opacity = opacitySet.toString(10)
- const easeIn = setInterval(() => {
- opacitySet += 0.2
- if (opacitySet >= 1) {
- opacitySet = 1
- clearInterval(easeIn)
- }
- qTipDOM.style.opacity = opacitySet.toString(10)
- }, 16.67)
- qTipDOM.dataset.timer = easeIn
- }
- }
- qTipDOM.style.left = `${x}px`
- qTipDOM.style.top = `${y}px`
- }
- const insertSearchPageDownloadBtns = (target = document) => {
- /**
- * @type {NodeListOf<HTMLDivElement>}
- */
- const beatmapsetPannels = target.querySelectorAll('.beatmapset-panel')
- const addDownloadBtnToPannel = (beatmapsetPannel) => {
- if (beatmapsetPannel.querySelector(`.${searchPageBtnClassName}`)) {
- return
- }
- const audioURL = beatmapsetPannel.dataset.audioUrl
- const anchorLinkDOM = beatmapsetPannel.querySelectorAll('a')[0]
- const [domain, type, sid] = anchorLinkDOM.href.substr(anchorLinkDOM.href.indexOf('//') + 2).split('/')
- if (type !== 'beatmapsets') {
- return
- }
- const downloadBtn = getSearchPageDownloadBtn(sid, 's')
- const iconBox = beatmapsetPannel.querySelector('.beatmapset-panel__icons-box')
- iconBox.insertBefore(downloadBtn, iconBox.lastElementChild)
- const downloadBtnToBind = iconBox.querySelector(`.${searchPageBtnClassName}`)
- let tipWidth = 0
- let tipHeight = 0
- const showTip = () => {
- const { x, y, width, height } = getAbsolutePostion(downloadBtnToBind)
- setSearchPageDownloadTipPosition({ x: x + width / 2 - tipWidth / 2, y: y - tipHeight - 8, show: true })
- if (!tipWidth || !tipHeight) {
- const qTipQueryWith = `.${searchPageDownloadTipClassName}`
- const qTipDOM = document.querySelector(qTipQueryWith)
- if (!qTipDOM) {
- return
- }
- const rect = getAbsolutePostion(qTipDOM)
- tipWidth = rect.width
- tipHeight = rect.height
- showTip()
- }
- }
- const hideTip = () => {
- setSearchPageDownloadTipPosition({ show: false })
- }
- downloadBtnToBind.addEventListener('mousemove', showTip)
- downloadBtnToBind.addEventListener('mouseover', showTip)
- downloadBtnToBind.addEventListener('mouseenter', showTip)
- downloadBtnToBind.addEventListener('mouseleave', hideTip)
- downloadBtnToBind.addEventListener('mouseout', hideTip)
- }
- beatmapsetPannels.forEach(addDownloadBtnToPannel)
- }
- let timer = 0
- const loader = () => {
- const listenElementBeapmapPageQueryWith = '.js-react--beatmapset-page'
- const listenElementBeatmapPageDOM = document.querySelector(listenElementBeapmapPageQueryWith)
- const listenElementBeapmapSearchQueryWith = '.js-react--beatmaps'
- const listenElementBeatmapSearchDOM = document.querySelector(listenElementBeapmapSearchQueryWith)
- insertStyle()
- if (listenElementBeatmapPageDOM && listenElementBeatmapPageDOM.dataset.reactTurbolinksLoaded === '1') {
- // 谱面详情
- const formated = formatURL()
- if (formated.type === 'beatmapset') {
- insertBeatmapPageDownloadBtn(formated.id, formated.type === 'beatmapset' ? 's' : 'b')
- }
- } else if (listenElementBeatmapSearchDOM && listenElementBeatmapSearchDOM.dataset.reactTurbolinksLoaded === '1') {
- // 搜索页面
- const formated = formatURL()
- if (formated.type === 'beatmapsearch') {
- insertSearchPageDownloadTip()
- const options = {
- childList: true,
- }
- const observer = new MutationObserver((mutationsList) => {
- mutationsList.forEach((mutation) => {
- switch (mutation.type) {
- case 'childList':
- if (mutation.addedNodes) {
- mutation.addedNodes.forEach(insertSearchPageDownloadBtns)
- }
- break
- }
- })
- })
- observer.observe(document.querySelector('.beatmapsets__items'), options)
- }
- } else {
- timer = requestAnimationFrame(loader)
- }
- }
- timer = requestAnimationFrame(loader)
- // 监听 turbolinks 渲染事件
- // https://greasyfork.org/zh-CN/scripts/3916-osu-my-download
- document.addEventListener('turbolinks:load', loader)
- })()