|
// ==UserScript== |
|
// @name Pinkbike Photoepics Fullscreen Viewer |
|
// @match https://www.pinkbike.com/news/*photo-epic* |
|
// @grant GM_addStyle |
|
// ==/UserScript== |
|
|
|
/* global alert */ |
|
|
|
const DEBUG = false |
|
const log = (...args) => { if (DEBUG) console.log("[PB Photoepics]", ...args) } |
|
|
|
// eslint-disable-next-line no-undef |
|
GM_addStyle(` |
|
#pb-photoepic-fullscreen { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100vw; |
|
height: 100vh; |
|
background: rgba(0, 0, 0, 0.95); |
|
z-index: 10000; |
|
display: none; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
#pb-photoepic-fullscreen.pb-pe-active { |
|
display: flex; |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-image-container { |
|
max-width: 100%; |
|
max-height: 100vh; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
} |
|
|
|
#pb-photoepic-fullscreen img { |
|
width: 100vw; |
|
height: 100vh; |
|
max-width: 100%; |
|
max-height: 100%; |
|
object-fit: contain; |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-caption { |
|
position: fixed; |
|
bottom: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
color: white; |
|
font-size: 16px; |
|
text-align: center; |
|
max-width: 800px; |
|
padding: 0 60px; |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-close-btn { |
|
color: white; |
|
font-size: 12px; |
|
opacity: 0.4; |
|
cursor: pointer; |
|
margin-left: 12px; |
|
text-decoration: none; |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-close-btn:hover { |
|
opacity: 0.6; |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-nav-arrows { |
|
position: absolute; |
|
top: 50%; |
|
transform: translateY(-50%); |
|
width: 100%; |
|
display: flex; |
|
justify-content: space-between; |
|
padding: 0 20px; |
|
pointer-events: none; |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-nav-arrow { |
|
background: rgba(255, 255, 255, 0.0); /* transparent */ |
|
border: none; |
|
color: white; |
|
padding: 12px 10px; |
|
cursor: pointer; |
|
font-size: 18px; |
|
border-radius: 4px; |
|
pointer-events: all; |
|
user-select: none; |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-nav-arrow:hover { |
|
background: rgba(255, 255, 255, 0.2); /* semi-transparent */ |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-nav-arrow:disabled { |
|
opacity: 0.3; |
|
cursor: not-allowed; |
|
} |
|
|
|
#pb-photoepic-fullscreen .pb-pe-counter { |
|
position: fixed; |
|
bottom: 20px; |
|
left: 20px; |
|
color: white; |
|
font-size: 12px; |
|
opacity: 0.4; |
|
} |
|
`) |
|
|
|
let photos = [] |
|
let currentIndex = 0 |
|
let fullscreenContainer = null |
|
|
|
function extractPhotos () { |
|
log("Extracting photos...") |
|
photos = [] |
|
|
|
const mediaElements = document.querySelectorAll(".media-full-width") |
|
log(`Found ${mediaElements.length} media elements`) |
|
|
|
mediaElements.forEach(el => { |
|
const img = el.querySelector("img.news-photo") |
|
const captionEl = el.querySelector(".news-photo-caption") |
|
|
|
if (img) { |
|
// Handle lazy-loaded images (data-src) and regular images (src) |
|
const imgSrc = img.dataset.src || img.src |
|
if (imgSrc) { |
|
photos.push({ |
|
src: imgSrc, |
|
alt: img.alt || "", |
|
caption: captionEl ? captionEl.textContent.trim() : "", |
|
}) |
|
} |
|
} |
|
}) |
|
|
|
log(`Extracted ${photos.length} photos`) |
|
return photos |
|
} |
|
|
|
function createFullscreenViewer () { |
|
if (fullscreenContainer) return fullscreenContainer |
|
|
|
const container = document.createElement("div") |
|
container.id = "pb-photoepic-fullscreen" |
|
container.innerHTML = ` |
|
<div class="pb-pe-counter"> |
|
<span class="pb-pe-counter-text"></span> |
|
<span class="pb-pe-close-btn">Close (ESC)</span> |
|
</div> |
|
<div class="pb-pe-nav-arrows"> |
|
<button class="pb-pe-nav-arrow pb-pe-prev-btn">←</button> |
|
<button class="pb-pe-nav-arrow pb-pe-next-btn">→</button> |
|
</div> |
|
<div class="pb-pe-image-container"> |
|
<img src="" alt=""> |
|
<div class="pb-pe-caption"></div> |
|
</div> |
|
` |
|
|
|
document.body.appendChild(container) |
|
|
|
// Event listeners |
|
container.querySelector(".pb-pe-close-btn").addEventListener("click", closeFullscreen) |
|
container.querySelector(".pb-pe-prev-btn").addEventListener("click", showPrevious) |
|
container.querySelector(".pb-pe-next-btn").addEventListener("click", showNext) |
|
|
|
// Click outside to close |
|
container.addEventListener("click", (e) => { |
|
if (e.target === container) { |
|
closeFullscreen() |
|
} |
|
}) |
|
|
|
fullscreenContainer = container |
|
return container |
|
} |
|
|
|
function showPhoto (index) { |
|
if (!photos.length) return |
|
|
|
currentIndex = Math.max(0, Math.min(index, photos.length - 1)) |
|
const photo = photos[currentIndex] |
|
|
|
const container = fullscreenContainer || createFullscreenViewer() |
|
const img = container.querySelector(".pb-pe-image-container img") |
|
const caption = container.querySelector(".pb-pe-caption") |
|
const counterText = container.querySelector(".pb-pe-counter-text") |
|
const prevBtn = container.querySelector(".pb-pe-prev-btn") |
|
const nextBtn = container.querySelector(".pb-pe-next-btn") |
|
|
|
img.src = photo.src |
|
img.alt = photo.alt |
|
caption.textContent = photo.caption |
|
counterText.textContent = `${currentIndex + 1} / ${photos.length}` |
|
|
|
prevBtn.disabled = currentIndex === 0 |
|
nextBtn.disabled = currentIndex === photos.length - 1 |
|
|
|
log(`Showing photo ${currentIndex + 1}/${photos.length}`) |
|
} |
|
|
|
function showNext () { |
|
if (currentIndex < photos.length - 1) { |
|
showPhoto(currentIndex + 1) |
|
} |
|
} |
|
|
|
function showPrevious () { |
|
if (currentIndex > 0) { |
|
showPhoto(currentIndex - 1) |
|
} |
|
} |
|
|
|
function openFullscreen (startIndex = 0) { |
|
log("Opening fullscreen") |
|
extractPhotos() |
|
|
|
if (!photos.length) { |
|
alert("No photos found!") |
|
return |
|
} |
|
|
|
const container = createFullscreenViewer() |
|
container.classList.add("pb-pe-active") |
|
showPhoto(startIndex) |
|
|
|
// Prevent body scroll |
|
document.body.style.overflow = "hidden" |
|
} |
|
|
|
function closeFullscreen () { |
|
log("Closing fullscreen") |
|
if (fullscreenContainer) { |
|
fullscreenContainer.classList.remove("pb-pe-active") |
|
} |
|
|
|
// Restore body scroll |
|
document.body.style.overflow = "" |
|
} |
|
|
|
function handleKeydown (e) { |
|
if (!fullscreenContainer || !fullscreenContainer.classList.contains("pb-pe-active")) { |
|
return |
|
} |
|
|
|
switch (e.key) { |
|
case "Escape": |
|
closeFullscreen() |
|
e.preventDefault() |
|
break |
|
case "ArrowLeft": |
|
showPrevious() |
|
e.preventDefault() |
|
break |
|
case "ArrowRight": |
|
showNext() |
|
e.preventDefault() |
|
break |
|
} |
|
} |
|
|
|
function addButton () { |
|
log("Adding fullscreen button") |
|
|
|
const container = document.querySelector("#blog-head .user-actions") |
|
if (!container) { |
|
log("User actions container not found") |
|
return |
|
} |
|
|
|
// Check if button already exists |
|
if (container.querySelector("#pb-fullscreen-btn")) { |
|
log("Button already exists") |
|
return |
|
} |
|
|
|
const button = document.createElement("button") |
|
button.id = "pb-fullscreen-btn" |
|
button.className = "pb-button small nl" |
|
button.textContent = "⛶ Fullscreen" |
|
button.style.marginLeft = "5px" |
|
|
|
button.addEventListener("click", () => { |
|
openFullscreen(0) |
|
}) |
|
|
|
container.appendChild(button) |
|
log("Button added") |
|
} |
|
|
|
// Initialize |
|
function init () { |
|
log("Initializing...") |
|
|
|
// Add keyboard event listener |
|
document.addEventListener("keydown", handleKeydown) |
|
|
|
// Wait for DOM to be ready |
|
if (document.readyState === "loading") { |
|
document.addEventListener("DOMContentLoaded", addButton) |
|
} else { |
|
addButton() |
|
} |
|
|
|
// Also try to add button after a delay in case the container loads later |
|
setTimeout(addButton, 1000) |
|
setTimeout(addButton, 2000) |
|
} |
|
|
|
init() |