Fetches post title for accuracy, features an expandable title header, and plays Media (Audio/Video) in a glassmorphism player. Supports Tampermonkey menu toggle for Video.
// ==UserScript==
// @name Kemono Tweaks & Player
// @namespace http://tampermonkey.net/
// @version 4.4
// @description Fetches post title for accuracy, features an expandable title header, and plays Media (Audio/Video) in a glassmorphism player. Supports Tampermonkey menu toggle for Video.
// @match https://kemono.su/*
// @match https://kemono.cr/*
// @match https://coomer.su/*
// @match https://coomer.party/*
// @author medy17
// @run-at document-start
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const baseCss = "/* \n * No Tailwind preflight/base - we don't want to reset the host page styles!\n * Only include utilities for our player component\n */\n.kt-pointer-events-none {\n pointer-events: none;\n}\n.kt-fixed {\n position: fixed;\n}\n.kt-absolute {\n position: absolute;\n}\n.kt-relative {\n position: relative;\n}\n.kt-inset-0 {\n inset: 0px;\n}\n.kt-left-0 {\n left: 0px;\n}\n.kt-right-\\[-6px\\] {\n right: -6px;\n}\n.kt-top-0 {\n top: 0px;\n}\n.kt-top-1\\/2 {\n top: 50%;\n}\n.kt-z-\\[-1\\] {\n z-index: -1;\n}\n.kt-z-\\[10000\\] {\n z-index: 10000;\n}\n.kt-z-\\[1\\] {\n z-index: 1;\n}\n.kt-z-\\[2\\] {\n z-index: 2;\n}\n.kt-m-5 {\n margin: 1.25rem;\n}\n.kt-mb-4 {\n margin-bottom: 1rem;\n}\n.kt-mt-1 {\n margin-top: 0.25rem;\n}\n.kt-mt-2 {\n margin-top: 0.5rem;\n}\n.kt-block {\n display: block;\n}\n.kt-flex {\n display: flex;\n}\n.kt-hidden {\n display: none;\n}\n.kt-h-1 {\n height: 0.25rem;\n}\n.kt-h-10 {\n height: 2.5rem;\n}\n.kt-h-3 {\n height: 0.75rem;\n}\n.kt-h-6 {\n height: 1.5rem;\n}\n.kt-h-8 {\n height: 2rem;\n}\n.kt-h-\\[110px\\] {\n height: 110px;\n}\n.kt-h-\\[180px\\] {\n height: 180px;\n}\n.kt-h-\\[18px\\] {\n height: 18px;\n}\n.kt-h-\\[40\\%\\] {\n height: 40%;\n}\n.kt-h-full {\n height: 100%;\n}\n.kt-max-h-\\[180px\\] {\n max-height: 180px;\n}\n.kt-w-0 {\n width: 0px;\n}\n.kt-w-10 {\n width: 2.5rem;\n}\n.kt-w-3 {\n width: 0.75rem;\n}\n.kt-w-5 {\n width: 1.25rem;\n}\n.kt-w-6 {\n width: 1.5rem;\n}\n.kt-w-8 {\n width: 2rem;\n}\n.kt-w-\\[110px\\] {\n width: 110px;\n}\n.kt-w-\\[18px\\] {\n width: 18px;\n}\n.kt-w-\\[40\\%\\] {\n width: 40%;\n}\n.kt-w-\\[520px\\] {\n width: 520px;\n}\n.kt-w-\\[70px\\] {\n width: 70px;\n}\n.kt-w-\\[90\\%\\] {\n width: 90%;\n}\n.kt-w-\\[95\\%\\] {\n width: 95%;\n}\n.kt-w-full {\n width: 100%;\n}\n.kt-min-w-0 {\n min-width: 0px;\n}\n.kt-min-w-\\[90px\\] {\n min-width: 90px;\n}\n.kt-max-w-\\[1000px\\] {\n max-width: 1000px;\n}\n.kt-max-w-\\[650px\\] {\n max-width: 650px;\n}\n.kt-flex-1 {\n flex: 1 1 0%;\n}\n.kt-flex-shrink-0 {\n flex-shrink: 0;\n}\n.kt-cursor-pointer {\n cursor: pointer;\n}\n.kt-select-none {\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n}\n.kt-flex-col {\n flex-direction: column;\n}\n.kt-items-center {\n align-items: center;\n}\n.kt-justify-end {\n justify-content: flex-end;\n}\n.kt-justify-center {\n justify-content: center;\n}\n.kt-justify-between {\n justify-content: space-between;\n}\n.kt-gap-2\\.5 {\n gap: 0.625rem;\n}\n.kt-gap-5 {\n gap: 1.25rem;\n}\n.kt-overflow-hidden {\n overflow: hidden;\n}\n.kt-text-ellipsis {\n text-overflow: ellipsis;\n}\n.kt-whitespace-nowrap {\n white-space: nowrap;\n}\n.kt-rounded {\n border-radius: 0.25rem;\n}\n.kt-rounded-2xl {\n border-radius: 1rem;\n}\n.kt-rounded-full {\n border-radius: 9999px;\n}\n.kt-rounded-lg {\n border-radius: 0.5rem;\n}\n.kt-border-none {\n border-style: none;\n}\n.kt-bg-black\\/70 {\n background-color: rgb(0 0 0 / 0.7);\n}\n.kt-bg-transparent {\n background-color: transparent;\n}\n.kt-p-0 {\n padding: 0px;\n}\n.kt-p-2 {\n padding: 0.5rem;\n}\n.kt-p-5 {\n padding: 1.25rem;\n}\n.kt-py-2\\.5 {\n padding-top: 0.625rem;\n padding-bottom: 0.625rem;\n}\n.kt-pb-2 {\n padding-bottom: 0.5rem;\n}\n.kt-pb-2\\.5 {\n padding-bottom: 0.625rem;\n}\n.kt-pr-2 {\n padding-right: 0.5rem;\n}\n.kt-text-center {\n text-align: center;\n}\n.kt-font-mono {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n.kt-text-lg {\n font-size: 1.125rem;\n line-height: 1.75rem;\n}\n.kt-text-sm {\n font-size: 0.875rem;\n line-height: 1.25rem;\n}\n.kt-text-xs {\n font-size: 0.75rem;\n line-height: 1rem;\n}\n.kt-font-semibold {\n font-weight: 600;\n}\n.kt-text-gray-400 {\n --tw-text-opacity: 1;\n color: rgb(156 163 175 / var(--tw-text-opacity, 1));\n}\n.kt-text-white {\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\n}\n.kt-opacity-0 {\n opacity: 0;\n}\n.kt-opacity-20 {\n opacity: 0.2;\n}\n.kt-opacity-90 {\n opacity: 0.9;\n}\n.kt-transition-all {\n transition-property: all;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n.kt-transition-opacity {\n transition-property: opacity;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n.kt-transition-transform {\n transition-property: transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n\n/* \n * Base styles that apply to the host page elements\n * These use regular CSS (not Tailwind) to target Kemono's existing classes\n */\n\n.post-card {\n position: relative !important;\n}\n\n.post-card__header {\n padding: 5px !important;\n z-index: 1 !important;\n color: #fff !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n max-width: 100% !important;\n display: block !important;\n position: relative !important;\n}\n\n.post-card:hover .post-card__header {\n white-space: normal !important;\n overflow: visible !important;\n background: #2e1905 !important;\n color: #fff !important;\n padding: 4px 6px !important;\n z-index: 9999 !important;\n position: absolute !important;\n width: auto !important;\n max-width: 300px !important;\n border-radius: 6px !important;\n}\n";
const playerCss = "/* Modal Overlay */\n.kt-modal-overlay {\n position: fixed;\n inset: 0px;\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgb(0 0 0 / 0.7);\n opacity: 0;\n transition-property: opacity;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 300ms;\n --tw-backdrop-blur: blur(4px);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n\n.kt-modal-overlay.kt-show {\n opacity: 1;\n}\n\n/* Minimized State */\n.kt-modal-overlay.kt-minimized {\n pointer-events: none;\n background-color: transparent;\n --tw-backdrop-blur: ;\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n align-items: flex-end;\n justify-content: flex-end;\n}\n\n.kt-modal-overlay.kt-minimized .kt-player-container {\n pointer-events: auto;\n margin: 1.25rem;\n width: 520px;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);\n transform: scale(1);\n}\n\n.kt-modal-overlay.kt-minimized .kt-album-art {\n /* Visible in minimized mode */\n display: flex !important;\n}\n\n.kt-modal-overlay.kt-minimized .kt-video-wrapper {\n margin-bottom: 1rem;\n height: 180px;\n max-height: 180px;\n}\n\n.kt-modal-overlay.kt-minimized .kt-media-loader {\n display: none;\n}\n\n.kt-modal-overlay.kt-minimized .kt-top-bar {\n padding-bottom: 0.625rem;\n}\n\n.kt-modal-overlay.kt-minimized .kt-controls-right {\n margin-left: 1rem;\n}\n\n/* Main Player Container */\n.kt-player-container {\n position: relative;\n z-index: 1;\n overflow: hidden;\n border-radius: 1rem;\n box-sizing: border-box;\n width: 90%;\n max-width: 650px;\n padding: 1.25rem;\n display: flex;\n flex-direction: column;\n background-color: rgba(30, 30, 30, 0.95);\n backdrop-filter: blur(20px);\n -webkit-backdrop-filter: blur(20px);\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);\n border: 1px solid rgba(255, 255, 255, 0.1);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n transform: scale(0.95) translateY(20px);\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.kt-player-container.kt-video-mode {\n width: 95%;\n max-width: 1000px;\n}\n\n.kt-modal-overlay.kt-show .kt-player-container {\n transform: scale(1) translateY(0);\n}\n\n/* Player Backdrop (blur effect) */\n.kt-player-backdrop {\n pointer-events: none;\n position: absolute;\n inset: 0px;\n z-index: -1;\n opacity: 0.2;\n background-size: cover;\n background-position: center;\n filter: blur(40px) saturate(150%);\n}\n\n/* Top Bar (Window Controls) */\n.kt-top-bar {\n z-index: 20;\n display: flex;\n width: 100%;\n justify-content: flex-end;\n padding-bottom: 0.625rem;\n}\n\n.kt-window-controls {\n display: flex;\n gap: 0.5rem;\n}\n\n.kt-control-btn {\n cursor: pointer;\n border-radius: 9999px;\n padding: 0.5rem;\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\n display: flex;\n height: 2rem;\n width: 2rem;\n align-items: center;\n justify-content: center;\n transition-property: all;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 200ms;\n background: rgba(0, 0, 0, 0.3);\n backdrop-filter: blur(4px);\n border: 1px solid rgba(255, 255, 255, 0.1);\n font-size: 1.5rem;\n}\n\n.kt-control-btn:hover {\n --tw-text-opacity: 1;\n color: rgb(58 134 255 / var(--tw-text-opacity, 1));\n background: rgba(255, 255, 255, 0.15);\n}\n\n.kt-control-btn.kt-close-btn:hover {\n color: #ff4d4d;\n}\n\n/* Content Wrapper */\n.kt-content-wrapper {\n position: relative;\n z-index: 2;\n display: flex;\n width: 100%;\n align-items: center;\n gap: 1.25rem;\n}\n\n/* Video Wrapper */\n.kt-video-wrapper {\n position: relative;\n margin-bottom: 1rem;\n width: 100%;\n overflow: hidden;\n border-radius: 0.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #000;\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);\n}\n\n.kt-video-wrapper::after {\n content: \"\";\n pointer-events: none;\n position: absolute;\n inset: 0px;\n box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);\n}\n\n.kt-video-element {\n display: block;\n width: 100%;\n outline: 2px solid transparent;\n outline-offset: 2px;\n max-height: 65vh;\n}\n\n/* Album Art */\n.kt-album-art {\n height: 110px;\n width: 110px;\n flex-shrink: 0;\n border-radius: 0.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n background-size: cover;\n background-position: center;\n background-color: rgba(255, 255, 255, 0.05);\n box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);\n border: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.kt-album-art svg {\n height: 40%;\n width: 40%;\n color: rgba(255, 255, 255, 0.2);\n}\n\n/* Main Column */\n.kt-main-column {\n display: flex;\n min-width: 0px;\n flex: 1 1 0%;\n flex-direction: column;\n justify-content: center;\n}\n\n/* Title */\n.kt-title-container {\n margin-bottom: 1rem;\n display: flex;\n cursor: pointer;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n align-items: center;\n gap: 0.625rem;\n padding-right: 0.625rem;\n}\n\n.kt-media-title {\n min-width: 0px;\n flex: 1 1 0%;\n font-size: 1.125rem;\n line-height: 1.75rem;\n font-weight: 600;\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\n margin: 0px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);\n}\n\n.kt-expand-arrow {\n flex-shrink: 0;\n font-size: 0.875rem;\n line-height: 1.25rem;\n --tw-text-opacity: 1;\n color: rgb(156 163 175 / var(--tw-text-opacity, 1));\n transition-property: transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 300ms;\n}\n\n.kt-title-container.kt-expanded .kt-media-title {\n white-space: normal;\n overflow-wrap: break-word;\n}\n\n.kt-title-container.kt-expanded .kt-expand-arrow {\n --tw-rotate: 180deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n\n/* Media Controls Container */\n.kt-controls-container {\n width: 100%;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\n}\n\n.kt-controls-container button {\n cursor: pointer;\n border-style: none;\n background-color: transparent;\n padding: 0px;\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\n opacity: 0.9;\n transition-property: opacity;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 200ms;\n}\n\n.kt-controls-container button:hover {\n opacity: 1;\n}\n\n.kt-controls-container button svg {\n display: block;\n height: 1.5rem;\n width: 1.5rem;\n stroke-width: 2;\n filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.3));\n}\n\n/* Controls Row */\n.kt-controls {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.kt-controls-left,\n.kt-controls-right {\n display: flex;\n align-items: center;\n gap: 1rem;\n}\n\n/* Play/Pause Button */\n.kt-play-pause-btn {\n display: flex;\n height: 2.5rem;\n width: 2.5rem;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n background: #fff !important;\n color: #000 !important;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);\n}\n\n.kt-play-pause-btn svg {\n height: 18px;\n width: 18px;\n margin-left: 1px;\n}\n\n.kt-play-pause-btn .kt-play-icon {\n display: block;\n fill: currentColor;\n stroke: none !important;\n}\n\n.kt-play-pause-btn .kt-pause-icon {\n display: none;\n fill: currentColor;\n stroke: none !important;\n}\n\n.kt-player-container:not(.kt-paused) .kt-play-pause-btn .kt-play-icon {\n display: none;\n}\n\n.kt-player-container:not(.kt-paused) .kt-play-pause-btn .kt-pause-icon {\n display: block;\n margin-left: 0;\n}\n\n/* Volume Container */\n.kt-volume-container {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.kt-volume-slider {\n height: 0.25rem;\n width: 70px;\n cursor: pointer;\n border-radius: 0.25rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n background: rgba(255, 255, 255, 0.3);\n}\n\n.kt-volume-slider::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 10px;\n height: 10px;\n background: #fff;\n border-radius: 50%;\n border: none;\n cursor: pointer;\n}\n\n.kt-volume-slider::-moz-range-thumb {\n width: 10px;\n height: 10px;\n background: #fff;\n border-radius: 50%;\n border: none;\n cursor: pointer;\n}\n\n.kt-volume-btn svg {\n height: 1.25rem;\n width: 1.25rem;\n stroke-width: 0;\n fill: currentColor;\n}\n\n.kt-volume-btn .kt-low-volume-icon,\n.kt-volume-btn .kt-muted-icon {\n display: none;\n}\n\n/* Time Display */\n.kt-time-container {\n min-width: 90px;\n white-space: nowrap;\n text-align: center;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 0.875rem;\n line-height: 1.25rem;\n color: rgba(255, 255, 255, 0.7);\n}\n\n/* Timeline */\n.kt-timeline-container {\n margin-top: 0.375rem;\n cursor: pointer;\n padding-top: 0.625rem;\n padding-bottom: 0.625rem;\n}\n\n.kt-timeline {\n position: relative;\n height: 0.25rem;\n width: 100%;\n border-radius: 0.25rem;\n transition-property: all;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 200ms;\n background-color: rgba(255, 255, 255, 0.15);\n}\n\n.kt-timeline-container:hover .kt-timeline {\n height: 0.375rem;\n}\n\n.kt-timeline-container:hover .kt-progress-bar::after {\n transform: translateY(-50%) scale(1);\n}\n\n.kt-progress-bar {\n position: relative;\n height: 100%;\n width: 0px;\n border-radius: 0.25rem;\n background: linear-gradient(90deg, #3a86ff, #007bff);\n transition: width 0.1s linear;\n}\n\n.kt-progress-bar::after {\n content: \"\";\n position: absolute;\n right: -6px;\n top: 50%;\n height: 0.75rem;\n width: 0.75rem;\n border-radius: 9999px;\n --tw-bg-opacity: 1;\n background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));\n transform: translateY(-50%) scale(0);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n transition: transform 0.2s;\n}\n\n.kt-buffered-bar,\n.kt-hover-indicator {\n position: absolute;\n top: 0px;\n left: 0px;\n height: 100%;\n width: 0px;\n border-radius: 0.25rem;\n background-color: rgba(255, 255, 255, 0.2);\n}\n\n/* Media Loader */\n.kt-media-loader {\n margin-top: 0.625rem;\n}\n\n.kt-progress-bar-loader {\n height: 0.375rem;\n width: 100%;\n overflow: hidden;\n border-radius: 0.25rem;\n background-color: rgba(255, 255, 255, 0.1);\n}\n\n.kt-progress-fill {\n height: 100%;\n width: 0px;\n background-image: linear-gradient(45deg, #007bff, #3a86ff);\n transition: width 0.1s linear;\n}\n\n.kt-progress-text {\n margin-top: 0.375rem;\n text-align: center;\n font-size: 0.75rem;\n line-height: 1rem;\n color: rgba(255, 255, 255, 0.6);\n}";
function getConfig() {
return {
videoEnabled: GM_getValue("enableVideoPlayer", true)
};
}
function registerMenuCommands(config) {
GM_registerMenuCommand(
`Video Player: ${config.videoEnabled ? "✅ ON" : "❌ OFF"} (Click to Toggle)`,
() => {
GM_setValue("enableVideoPlayer", !config.videoEnabled);
location.reload();
}
);
}
function injectTitleShim() {
const shimCode = `
(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 script = document.createElement("script");
script.textContent = shimCode;
(document.head || document.documentElement).appendChild(script);
script.remove();
} catch {
}
}
const icons = {
music: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12,3V12.26C11.5,12.09 11,12 10.5,12C8,12 6,14 6,16.5C6,19 8,21 10.5,21C13,21 15,19 15,16.5V6H19V3H12Z" /></svg>`,
minimize: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><path d="M19,13H5V11H19V13Z" /></svg>`,
maximize: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><path d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,19H5V5H19V19Z" /></svg>`,
close: `<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>`,
play: `<svg class="kt-play-icon" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`,
pause: `<svg class="kt-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>`,
volumeHigh: `<svg class="kt-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>`,
volumeLow: `<svg class="kt-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>`,
volumeMuted: `<svg class="kt-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>`,
fullscreen: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/></svg>`,
download: `<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>`
};
function createPlayerTemplate() {
return `
<div class="kt-modal-overlay" id="kt-media-modal" style="display: none;">
<div class="kt-player-container" id="kt-player-container">
<div class="kt-player-backdrop"></div>
<div class="kt-top-bar">
<div class="kt-window-controls">
<button id="kt-minimize-btn" class="kt-control-btn" aria-label="Minimize">
${icons.minimize}
</button>
<button id="kt-close-btn" class="kt-control-btn kt-close-btn" aria-label="Close">
${icons.close}
</button>
</div>
</div>
<!-- Video Element Wrapper -->
<div class="kt-video-wrapper" style="display:none;">
<video class="kt-video-element" id="kt-video-element" preload="metadata"></video>
</div>
<div class="kt-content-wrapper">
<!-- Album Art (Audio Only) -->
<div class="kt-album-art">
${icons.music}
</div>
<div class="kt-main-column">
<div class="kt-title-container">
<span class="kt-expand-arrow">▼</span>
<h3 class="kt-media-title"></h3>
</div>
<!-- Audio Element (Hidden) -->
<audio id="kt-audio-element" preload="metadata"></audio>
<div class="kt-media-loader">
<div class="kt-progress-bar-loader">
<div class="kt-progress-fill"></div>
</div>
<div class="kt-progress-text">Buffering...</div>
</div>
<div class="kt-controls-container" style="display:none;">
<div class="kt-timeline-container">
<div class="kt-timeline">
<div class="kt-hover-indicator"></div>
<div class="kt-buffered-bar"></div>
<div class="kt-progress-bar"></div>
</div>
</div>
<div class="kt-controls">
<div class="kt-controls-left">
<button class="kt-play-pause-btn" aria-label="Play/Pause">
${icons.play}
${icons.pause}
</button>
<div class="kt-volume-container">
<button class="kt-volume-btn" aria-label="Mute/Unmute">
${icons.volumeHigh}
${icons.volumeLow}
${icons.volumeMuted}
</button>
<input class="kt-volume-slider" type="range" min="0" max="1" step="any" value="1">
</div>
<div class="kt-time-container">
<span class="kt-current-time">0:00</span> / <span class="kt-total-time">0:00</span>
</div>
</div>
<div class="kt-controls-right">
<button class="kt-fullscreen-btn" aria-label="Fullscreen" style="display:none;">
${icons.fullscreen}
</button>
<button class="kt-download-btn" aria-label="Download Media">
${icons.download}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
}
class MediaPlayer {
isInitialized = false;
elements = null;
currentMediaEl = null;
currentMediaUrl = null;
currentFileName = "";
currentType = "audio";
activeRequest = null;
lastVolume = 1;
init() {
if (this.isInitialized) return;
const tempContainer = document.createElement("div");
tempContainer.innerHTML = createPlayerTemplate().trim();
const modal = tempContainer.firstChild;
if (modal) {
document.body.appendChild(modal);
}
this.elements = this.getElements();
this.bindEvents();
this.isInitialized = true;
}
getElements() {
const $ = (sel) => {
const el = document.querySelector(sel);
if (!el) throw new Error(`Element not found: ${sel}`);
return el;
};
return {
modalOverlay: $("#kt-media-modal"),
playerContainer: $("#kt-player-container"),
backdrop: $(".kt-player-backdrop"),
albumArt: $(".kt-album-art"),
videoWrapper: $(".kt-video-wrapper"),
audioEl: $("#kt-audio-element"),
videoEl: $("#kt-video-element"),
closeBtn: $("#kt-close-btn"),
minimizeBtn: $("#kt-minimize-btn"),
titleContainer: $(".kt-title-container"),
titleEl: $(".kt-media-title"),
loaderContainer: $(".kt-media-loader"),
progressFill: $(".kt-progress-fill"),
progressText: $(".kt-progress-text"),
controlsContainer: $(".kt-controls-container"),
playPauseBtn: $(".kt-play-pause-btn"),
volumeBtn: $(".kt-volume-btn"),
volumeSlider: $(".kt-volume-slider"),
currentTimeEl: $(".kt-current-time"),
totalTimeEl: $(".kt-total-time"),
timelineContainer: $(".kt-timeline-container"),
progressBar: $(".kt-progress-bar"),
bufferedBar: $(".kt-buffered-bar"),
hoverIndicator: $(".kt-hover-indicator"),
downloadBtn: $(".kt-download-btn"),
fullscreenBtn: $(".kt-fullscreen-btn")
};
}
bindEvents() {
const el = this.elements;
el.closeBtn.addEventListener("click", () => this.close());
el.minimizeBtn.addEventListener("click", (e) => this.toggleMinimize(e));
el.modalOverlay.addEventListener("click", (e) => {
if (e.target === el.modalOverlay) this.toggleMinimize(e);
});
el.titleContainer.addEventListener("click", () => {
el.titleContainer.classList.toggle("kt-expanded");
});
[el.audioEl, el.videoEl].forEach((media) => {
media.addEventListener("play", () => el.playerContainer.classList.remove("kt-paused"));
media.addEventListener("pause", () => el.playerContainer.classList.add("kt-paused"));
media.addEventListener("loadedmetadata", () => this.handleMetadataLoaded());
media.addEventListener("timeupdate", () => this.handleTimeUpdate());
media.addEventListener("progress", () => this.handleBufferUpdate());
media.addEventListener("volumechange", () => this.updateVolumeUI());
media.addEventListener("ended", () => el.playerContainer.classList.add("kt-paused"));
});
el.videoEl.addEventListener("click", () => this.togglePlay());
el.videoEl.addEventListener("dblclick", () => this.toggleFullscreen());
el.playPauseBtn.addEventListener("click", () => this.togglePlay());
el.volumeBtn.addEventListener("click", () => this.toggleMute());
el.volumeSlider.addEventListener("input", (e) => {
if (this.currentMediaEl) {
this.currentMediaEl.volume = parseFloat(e.target.value);
}
});
el.downloadBtn.addEventListener("click", () => this.downloadMedia());
el.fullscreenBtn.addEventListener("click", () => this.toggleFullscreen());
el.timelineContainer.addEventListener("mousemove", (e) => this.handleTimelineHover(e));
el.timelineContainer.addEventListener("click", (e) => this.handleTimelineSeek(e));
}
toggleMinimize(e) {
e.stopPropagation();
const el = this.elements;
el.modalOverlay.classList.toggle("kt-minimized");
const isMinimized = el.modalOverlay.classList.contains("kt-minimized");
if (isMinimized) {
document.body.style.overflow = "";
el.minimizeBtn.innerHTML = icons.maximize;
} else {
document.body.style.overflow = "hidden";
el.minimizeBtn.innerHTML = icons.minimize;
}
}
toggleFullscreen() {
if (this.currentType !== "video") return;
const el = this.elements;
if (!document.fullscreenElement) {
if (el.videoEl.requestFullscreen) {
el.videoEl.requestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
handleMetadataLoaded() {
if (!this.currentMediaEl) return;
const el = this.elements;
el.playerContainer.classList.add("kt-paused");
el.totalTimeEl.textContent = this.formatTime(this.currentMediaEl.duration);
this.currentMediaEl.volume = parseFloat(el.volumeSlider.value);
this.updateVolumeUI();
this.currentMediaEl.play().catch(() => {
});
}
togglePlay() {
if (!this.currentMediaEl) return;
if (this.currentMediaEl.paused) {
this.currentMediaEl.play();
} else {
this.currentMediaEl.pause();
}
}
toggleMute() {
if (!this.currentMediaEl) return;
if (this.currentMediaEl.volume > 0) {
this.lastVolume = this.currentMediaEl.volume;
this.currentMediaEl.volume = 0;
} else {
this.currentMediaEl.volume = this.lastVolume;
}
}
handleTimeUpdate() {
if (!this.currentMediaEl) return;
const el = this.elements;
el.currentTimeEl.textContent = this.formatTime(this.currentMediaEl.currentTime);
if (this.currentMediaEl.duration) {
const percent = this.currentMediaEl.currentTime / this.currentMediaEl.duration * 100;
el.progressBar.style.width = `${percent}%`;
}
}
handleBufferUpdate() {
if (!this.currentMediaEl || !this.currentMediaEl.duration) return;
const el = this.elements;
for (let i = 0; i < this.currentMediaEl.buffered.length; i++) {
const start = this.currentMediaEl.buffered.start(i);
const end = this.currentMediaEl.buffered.end(i);
if (start <= this.currentMediaEl.currentTime && this.currentMediaEl.currentTime <= end) {
const percent = end / this.currentMediaEl.duration * 100;
el.bufferedBar.style.width = `${percent}%`;
break;
}
}
}
handleTimelineHover(e) {
const el = this.elements;
const rect = el.timelineContainer.getBoundingClientRect();
const pos = Math.min(Math.max(0, e.clientX - rect.left), rect.width);
el.hoverIndicator.style.width = `${pos / rect.width * 100}%`;
}
handleTimelineSeek(e) {
if (!this.currentMediaEl) return;
const el = this.elements;
const rect = el.timelineContainer.getBoundingClientRect();
const pos = Math.min(Math.max(0, e.clientX - rect.left), rect.width) / rect.width;
this.currentMediaEl.currentTime = pos * this.currentMediaEl.duration;
}
updateVolumeUI() {
if (!this.currentMediaEl) return;
const el = this.elements;
el.volumeSlider.value = String(this.currentMediaEl.volume);
const highIcon = el.volumeBtn.querySelector(".kt-high-volume-icon");
const lowIcon = el.volumeBtn.querySelector(".kt-low-volume-icon");
const mutedIcon = el.volumeBtn.querySelector(".kt-muted-icon");
[highIcon, lowIcon, mutedIcon].forEach((i) => i.style.display = "none");
if (this.currentMediaEl.volume === 0 || this.currentMediaEl.muted) {
mutedIcon.style.display = "block";
} else if (this.currentMediaEl.volume < 0.5) {
lowIcon.style.display = "block";
} else {
highIcon.style.display = "block";
}
}
formatTime(t) {
if (isNaN(t)) return "0:00";
const result = new Date(t * 1e3).toISOString().substr(11, 8);
return result.startsWith("00:") ? result.substr(3) : result;
}
formatBytes(bytes, decimals = 2) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals < 0 ? 0 : decimals))} ${sizes[i]}`;
}
downloadMedia() {
if (!this.currentMediaEl) return;
const url = this.currentMediaEl.src;
const name = this.currentFileName || "kemono-media";
if (typeof GM_download === "function") {
GM_download({
url,
name,
saveAs: true
});
return;
}
const link = document.createElement("a");
link.href = url;
link.download = name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
handleKeyboardShortcuts = (e) => {
const activeTag = document.activeElement?.tagName.toLowerCase();
if (activeTag === "input") return;
const el = this.elements;
if (!el.modalOverlay.classList.contains("kt-show")) return;
switch (e.key.toLowerCase()) {
case "escape":
this.close();
break;
case " ":
if (activeTag !== "button") {
e.preventDefault();
this.togglePlay();
}
break;
case "f":
this.toggleFullscreen();
break;
case "m":
this.toggleMute();
break;
case "arrowright":
if (this.currentMediaEl) {
this.currentMediaEl.currentTime = Math.min(
this.currentMediaEl.duration,
this.currentMediaEl.currentTime + 5
);
}
break;
case "arrowleft":
if (this.currentMediaEl) {
this.currentMediaEl.currentTime = Math.max(0, this.currentMediaEl.currentTime - 5);
}
break;
}
};
open(url, fileName, type) {
if (this.currentMediaUrl) {
URL.revokeObjectURL(this.currentMediaUrl);
}
const el = this.elements;
this.currentFileName = fileName;
this.currentType = type;
el.titleContainer.classList.remove("kt-expanded");
el.modalOverlay.classList.remove("kt-minimized");
el.minimizeBtn.innerHTML = icons.minimize;
el.titleEl.textContent = fileName;
el.loaderContainer.style.display = "block";
el.controlsContainer.style.display = "none";
el.progressFill.style.width = "0%";
el.progressText.textContent = "Initializing...";
el.modalOverlay.style.display = "flex";
el.audioEl.pause();
el.videoEl.pause();
el.audioEl.removeAttribute("src");
el.videoEl.removeAttribute("src");
const img = document.querySelector(".post__thumbnail img") || document.querySelector(".post__content img") || document.querySelector(".user-header__avatar img") || document.querySelector("img.fancy-image__image");
if (img && img.src) {
el.backdrop.style.backgroundImage = `url('${img.src}')`;
el.albumArt.style.backgroundImage = `url('${img.src}')`;
el.albumArt.innerHTML = "";
} else {
el.backdrop.style.backgroundImage = "none";
el.albumArt.style.backgroundImage = "none";
el.albumArt.innerHTML = icons.music;
}
if (type === "video") {
this.currentMediaEl = el.videoEl;
el.playerContainer.classList.add("kt-video-mode");
el.albumArt.style.display = "none";
el.videoWrapper.style.display = "flex";
el.fullscreenBtn.style.display = "block";
el.videoEl.src = url;
el.videoEl.load();
el.loaderContainer.style.display = "none";
el.controlsContainer.style.display = "block";
} else {
this.currentMediaEl = el.audioEl;
el.playerContainer.classList.remove("kt-video-mode");
el.albumArt.style.display = "flex";
el.videoWrapper.style.display = "none";
el.fullscreenBtn.style.display = "none";
this.activeRequest = GM_xmlhttpRequest({
method: "GET",
url,
responseType: "blob",
onprogress: (p) => {
if (p.lengthComputable) {
const percent = Math.round(p.loaded / p.total * 100);
el.progressFill.style.width = `${percent}%`;
el.progressText.textContent = `Downloading... ${percent}% (${this.formatBytes(p.loaded)} / ${this.formatBytes(p.total)})`;
}
},
onload: (res) => {
this.activeRequest = null;
this.currentMediaUrl = URL.createObjectURL(res.response);
el.audioEl.src = this.currentMediaUrl;
el.audioEl.load();
el.loaderContainer.style.display = "none";
el.controlsContainer.style.display = "block";
},
onerror: () => {
this.activeRequest = null;
el.progressText.textContent = "Error: Could not load audio file.";
},
onabort: () => {
this.activeRequest = null;
}
});
}
setTimeout(() => el.modalOverlay.classList.add("kt-show"), 10);
document.body.style.overflow = "hidden";
document.addEventListener("keydown", this.handleKeyboardShortcuts);
}
close() {
const el = this.elements;
if (this.activeRequest) this.activeRequest.abort();
if (this.currentMediaEl) this.currentMediaEl.pause();
el.modalOverlay.classList.remove("kt-show");
setTimeout(() => {
el.modalOverlay.style.display = "none";
el.modalOverlay.classList.remove("kt-minimized");
if (this.currentMediaUrl) {
URL.revokeObjectURL(this.currentMediaUrl);
this.currentMediaUrl = null;
}
el.audioEl.removeAttribute("src");
el.videoEl.removeAttribute("src");
el.videoEl.load();
el.audioEl.load();
this.currentFileName = "";
}, 300);
document.body.style.overflow = "";
document.removeEventListener("keydown", this.handleKeyboardShortcuts);
}
}
try {
const style = document.createElement("style");
style.textContent = baseCss + playerCss;
(document.head || document.documentElement).appendChild(style);
} catch (e) {
}
let config;
try {
config = getConfig();
registerMenuCommands(config);
} catch (e) {
console.error("[KemonoTweaks] Error loading config:", e);
config = { videoEnabled: true };
}
try {
injectTitleShim();
} catch (e) {
console.error("[KemonoTweaks] Error injecting title shim:", e);
}
const mediaPlayer = new MediaPlayer();
function initializeScript() {
try {
mediaPlayer.init();
const audioExtensions = /\.(mp3|wav|m4a|ogg|flac)$/i;
const videoExtensions = /\.(mp4|m4v|webm|mov)$/i;
window.addEventListener(
"click",
(e) => {
const link = e.target.closest(
'a[href*="?f="]'
);
if (link) {
let type = null;
let checkStr = "";
try {
const urlParams = new URLSearchParams(link.search);
checkStr = urlParams.get("f") || "";
} catch {
}
if (!checkStr) {
checkStr = link.pathname;
}
if (audioExtensions.test(checkStr)) {
type = "audio";
} else if (config.videoEnabled && videoExtensions.test(checkStr)) {
type = "video";
}
if (type) {
e.preventDefault();
e.stopPropagation();
let title = "Media Player";
const titleElement = document.querySelector("h1.post__title");
if (titleElement) {
title = titleElement.textContent?.trim() || title;
} else if (checkStr) {
title = decodeURIComponent(checkStr);
}
mediaPlayer.open(link.href, title, type);
}
}
},
true
// Capture phase
);
} catch (e) {
console.error("[KemonoTweaks] Error in initializeScript:", e);
}
}
if (document.body) {
initializeScript();
} else {
new MutationObserver((_, observer) => {
if (document.body) {
initializeScript();
observer.disconnect();
}
}).observe(document.documentElement, { childList: true });
}
})();