您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fetches post title for accuracy, features an expandable title header for long names, and plays .wav/.mp3 files in a feature-rich audio player modal. No need to download WAVs or MP#s to play them anymore.
// ==UserScript== // @name Kemono Tweaks & Player // @namespace http://tampermonkey.net/ // @version 3.0 // @description Fetches post title for accuracy, features an expandable title header for long names, and plays .wav/.mp3 files in a feature-rich audio player modal. No need to download WAVs or MP#s to play them anymore. // @match https://kemono.cr/* // @author medy17 // @run-at document-start // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function () { "use strict"; try { const style = document.createElement("style"); style.textContent = ` :root { --c-primary: #3a86ff; --c-secondary: #007bff; --brilliant-white: #ffffff; } .post-card { position: relative !important; } .post-card__header { padding: 5px !important; z-index: 1 !important; color: #fff !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: 100% !important; display: block !important; position: relative !important; } .post-card:hover .post-card__header { white-space: normal !important; overflow: visible !important; background: #2e1905 !important; color: #fff !important; padding: 4px 6px !important; z-index: 9999 !important; position: absolute !important; width: auto !important; max-width: 300px !important; border-radius: 6px !important; } .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 10000; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } .modal-overlay.show { opacity: 1; } #audio-player-container { position: relative; z-index: 1; background-color: #1e1e1e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); border: 1px solid rgba(255, 255, 255, 0.1); width: 90%; max-width: 600px; transform: scale(0.95) translateY(20px); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding: 25px; } .modal-overlay.show #audio-player-container { transform: scale(1) translateY(0); } #audio-element { display: none; } #close-audio-btn { position: absolute; top: 1rem; right: 1rem; z-index: 20; background: rgba(24, 24, 27, 0.5); backdrop-filter: blur(4px); border: none; font-size: 1.5rem; color: var(--brilliant-white); cursor: pointer; padding: 0.5rem; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; transition: opacity 0.3s ease, background 0.2s ease, transform 0.2s ease; } #close-audio-btn:hover { background: linear-gradient(135deg, rgba(245, 169, 184, 0.15), rgba(91, 206, 250, 0.1)), rgba(24, 24, 27, 0.7); background-blend-mode: overlay; color: var(--c-primary); transform: rotate(90deg); } .audio-title-container { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; margin: 0 40px 20px 0; } .audio-title { flex: 1; min-width: 0; font-size: 1.3em; font-weight: 500; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .expand-arrow { flex-shrink: 0; font-size: 1.2em; color: #aaa; transition: transform 0.3s ease; } .audio-title-container.expanded .audio-title { white-space: normal; overflow-wrap: break-word; } .audio-title-container.expanded .expand-arrow { transform: rotate(180deg); } .audio-controls-container { color: #fff; user-select: none; } .audio-controls-container button { background: none; border: none; color: #fff; padding: 0; cursor: pointer; opacity: 0.85; transition: opacity 0.2s, transform 0.2s; } .audio-controls-container button:hover { opacity: 1; transform: scale(1.1); } .audio-controls-container button svg { width: 24px; height: 24px; display: block; stroke-width: 2; } .controls { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0.2rem; } .controls-left, .controls-right { display: flex; align-items: center; gap: 0.75rem; } .play-pause-btn .play-icon { display: block; fill: currentColor; } .play-pause-btn .pause-icon { display: none; fill: currentColor; } #audio-player-container:not(.paused) .play-pause-btn .play-icon { display: none; } #audio-player-container:not(.paused) .play-pause-btn .pause-icon { display: block; } .volume-container { display: flex; align-items: center; } .volume-container:hover .volume-slider { width: 80px; transform: scaleX(1); } .volume-slider { width: 0; transform: scaleX(0); transform-origin: left; transition: width 0.2s ease-in-out, transform 0.2s ease-in-out; cursor: pointer; } .volume-btn svg { stroke-width: 0; fill: currentColor; } .volume-btn .low-volume-icon, .volume-btn .muted-icon { display: none; } .time-container { font-size: 0.9rem; min-width: 95px; white-space: nowrap; text-align: center; padding: 0 0.25rem; } .timeline-container { padding: 0.5rem 0.2rem; cursor: pointer; } .timeline-container:hover .timeline { height: 8px; } .timeline-container:hover .timeline .progress-bar::after { transform: translateY(-50%) scale(1); } .timeline { height: 5px; width: 100%; background-color: rgba(255, 255, 255, 0.3); border-radius: 3px; position: relative; } .timeline .progress-bar { height: 100%; width: 0%; background: linear-gradient(90deg, var(--c-primary), var(--c-secondary)); border-radius: 3px; position: relative; transition: width 0.1s linear; } .timeline .progress-bar::after { content: ''; position: absolute; right: -6px; top: 50%; transform: translateY(-50%) scale(0); width: 12px; height: 12px; border-radius: 50%; background-color: var(--brilliant-white); transition: transform 0.2s; } .timeline .buffered-bar, .timeline .hover-indicator { position: absolute; top: 0; left: 0; height: 100%; width: 0; background-color: rgba(255, 255, 255, 0.5); border-radius: 3px; } input[type=range].volume-slider { -webkit-appearance: none; appearance: none; background: #fff0; } input[type=range].volume-slider::-webkit-slider-runnable-track { height: 5px; background: rgba(255, 255, 255, 0.3); border-radius: 3px; } input[type=range].volume-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -3.5px; background-color: #fff; height: 12px; width: 12px; border-radius: 50%; } .audio-loader { margin-bottom: 5px; } .audio-progress-bar { width: 100%; background-color: #333; border-radius: 4px; overflow: hidden; height: 8px; } .audio-progress-fill { width: 0%; height: 100%; background-image: linear-gradient(45deg, var(--c-secondary), var(--c-primary)); transition: width 0.1s linear; } .audio-progress-text { font-size: 0.8em; color: #aaa; text-align: center; margin-top: 8px; } `; (document.head || document.documentElement).appendChild(style); } catch (e) { } const shim = function () { try { const S = String.prototype; const origSlice = S.slice; const origConcat = S.concat; let lastSliceValue = null; let lastSliceSource = null; Object.defineProperty(S, "slice", { configurable: true, writable: true, value: function (start, end) { const src = String(this); const out = origSlice.call(src, start, end); if ( start === 0 && end === 50 && typeof out === "string" && src.length > 50 ) { lastSliceValue = out; lastSliceSource = src; } else { lastSliceValue = null; lastSliceSource = null; } return out; }, }); Object.defineProperty(S, "concat", { configurable: true, writable: true, value: function (...args) { try { if ( (this === "" || String(this) === "") && args.length === 2 && args[1] === "..." && typeof args[0] === "string" && lastSliceValue !== null && args[0] === lastSliceValue ) { return lastSliceSource; } } catch (e) { } return origConcat.apply(this, args); }, }); } catch (e) { } }; try { const s = document.createElement("script"); s.textContent = `(${shim})();`; (document.head || document.documentElement).appendChild(s); s.remove(); } catch (e) { } const audioPlayer = (() => { let isInitialized = false; let modalOverlay, playerContainer, audio, closeBtn, titleContainer, titleEl, loaderContainer, progressFill, progressText, controlsContainer, playPauseBtn, volumeBtn, volumeSlider, currentTimeEl, totalTimeEl, timelineContainer, progressBar, bufferedBar, hoverIndicator, downloadBtn; let currentAudioUrl = null; let currentFileName = ""; let activeRequest = null; let lastVolume = 1; function init() { if (isInitialized) return; const playerTemplate = ` <div class="modal-overlay" id="audioModal" style="display: none;"> <div id="audio-player-container"> <button id="close-audio-btn" aria-label="Close Audio Player"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/></svg></button> <div class="audio-title-container"> <span class="expand-arrow">▼</span> <h3 class="audio-title"></h3> </div> <div class="audio-loader"> <div class="audio-progress-bar"><div class="audio-progress-fill"></div></div> <div class="audio-progress-text">Initializing...</div> </div> <audio id="audio-element" preload="metadata"></audio> <div class="audio-controls-container" style="display:none;"> <div class="timeline-container"> <div class="timeline"> <div class="hover-indicator"></div><div class="buffered-bar"></div><div class="progress-bar"></div> </div> </div> <div class="controls"> <div class="controls-left"> <button class="play-pause-btn" aria-label="Play/Pause"> <svg class="play-icon" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> <svg class="pause-icon" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg> </button> <div class="volume-container"> <button class="volume-btn" aria-label="Mute/Unmute"> <svg class="high-volume-icon" viewBox="0 0 24 24"><path d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z" /></svg> <svg class="low-volume-icon" viewBox="0 0 24 24"><path d="M5,9V15H9L14,20V4L9,9M18.5,12C18.5,10.23 17.5,8.71 16,7.97V16C17.5,15.29 18.5,13.76 18.5,12Z" /></svg> <svg class="muted-icon" viewBox="0 0 24 24"><path d="M12,4L9.91,6.09L12,8.18M4.27,3L3,4.27L7.73,9H3V15H7L12,20V13.27L16.25,17.53C15.58,18.04 14.83,18.46 14,18.7V20.77C15.38,20.45 16.63,19.82 17.68,18.96L19.73,21L21,19.73L12,10.73M19,12C19,12.94 18.8,13.82 18.46,14.64L19.97,16.15C20.62,14.91 21,13.5 21,12C21,7.72 18,4.14 14,3.23V5.29C16.89,6.15 19,8.83 19,12M16.5,12C16.5,12.22 16.47,12.43 16.43,12.64L14,10.21V7.97C15.5,8.71 16.5,10.23 16.5,12Z" /></svg> </button> <input class="volume-slider" type="range" min="0" max="1" step="any" value="1"> </div> <div class="time-container"><span class="current-time">0:00</span> / <span class="total-time">0:00</span></div> </div> <div class="controls-right"><button class="download-btn" aria-label="Download Audio"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5"/></svg></button></div> </div> </div> </div> </div> `; const tempContainer = document.createElement("div"); tempContainer.innerHTML = playerTemplate.trim(); document.body.appendChild(tempContainer.firstChild); modalOverlay = document.getElementById("audioModal"); playerContainer = document.getElementById("audio-player-container"); audio = document.getElementById("audio-element"); closeBtn = document.getElementById("close-audio-btn"); titleContainer = playerContainer.querySelector(".audio-title-container"); titleEl = titleContainer.querySelector(".audio-title"); loaderContainer = playerContainer.querySelector(".audio-loader"); progressFill = playerContainer.querySelector(".audio-progress-fill"); progressText = playerContainer.querySelector(".audio-progress-text"); controlsContainer = playerContainer.querySelector( ".audio-controls-container", ); playPauseBtn = playerContainer.querySelector(".play-pause-btn"); volumeBtn = playerContainer.querySelector(".volume-btn"); volumeSlider = playerContainer.querySelector(".volume-slider"); currentTimeEl = playerContainer.querySelector(".current-time"); totalTimeEl = playerContainer.querySelector(".total-time"); timelineContainer = playerContainer.querySelector( ".timeline-container", ); progressBar = playerContainer.querySelector(".progress-bar"); bufferedBar = playerContainer.querySelector(".buffered-bar"); hoverIndicator = playerContainer.querySelector(".hover-indicator"); downloadBtn = playerContainer.querySelector(".download-btn"); bindEvents(); isInitialized = true; } const formatBytes = (bytes, d = 2) => bytes === 0 ? "0 Bytes" : `${parseFloat((bytes / Math.pow(1024, Math.floor(Math.log(bytes) / Math.log(1024)))).toFixed(d < 0 ? 0 : d))} ${["Bytes", "KB", "MB", "GB", "TB"][Math.floor(Math.log(bytes) / Math.log(1024))]}`; function bindEvents() { closeBtn.addEventListener("click", close); modalOverlay.addEventListener( "click", (e) => e.target === modalOverlay && close(), ); titleContainer.addEventListener("click", () => titleContainer.classList.toggle("expanded"), ); playPauseBtn.addEventListener("click", togglePlay); audio.addEventListener("play", () => playerContainer.classList.remove("paused"), ); audio.addEventListener("pause", () => playerContainer.classList.add("paused"), ); audio.addEventListener("loadedmetadata", handleMetadataLoaded); audio.addEventListener("timeupdate", handleTimeUpdate); audio.addEventListener("progress", handleBufferUpdate); audio.addEventListener("volumechange", updateVolumeUI); volumeBtn.addEventListener("click", toggleMute); volumeSlider.addEventListener( "input", (e) => (audio.volume = e.target.value), ); downloadBtn.addEventListener("click", downloadAudio); timelineContainer.addEventListener("mousemove", handleTimelineHover); timelineContainer.addEventListener("click", handleTimelineSeek); } function handleMetadataLoaded() { playerContainer.classList.add("paused"); totalTimeEl.textContent = formatTime(audio.duration); audio.volume = volumeSlider.value; updateVolumeUI(); audio.play().catch((e) => console.error("Autoplay prevented:", e)); } function togglePlay() { audio.paused ? audio.play() : audio.pause(); } function toggleMute() { audio.volume > 0 ? ((lastVolume = audio.volume), (audio.volume = 0)) : (audio.volume = lastVolume); } function handleTimeUpdate() { currentTimeEl.textContent = formatTime(audio.currentTime); progressBar.style.width = `${(audio.currentTime / audio.duration) * 100}%`; } function handleBufferUpdate() { if (audio.duration > 0) for (let i = 0; i < audio.buffered.length; i++) if ( audio.buffered.start(i) <= audio.currentTime && audio.currentTime <= audio.buffered.end(i) ) { bufferedBar.style.width = `${(audio.buffered.end(i) / audio.duration) * 100}%`; break; } } function handleTimelineHover(e) { const rect = timelineContainer.getBoundingClientRect(); hoverIndicator.style.width = `${(Math.min(Math.max(0, e.x - rect.x), rect.width) / rect.width) * 100}%`; } function handleTimelineSeek(e) { const rect = timelineContainer.getBoundingClientRect(); audio.currentTime = (Math.min(Math.max(0, e.x - rect.x), rect.width) / rect.width) * audio.duration; } function updateVolumeUI() { volumeSlider.value = audio.volume; const icons = [ ".high-volume-icon", ".low-volume-icon", ".muted-icon", ].map((s) => volumeBtn.querySelector(s)); icons.forEach((i) => (i.style.display = "none")); if (audio.volume === 0 || audio.muted) icons[2].style.display = "block"; else if (audio.volume < 0.5) icons[1].style.display = "block"; else icons[0].style.display = "block"; } function formatTime(t) { const r = new Date(t * 1000).toISOString().substr(11, 8); return r.startsWith("00:") ? r.substr(3) : r; } function downloadAudio() { if (!audio.src) return; const link = document.createElement("a"); link.href = audio.src; link.download = currentFileName || "kemono-audio"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function handleKeyboardShortcuts(e) { if (document.activeElement.tagName.toLowerCase() === "input") return; switch (e.key.toLowerCase()) { case "escape": close(); break; case " ": if (document.activeElement.tagName.toLowerCase() !== "button") { e.preventDefault(); togglePlay(); } break; case "m": toggleMute(); break; case "arrowright": audio.currentTime = Math.min( audio.duration, audio.currentTime + 5, ); break; case "arrowleft": audio.currentTime = Math.max(0, audio.currentTime - 5); break; } } function open(url, fileName) { if (currentAudioUrl) URL.revokeObjectURL(currentAudioUrl); currentFileName = fileName; titleContainer.classList.remove("expanded"); titleEl.textContent = fileName; loaderContainer.style.display = "block"; controlsContainer.style.display = "none"; progressFill.style.width = "0%"; progressText.textContent = "Initializing..."; modalOverlay.style.display = "flex"; setTimeout(() => modalOverlay.classList.add("show"), 10); document.body.style.overflow = "hidden"; document.addEventListener("keydown", handleKeyboardShortcuts); activeRequest = GM_xmlhttpRequest({ method: "GET", url, responseType: "blob", onprogress: (p) => { if (p.lengthComputable) { const percent = Math.round((p.loaded / p.total) * 100); progressFill.style.width = `${percent}%`; progressText.textContent = `Downloading... ${percent}% (${formatBytes(p.loaded)} / ${formatBytes(p.total)})`; } }, onload: (res) => { activeRequest = null; currentAudioUrl = URL.createObjectURL(res.response); audio.src = currentAudioUrl; audio.load(); loaderContainer.style.display = "none"; controlsContainer.style.display = "block"; }, onerror: () => { activeRequest = null; progressText.textContent = "Error: Could not load audio file."; }, onabort: () => (activeRequest = null), }); } function close() { if (activeRequest) activeRequest.abort(); audio.pause(); modalOverlay.classList.remove("show"); setTimeout(() => { modalOverlay.style.display = "none"; if (currentAudioUrl) { URL.revokeObjectURL(currentAudioUrl); currentAudioUrl = null; currentFileName = ""; audio.removeAttribute("src"); audio.load(); } }, 300); document.body.style.overflow = ""; document.removeEventListener("keydown", handleKeyboardShortcuts); } return { init, open }; })(); function initializeScript() { audioPlayer.init(); document.body.addEventListener("click", (e) => { const link = e.target.closest( 'a[href*=".wav?f="], a[href*=".mp3?f="]', ); if (link) { e.preventDefault(); e.stopPropagation(); let title = "Audio Player"; const titleElement = document.querySelector("h1.post__title"); if (titleElement) { title = titleElement.textContent.trim(); } else { try { const urlParams = new URLSearchParams(link.search); title = decodeURIComponent(urlParams.get("f")) || title; } catch { } } audioPlayer.open(link.href, title); } }); } if (document.body) { initializeScript(); } else { new MutationObserver((mutations, observer) => { if (document.body) { initializeScript(); observer.disconnect(); } }).observe(document.documentElement, { childList: true }); } })();