Skip to content

Instantly share code, notes, and snippets.

@Atulin
Last active March 7, 2026 22:00
Show Gist options
  • Select an option

  • Save Atulin/9cc5a10b615a8a21adb1cd61c963e25a to your computer and use it in GitHub Desktop.

Select an option

Save Atulin/9cc5a10b615a8a21adb1cd61c963e25a to your computer and use it in GitHub Desktop.

Derpi Show

vgy.me

Usage

hotkey action
A or 🠈 previous image
D or 🠊 next image
F favourite
U upvote
V change transparent image background color
, and . seek video back and forth
space play/pause video
M mute and unmute video
Alt + Enter fullscreen video
Esc close slideshow

Changelog

2.2.0

  • Fixed hotkeys being active even when slideshow wasn't, leading to switching between images when writing a comment, for example
  • Added the V hotkey to cycle the background color of transparent images between transparent/black/white

2.1.0

Major refactor

  • GM_ functions instead of localstorage to keep the state
  • More funky, but also more ergonomic handling of hotkeys
  • Added prefething of the previous page, not just the next one
  • Added fullscreen hotkey

2.0.0

Initial public release

// ==UserScript==
// @name Derpi Show
// @namespace Violentmonkey scripts
// @version 2.3.0
// @author Angius
// @description Derpibooru slideshow viewer
// @match https://derpibooru.org/images/*
// @match https://ponerpics.org/images/*
// @match https://ponybooru.org/images/*
// @match https://twibooru.org/images/*
// @match https://manebooru.art/images/*
// @match https://furbooru.org/images/*
// @require https://vanjs.org/code/van-1.6.0.nomodule.min.js
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @top-level-await
// ==/UserScript==
var derpi_show_default = `[derpi-show-active="true"] {
#content {
position: fixed;
inset: 0;
padding: 0;
margin: 0;
background-color: oklch(0.19 0.002 285);
> .block {
background-color: transparent;
position: fixed;
top: 0;
left: 0;
}
.image-metabar {
background-color: transparent;
opacity: 0.5;
&:hover {
opacity: 0.9;
}
div:not(:nth-of-type(2)) {
display: none;
}
[class*="interaction--"] {
background-color: transparent;
}
.interaction--comments,
.interaction--hide {
display: none;
}
}
#extrameta {
display: none;
}
.center--layout--flex {
height: 100vh;
width: 100vw;
align-content: center;
align-items: center;
justify-items: center;
display: flex;
justify-content: center;
margin-left: calc(var(--normal-margin) * -1);
.image-show-container {
box-shadow: 0 0 5rem black;
padding: 0;
}
.image-scaled {
max-height: 100vh;
}
}
}
}
#derpi-show-btn {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
border: var(--border);
background-color: var(--block-header-color);
border-radius: 500rem;
aspect-ratio: 1 / 1;
color: var(--foreground-color);
display: flex;
padding: 0.25rem;
cursor: pointer;
&:hover {
background-color: var(--block-header-hover-color);
}
svg {
width: 24px;
height: 24px;
}
}
`;
var hotkeys = (on) => {
const handlers = [];
const conditions = [];
return {
when(condition) {
conditions.push(condition);
return this;
},
add(keys, handler) {
handlers.push({ keys, handler });
return this;
},
handle() {
const flat = handlers.reduce((acc, curr) => {
for (const key of curr.keys) {
acc.set(key, curr.handler);
}
return acc;
}, new Map);
const listener = async (e) => {
if (!(e instanceof KeyboardEvent)) {
return;
}
if (!conditions.some((c) => c())) {
return;
}
const handler = flat.get(e.key);
if (!handler) {
return;
}
await handler(e);
};
window.addEventListener(on, listener);
return () => window.removeEventListener(on, listener);
}
};
};
var rolling = (items) => {
if (items.length === 0) {
throw Error("Rolling collection cannot be empty");
}
let index = 0;
const n = items.length;
return {
next() {
index = (index + 1) % n;
return items[index];
},
prev() {
index = (index - 1 + n) % n;
return items[index];
},
current() {
return items[index];
}
};
};
var slideshow_default = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Remix Icon by Remix Design - https://github.com/Remix-Design/RemixIcon/blob/master/License --><path fill="currentColor" d="M13 17v3h5v2H6v-2h5v-3H4a1 1 0 0 1-1-1V4H2V2h20v2h-1v12a1 1 0 0 1-1 1zm-8-2h14V4H5zm5-9l5 3.5l-5 3.5z"/></svg>';
GM_addStyle(derpi_show_default);
var { button, link } = van.tags;
var isActive = van.state(GM_getValue("derpi-show-active"));
document.documentElement.setAttribute("derpi-show-active", String(isActive.val));
van.derive(() => {
console.log(`Derpi Show is ${isActive.val ? "active" : "inactive"}`);
GM_setValue("derpi-show-active", isActive.val);
document.documentElement.setAttribute("derpi-show-active", String(isActive.val));
});
var toggleBtn = button({
id: "derpi-show-btn",
innerHTML: slideshow_default,
onclick: () => isActive.val = !isActive.val
});
van.add(document.body, toggleBtn);
var preload = (url) => {
if (!url) {
return;
}
const el = link({
rel: "prefetch",
href: url,
fetchPriority: "auto",
as: "document"
});
van.add(document.head, el);
};
var prev = document.querySelector("a.js-prev[href]");
var next = document.querySelector("a.js-next[href]");
if (prev) {
preload(prev.href);
}
if (next) {
preload(next.href);
}
var withPlayer = (func) => {
const vid = document.querySelector(".image-target video");
if (vid) {
return func(vid);
}
};
var backgrounds = rolling(["transparent", "black", "white"]);
hotkeys("keyup").when(() => isActive.val).add(["a", "ArrowLeft"], () => {
preload(prev?.href);
prev?.click();
}).add(["d", "ArrowRight"], () => {
preload(next?.href);
next?.click();
}).add(["Escape"], () => {
isActive.val = false;
}).add(["v"], () => {
const img = document.querySelector("img#image-display");
if (!img)
return;
img.style.backgroundColor = backgrounds.next();
}).add([" "], (e) => {
e.preventDefault();
withPlayer(async (vid) => {
if (vid.paused) {
await vid.play();
} else {
vid.pause();
}
});
}).add([","], () => withPlayer((vid) => {
vid.currentTime = vid.currentTime - Math.min(vid.currentTime / 10, 5);
})).add(["."], () => withPlayer((vid) => {
vid.currentTime = vid.currentTime + Math.min(vid.currentTime / 10, 5);
})).add(["m"], () => withPlayer((vid) => {
vid.muted = !vid.muted;
})).add(["Enter"], (e) => withPlayer(async (vid) => {
if (!e.altKey) {
return;
}
await vid.requestFullscreen();
})).handle();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment