media-viewer-pro / index.html
Freefall's picture
added features
cf09a43 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WontView LeeT - Cyber Media Viewer</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
cyber: {
primary: '#00ff9d',
secondary: '#00b8ff',
dark: '#0a0a1a',
darker: '#050510',
accent: '#ff00aa',
glow: 'rgba(0, 255, 157, 0.3)',
matrix: '#00ff41'
}
},
fontFamily: {
'cyber': ['"Courier New"', 'monospace']
},
boxShadow: {
'cyber': '0 0 10px rgba(0, 255, 157, 0.7)',
'cyber-sm': '0 0 5px rgba(0, 255, 157, 0.5)',
'cyber-lg': '0 0 15px rgba(0, 255, 157, 0.9)',
'matrix': '0 0 8px #00ff41'
},
animation: {
'matrix-flicker': 'matrix-flicker 0.5s infinite alternate',
'scanline': 'scanline 8s linear infinite',
'glitch': 'glitch 2s infinite alternate',
'text-flicker': 'text-flicker 3s infinite alternate'
},
keyframes: {
'matrix-flicker': {
'0%': { opacity: '0.1' },
'2%': { opacity: '0.1' },
'4%': { opacity: '0.5' },
'19%': { opacity: '0.5' },
'21%': { opacity: '0.1' },
'23%': { opacity: '1' },
'80%': { opacity: '0.5' },
'100%': { opacity: '0.1' }
},
'scanline': {
'0%': { transform: 'translateY(-100%)' },
'100%': { transform: 'translateY(100%)' }
},
'glitch': {
'0%': { textShadow: '2px 0 #00ff9d, -2px 0 #ff00aa' },
'50%': { textShadow: '4px 0 #00ff9d, -4px 0 #ff00aa' },
'100%': { textShadow: '2px 0 #00ff9d, -2px 0 #ff00aa' }
},
'text-flicker': {
'0%': { opacity: '0.1' },
'2%': { opacity: '1' },
'8%': { opacity: '0.1' },
'9%': { opacity: '1' },
'12%': { opacity: '0.1' },
'20%': { opacity: '1' },
'25%': { opacity: '0.3' },
'30%': { opacity: '1' },
'70%': { opacity: '0.7' },
'72%': { opacity: '0.2' },
'77%': { opacity: '0.9' },
'100%': { opacity: '0.9' }
}
}
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
body {
font-family: 'Share Tech Mono', monospace;
background-color: #050510;
color: #00ff9d;
overflow-x: hidden;
position: relative;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
linear-gradient(rgba(0, 255, 157, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 255, 157, 0.03) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
z-index: -1;
}
.cyber-border {
border: 1px solid #00ff9d;
box-shadow: 0 0 10px rgba(0, 255, 157, 0.3);
}
.cyber-bg {
background: linear-gradient(135deg, #0a0a1a 0%, #050510 100%);
}
.cyber-text {
text-shadow: 0 0 5px rgba(0, 255, 157, 0.7);
}
.cyber-button {
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.cyber-button:hover {
transform: translateY(-2px);
box-shadow: 0 0 15px rgba(0, 255, 157, 0.7);
}
.cyber-button:active {
transform: translateY(0);
}
.cyber-button:after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 255, 157, 0.4), transparent);
transition: all 0.5s ease;
}
.cyber-button:hover:after {
left: 100%;
}
.glow {
animation: glow 2s infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 5px rgba(0, 255, 157, 0.5);
}
to {
box-shadow: 0 0 15px rgba(0, 255, 157, 0.9);
}
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
.media-container {
transition: transform 0.3s ease;
transform-origin: 0 0;
}
.thumbnail {
transition: all 0.2s ease;
position: relative;
}
.thumbnail:hover {
transform: scale(1.05);
box-shadow: 0 0 10px rgba(0, 255, 157, 0.7);
}
.thumbnail.active {
border: 2px solid #00ff9d;
box-shadow: 0 0 15px rgba(0, 255, 157, 0.9);
}
.tooltip {
position: relative;
}
.tooltip:before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #0a0a1a;
color: #00ff9d;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
border: 1px solid #00ff9d;
box-shadow: 0 0 5px rgba(0, 255, 157, 0.5);
}
.tooltip:hover:before {
opacity: 1;
visibility: visible;
bottom: calc(100% + 5px);
}
.dropdown-content {
display: none;
position: absolute;
background-color: #0a0a1a;
min-width: 160px;
box-shadow: 0 0 15px rgba(0, 255, 157, 0.5);
z-index: 1;
border-radius: 4px;
overflow: hidden;
border: 1px solid #00ff9d;
}
.dropdown-content a {
color: #00ff9d;
padding: 8px 12px;
text-decoration: none;
display: block;
font-size: 14px;
transition: all 0.2s ease;
}
.dropdown-content a:hover {
background-color: rgba(0, 255, 157, 0.2);
color: white;
}
.dropdown:hover .dropdown-content {
display: block;
}
.progress-bar-container { /* Added container */
height: 10px; /* Increased height for easier clicking */
padding: 3px 0; /* Vertical padding */
cursor: pointer;
width: 100%;
}
.progress-bar {
height: 4px;
background: #0a0a1a;
border-radius: 2px;
overflow: hidden;
pointer-events: none; /* Prevent clicks on the bar itself */
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00ff9d, #00b8ff);
transition: width 0.1s linear;
}
.volume-slider {
-webkit-appearance: none;
width: 100px;
height: 4px;
background: #0a0a1a;
border-radius: 2px;
outline: none;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #00ff9d;
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 255, 157, 0.7);
}
.volume-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #00ff9d;
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 255, 157, 0.7);
}
.drag-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(5, 5, 16, 0.8);
border: 4px dashed #00ff9d;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.drag-overlay.active {
opacity: 1;
pointer-events: all;
}
.nav-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 100px;
background: rgba(10, 10, 26, 0.7);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
border: 1px solid #00ff9d;
z-index: 10;
}
.nav-arrow:hover {
background: rgba(0, 255, 157, 0.2);
opacity: 1 !important;
}
.media-area:hover .nav-arrow {
opacity: 0.7;
}
.nav-arrow.left {
left: 0;
border-left: none;
border-radius: 0 5px 5px 0;
}
.nav-arrow.right {
right: 0;
border-right: none;
border-radius: 5px 0 0 5px;
}
.settings-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #0a0a1a;
border: 1px solid #00ff9d;
box-shadow: 0 0 20px rgba(0, 255, 157, 0.5);
z-index: 100;
padding: 20px;
border-radius: 5px;
max-width: 500px;
width: 90%;
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
}
.settings-panel.active {
opacity: 1;
pointer-events: all;
}
.settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 99;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.settings-overlay.active {
opacity: 1;
pointer-events: all;
}
.checkbox-container {
display: block;
position: relative;
padding-left: 30px;
margin-bottom: 12px;
cursor: pointer;
user-select: none;
-webkit-user-select: none; /* Safari */
}
/* Metadata Panel Styles (similar to settings) */
.metadata-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #0a0a1a;
border: 1px solid #00ff9d;
box-shadow: 0 0 20px rgba(0, 255, 157, 0.5);
z-index: 101; /* Above settings */
padding: 20px;
border-radius: 5px;
max-width: 80vw; /* Wider */
width: 90%;
max-height: 80vh; /* Limit height */
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
display: flex; /* Use flex for layout */
flex-direction: column;
}
.metadata-panel.active {
opacity: 1;
pointer-events: all;
}
.metadata-content {
flex-grow: 1; /* Allow content to take available space */
overflow-y: auto; /* Enable vertical scroll */
background: #050510;
padding: 10px;
border: 1px solid #00b8ff;
border-radius: 3px;
font-size: 0.8rem;
white-space: pre-wrap; /* Wrap long lines */
word-break: break-all; /* Break long words/strings */
color: #00ff9d; /* Match body text */
}
.metadata-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8); /* Darker overlay */
z-index: 100; /* Below panel, above rest */
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.metadata-overlay.active {
opacity: 1;
pointer-events: all;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 20px;
width: 20px;
background-color: #050510;
border: 1px solid #00ff9d;
border-radius: 3px;
}
.checkbox-container:hover input ~ .checkmark {
background-color: rgba(0, 255, 157, 0.1);
}
.checkbox-container input:checked ~ .checkmark {
background-color: #00ff9d;
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
.checkbox-container .checkmark:after {
left: 7px;
top: 3px;
width: 5px;
height: 10px;
border: solid #050510;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
cursor: grab; /* Add grab cursor */
}
.scrollbar-hide:active {
cursor: grabbing; /* Add grabbing cursor when dragging */
}
/* Cyberpunk scanline effect */
.scanline {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 255, 157, 0.05) 50%,
transparent 100%
);
background-size: 100% 8px;
pointer-events: none;
z-index: 9999;
animation: scanline 8s linear infinite;
}
/* Matrix rain effect */
.matrix-rain {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: -1;
opacity: 0.1;
}
.matrix-column {
position: absolute;
top: 0;
width: 1em;
height: 100%;
color: #00ff41;
font-size: 1.2em;
writing-mode: vertical-rl;
text-orientation: mixed;
text-shadow: 0 0 5px #00ff41;
animation: matrix-flicker 0.5s infinite alternate;
}
/* Glitch effect */
.glitch-effect {
position: relative;
}
.glitch-effect::before, .glitch-effect::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0a0a1a;
}
.glitch-effect::before {
left: 2px;
text-shadow: -2px 0 #ff00aa;
clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim-1 2s infinite linear alternate-reverse;
}
.glitch-effect::after {
left: -2px;
text-shadow: -2px 0 #00b8ff;
clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim-2 2s infinite linear alternate-reverse;
}
@keyframes glitch-anim-1 {
0% { clip: rect(32px, 9999px, 28px, 0); }
10% { clip: rect(13px, 9999px, 37px, 0); }
20% { clip: rect(45px, 9999px, 33px, 0); }
30% { clip: rect(31px, 9999px, 94px, 0); }
40% { clip: rect(58px, 9999px, 34px, 0); }
50% { clip: rect(24px, 9999px, 23px, 0); }
60% { clip: rect(64px, 9999px, 78px, 0); }
70% { clip: rect(67px, 9999px, 62px, 0); }
80% { clip: rect(55px, 9999px, 39px, 0); }
90% { clip: rect(39px, 9999px, 96px, 0); }
100% { clip: rect(82px, 9999px, 40px, 0); }
}
@keyframes glitch-anim-2 {
0% { clip: rect(65px, 9999px, 119px, 0); }
10% { clip: rect(79px, 9999px, 85px, 0); }
20% { clip: rect(74px, 9999px, 14px, 0); }
30% { clip: rect(27px, 9999px, 53px, 0); }
40% { clip: rect(64px, 9999px, 40px, 0); }
50% { clip: rect(61px, 9999px, 73px, 0); }
60% { clip: rect(99px, 9999px, 103px, 0); }
70% { clip: rect(34px, 9999px, 115px, 0); }
80% { clip: rect(98px, 9999px, 54px, 0); }
90% { clip: rect(43px, 9999px, 96px, 0); }
100% { clip: rect(82px, 9999px, 26px, 0); }
}
/* Terminal style cursor */
.terminal-cursor {
display: inline-block;
width: 10px;
height: 20px;
background: #00ff9d;
animation: blink 1s step-end infinite;
vertical-align: middle;
margin-left: 3px;
}
@keyframes blink {
from, to { opacity: 1; }
50% { opacity: 0; }
}
/* Responsive adjustments */
@media (max-width: 768px) {
.media-area {
height: 50vh;
}
.thumbnail {
width: 60px;
height: 60px;
}
.nav-arrow {
width: 30px;
height: 60px;
}
}
</style>
</head>
<body class="bg-cyber-darker text-cyber-primary">
<!-- Cyberpunk effects -->
<div class="scanline"></div>
<div class="matrix-rain" id="matrix-rain"></div>
<!-- Drag and Drop Overlay -->
<div id="drag-overlay" class="drag-overlay">
<div class="text-center p-8 cyber-border rounded-lg bg-cyber-dark">
<i class="fas fa-cloud-upload-alt text-6xl mb-4 text-cyber-accent"></i>
<h2 class="text-2xl mb-2">DROP MEDIA HERE</h2>
<p class="text-cyber-secondary">IMAGES OR VIDEOS</p>
</div>
</div>
<!-- Settings Panel -->
<div id="settings-overlay" class="settings-overlay"></div>
<div id="settings-panel" class="settings-panel">
<div class="flex justify-between items-center mb-4 border-b border-cyber-primary pb-2">
<h3 class="text-xl">SYSTEM SETTINGS</h3>
<button id="close-settings" class="text-cyber-primary hover:text-cyber-accent" title="Close Settings">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<h4 class="text-lg mb-2">MOUSE WHEEL BEHAVIOR</h4>
<label class="checkbox-container">ZOOM WITH MOUSE WHEEL
<input type="radio" name="wheel-behavior" value="zoom" checked>
<span class="checkmark"></span>
</label>
<label class="checkbox-container">NAVIGATE WITH MOUSE WHEEL
<input type="radio" name="wheel-behavior" value="navigate">
<span class="checkmark"></span>
</label>
</div>
<div>
<h4 class="text-lg mb-2">SLIDESHOW OPTIONS</h4>
<label class="checkbox-container">SKIP VIDEOS DURING SLIDESHOW
<input type="checkbox" id="skip-videos" checked>
<span class="checkmark"></span>
</label>
</div>
<div>
<h4 class="text-lg mb-2">APPEARANCE</h4>
<label class="checkbox-container">ENABLE CYBER EFFECTS
<input type="checkbox" id="cyber-effects" checked>
<span class="checkmark"></span>
</label>
<label class="checkbox-container">ENABLE MATRIX RAIN
<input type="checkbox" id="matrix-rain-toggle" checked>
<span class="checkmark"></span>
</label>
</div>
</div>
<div class="mt-6 pt-4 border-t border-cyber-primary">
<button id="save-settings" class="w-full bg-cyber-primary text-cyber-dark py-2 rounded hover:bg-cyber-secondary transition-all">
SAVE SETTINGS
</button>
</div>
</div>
<!-- Metadata Panel -->
<div id="metadata-overlay" class="metadata-overlay"></div>
<div id="metadata-panel" class="metadata-panel">
<div class="flex justify-between items-center mb-4 border-b border-cyber-primary pb-2">
<h3 class="text-xl">FULL METADATA</h3>
<button id="close-metadata" class="text-cyber-primary hover:text-cyber-accent" title="Close Metadata">
<i class="fas fa-times"></i>
</button>
</div>
<pre id="full-metadata-content" class="metadata-content scrollbar-hide"></pre>
<div class="mt-4 pt-2 border-t border-cyber-primary text-right">
<button id="copy-metadata-btn" class="cyber-button bg-cyber-dark px-3 py-1 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="COPY METADATA" title="Copy Metadata">
<i class="fas fa-copy mr-1"></i> COPY
</button>
</div>
</div>
<!-- Main Container -->
<div class="container mx-auto p-4">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<div class="glitch-effect" data-text="WONTV1EW L33T">
<h1 class="text-4xl font-bold cyber-text text-flicker">WONTV1EW L33T</h1>
<p class="text-cyber-secondary text-sm">CYBER MEDIA VIEWER v1.0<span class="terminal-cursor"></span></p>
</div>
<div class="flex space-x-3">
<div class="dropdown">
<button id="open-files" class="cyber-button bg-cyber-dark px-4 py-2 rounded cyber-border hover:shadow-cyber">
<i class="fas fa-folder-open mr-2"></i> OPEN <i class="fas fa-caret-down ml-1"></i>
</button>
<div class="dropdown-content">
<a href="#" id="open-files-btn"><i class="fas fa-file-image mr-2"></i> FILES</a>
<a href="#" id="open-folder-btn"><i class="fas fa-folder mr-2"></i> FOLDER</a>
</div>
</div>
<input type="file" id="file-input" class="hidden" multiple accept="image/*,video/*" aria-label="File Input">
<input type="file" id="folder-input" class="hidden" webkitdirectory directory multiple accept="image/*,video/*" aria-label="Folder Input">
<button id="settings-btn" class="cyber-button bg-cyber-dark px-4 py-2 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="SETTINGS" title="Open Settings">
<i class="fas fa-cog"></i>
</button>
</div>
</div>
<!-- Main Content -->
<div class="cyber-bg cyber-border rounded-xl overflow-hidden">
<!-- Media Display Area -->
<div class="media-area relative h-[75vh] bg-black flex items-center justify-center">
<div id="media-display" class="w-full h-full flex items-center justify-center relative">
<div id="no-media" class="text-cyber-secondary text-center p-8">
<i class="fas fa-images text-6xl mb-4 text-cyber-accent pulse"></i>
<p class="text-xl mb-2">NO MEDIA DETECTED</p>
<p class="text-sm">DRAG & DROP FILES OR CLICK "OPEN"</p>
<p class="text-xs mt-4 text-cyber-secondary">SUPPORTS: JPG, PNG, GIF, MP4, WEBM</p>
</div>
<div id="image-container" class="hidden absolute inset-0 overflow-auto cursor-grab">
<div id="image-wrapper" class="media-container w-full h-full flex items-center justify-center">
<img id="current-image" class="max-w-full max-h-full" src="" alt="">
</div>
</div>
<div id="video-container" class="hidden relative w-full h-full">
<video id="current-video" class="max-w-full max-h-full" controls>
YOUR BROWSER DOES NOT SUPPORT THE VIDEO TAG.
</video>
</div>
</div>
<!-- Navigation Arrows -->
<div id="prev-btn" class="nav-arrow left tooltip" data-tooltip="PREVIOUS (←)">
<i class="fas fa-chevron-left text-2xl"></i>
</div>
<div id="next-btn" class="nav-arrow right tooltip" data-tooltip="NEXT (→)">
<i class="fas fa-chevron-right text-2xl"></i>
</div>
<!-- Zoom Controls -->
<div id="zoom-controls" class="hidden absolute bottom-4 right-4 bg-cyber-dark bg-opacity-90 text-cyber-primary p-2 rounded cyber-border">
<button id="zoom-in" class="p-2 hover:text-cyber-accent tooltip" data-tooltip="ZOOM IN (+)" title="Zoom In">
<i class="fas fa-search-plus"></i>
</button>
<div class="h-px bg-cyber-primary my-1"></div>
<button id="zoom-out" class="p-2 hover:text-cyber-accent tooltip" data-tooltip="ZOOM OUT (-)" title="Zoom Out">
<i class="fas fa-search-minus"></i>
</button>
<div class="h-px bg-cyber-primary my-1"></div>
<button id="copy-image-btn" class="p-2 hover:text-cyber-accent tooltip" data-tooltip="COPY IMAGE (C)" title="Copy Image">
<i class="fas fa-copy"></i>
</button>
<div class="h-px bg-cyber-primary my-1"></div>
<button id="zoom-reset" class="p-2 hover:text-cyber-accent tooltip" data-tooltip="RESET ZOOM (0)" title="Reset Zoom">
<i class="fas fa-expand"></i>
</button>
</div>
<!-- Video Controls -->
<div id="video-controls" class="hidden absolute bottom-4 left-4 right-4 bg-cyber-dark bg-opacity-90 text-cyber-primary p-3 rounded cyber-border flex items-center justify-between">
<button id="play-pause" class="p-2 hover:text-cyber-accent tooltip" data-tooltip="PLAY/PAUSE (SPACE)" title="Play/Pause">
<i class="fas fa-play" id="play-icon"></i>
</button>
<div class="flex-1 mx-4 progress-bar-container"> <!-- Added container -->
<div class="progress-bar">
<div id="progress-fill" class="progress-fill" style="width: 0%"></div>
</div>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-volume-down"></i>
<input type="range" id="volume-slider" class="volume-slider" min="0" max="1" step="0.01" value="1" aria-label="Volume">
<i class="fas fa-volume-up"></i>
</div>
</div>
</div>
<!-- Thumbnail Sidebar -->
<div id="thumbnail-sidebar" class="hidden w-full bg-cyber-dark border-t border-cyber-primary p-2 overflow-x-auto scrollbar-hide">
<div id="thumbnail-container" class="flex space-x-2">
<!-- Thumbnails will be added here -->
</div>
</div>
<!-- Controls -->
<div class="bg-cyber-dark p-4 border-t border-cyber-primary">
<div class="flex flex-wrap items-center justify-between gap-4">
<!-- Playback Controls -->
<div class="flex items-center space-x-2">
<button id="play-pause-bottom" class="cyber-button bg-cyber-dark px-3 py-1 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="PLAY/PAUSE (SPACE)">
<i class="fas fa-play mr-1" id="play-icon-bottom"></i> PLAY
</button>
<button id="stop-bottom" class="cyber-button bg-cyber-dark px-3 py-1 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="STOP">
<i class="fas fa-stop mr-1"></i> STOP
</button>
<button id="prev-bottom" class="cyber-button bg-cyber-dark px-3 py-1 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="PREVIOUS (←)">
<i class="fas fa-step-backward mr-1"></i> PREV
</button>
<button id="next-bottom" class="cyber-button bg-cyber-dark px-3 py-1 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="NEXT (→)">
<i class="fas fa-step-forward mr-1"></i> NEXT
</button>
</div>
<!-- Slideshow Controls -->
<div class="flex items-center space-x-4">
<div class="flex items-center">
<span class="text-sm mr-2">SPEED:</span>
<select id="slideshow-speed" class="bg-cyber-dark border border-cyber-primary rounded px-2 py-1 text-sm text-cyber-primary" aria-label="Slideshow Speed">
<option value="1000">1 SEC</option>
<option value="2000" selected>2 SEC</option>
<option value="3000">3 SEC</option>
<option value="5000">5 SEC</option>
<option value="10000">10 SEC</option>
</select>
</div>
<button id="toggle-slideshow" class="cyber-button bg-cyber-dark px-3 py-1 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="START SLIDESHOW">
<i class="fas fa-images mr-1"></i> SLIDESHOW
</button>
</div>
<!-- Display Options -->
<div class="flex items-center space-x-4">
<div class="flex items-center">
<span class="text-sm mr-2">SIZE:</span>
<select id="image-size" class="bg-cyber-dark border border-cyber-primary rounded px-2 py-1 text-sm text-cyber-primary" aria-label="Image Size">
<option value="contain">FIT</option>
<option value="cover">FILL</option>
<option value="original">ORIGINAL</option>
</select>
</div>
<button id="toggle-sidebar" class="cyber-button bg-cyber-dark px-3 py-1 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="TOGGLE THUMBNAILS (T)">
<i class="fas fa-th mr-1"></i> THUMBNAILS
</button>
<button id="toggle-fullscreen" class="cyber-button bg-cyber-dark px-3 py-1 rounded cyber-border hover:shadow-cyber tooltip" data-tooltip="FULLSCREEN (F)">
<i class="fas fa-expand mr-1"></i><span class="button-text"> FULLSCREEN</span>
</button>
</div>
</div>
<!-- Status Bar -->
<div class="mt-4 text-xs space-y-1">
<div class="flex justify-between items-center">
<div>
<span id="current-index" class="text-cyber-secondary mr-2">0/0</span>
<span id="media-name" class="text-cyber-primary"></span>
</div>
<div id="status-message" class="text-cyber-accent pulse"></div>
</div>
<div class="flex justify-between items-center">
<span id="media-dimensions" class="text-cyber-secondary mr-4"></span>
<div class="flex items-center min-w-0"> <!-- Flex container for metadata and button -->
<span id="media-metadata" class="text-cyber-secondary truncate mr-2" title="Metadata"></span>
<button id="view-metadata-btn" class="hidden text-cyber-accent hover:text-cyber-primary text-xs tooltip" data-tooltip="VIEW FULL METADATA" title="View Full Metadata">
<i class="fas fa-info-circle"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const fileInput = document.getElementById('file-input');
const folderInput = document.getElementById('folder-input');
const openFilesBtn = document.getElementById('open-files-btn');
const openFolderBtn = document.getElementById('open-folder-btn');
const mediaDisplay = document.getElementById('media-display');
const noMedia = document.getElementById('no-media');
const imageContainer = document.getElementById('image-container');
const videoContainer = document.getElementById('video-container');
const currentImage = document.getElementById('current-image');
const currentVideo = document.getElementById('current-video');
const thumbnailContainer = document.getElementById('thumbnail-container');
const thumbnailSidebar = document.getElementById('thumbnail-sidebar');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const prevBottomBtn = document.getElementById('prev-bottom');
const nextBottomBtn = document.getElementById('next-bottom');
const zoomInBtn = document.getElementById('zoom-in');
const zoomOutBtn = document.getElementById('zoom-out');
const zoomResetBtn = document.getElementById('zoom-reset');
const copyImageBtn = document.getElementById('copy-image-btn');
const zoomControls = document.getElementById('zoom-controls');
const playPauseBtn = document.getElementById('play-pause');
const playPauseBottomBtn = document.getElementById('play-pause-bottom');
const stopBottomBtn = document.getElementById('stop-bottom');
const playIcon = document.getElementById('play-icon');
const playIconBottom = document.getElementById('play-icon-bottom');
const toggleSlideshowBtn = document.getElementById('toggle-slideshow');
const toggleSidebarBtn = document.getElementById('toggle-sidebar');
const toggleFullscreenBtn = document.getElementById('toggle-fullscreen');
const currentIndexDisplay = document.getElementById('current-index');
const mediaNameDisplay = document.getElementById('media-name');
const mediaDimensionsDisplay = document.getElementById('media-dimensions');
const mediaMetadataDisplay = document.getElementById('media-metadata');
const viewMetadataBtn = document.getElementById('view-metadata-btn');
const metadataOverlay = document.getElementById('metadata-overlay');
const metadataPanel = document.getElementById('metadata-panel');
const fullMetadataContent = document.getElementById('full-metadata-content');
const closeMetadataBtn = document.getElementById('close-metadata');
const copyMetadataBtn = document.getElementById('copy-metadata-btn');
const progressFill = document.getElementById('progress-fill');
const progressBarContainer = document.querySelector('.progress-bar-container');
const videoControls = document.getElementById('video-controls');
const volumeSlider = document.getElementById('volume-slider');
const imageSizeSelect = document.getElementById('image-size');
const slideshowSpeedSelect = document.getElementById('slideshow-speed');
const dragOverlay = document.getElementById('drag-overlay');
const settingsBtn = document.getElementById('settings-btn');
const settingsPanel = document.getElementById('settings-panel');
const settingsOverlay = document.getElementById('settings-overlay');
const closeSettingsBtn = document.getElementById('close-settings');
const saveSettingsBtn = document.getElementById('save-settings');
const statusMessage = document.getElementById('status-message');
const matrixRainToggle = document.getElementById('matrix-rain-toggle');
const matrixRainContainer = document.getElementById('matrix-rain');
// State variables
let mediaFiles = [];
let currentIndex = 0;
let isSlideshowRunning = false;
let slideshowInterval;
let zoomLevel = 1;
let isDragging = false;
let startX, startY, translateX = 0, translateY = 0;
let wheelBehavior = 'zoom'; // 'zoom' or 'navigate'
let skipVideosInSlideshow = true;
let cyberEffectsEnabled = true;
let matrixRainEnabled = true;
let isScrubbingVideo = false; // Added for video scrubbing
let isDraggingThumbnails = false; // Added for thumbnail dragging
let thumbnailDragStartX = 0; // Added for thumbnail dragging
let thumbnailScrollLeftStart = 0; // Added for thumbnail dragging
let fullMetadataText = ''; // Added to store full metadata
// Initialize settings from localStorage
loadSettings();
// Initialize matrix rain
initMatrixRain();
// Event Listeners for file/folder opening
openFilesBtn.addEventListener('click', () => fileInput.click());
openFolderBtn.addEventListener('click', () => folderInput.click());
fileInput.addEventListener('change', handleFileSelection);
folderInput.addEventListener('change', handleFileSelection);
// Drag and drop functionality
document.addEventListener('dragover', (e) => {
e.preventDefault();
dragOverlay.classList.add('active');
});
document.addEventListener('dragleave', () => {
dragOverlay.classList.remove('active');
});
document.addEventListener('drop', (e) => {
e.preventDefault();
dragOverlay.classList.remove('active');
if (e.dataTransfer.items) {
const files = [];
const items = e.dataTransfer.items;
// Check if it's a directory (Chrome only)
const entry = items[0].webkitGetAsEntry();
if (entry && entry.isDirectory) {
statusMessage.textContent = "PLEASE USE THE FOLDER OPEN OPTION FOR DIRECTORIES";
setTimeout(() => statusMessage.textContent = "", 3000);
return;
}
// Handle files
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
const file = items[i].getAsFile();
if (file.type.startsWith('image/') || file.type.startsWith('video/')) {
files.push(file);
}
}
}
if (files.length > 0) {
mediaFiles = files;
currentIndex = 0;
displayCurrentMedia();
updateThumbnails();
showStatusMessage(`LOADED ${files.length} FILES`);
}
}
});
// Navigation controls
prevBtn.addEventListener('click', goToPrevious);
nextBtn.addEventListener('click', goToNext);
prevBottomBtn.addEventListener('click', goToPrevious);
nextBottomBtn.addEventListener('click', goToNext);
// Zoom controls
zoomInBtn.addEventListener('click', () => zoomImage(1.2));
zoomOutBtn.addEventListener('click', () => zoomImage(0.8));
zoomResetBtn.addEventListener('click', resetZoom);
copyImageBtn.addEventListener('click', copyImageToClipboard);
// Playback controls
playPauseBtn.addEventListener('click', togglePlayPause);
playPauseBottomBtn.addEventListener('click', togglePlayPause);
stopBottomBtn.addEventListener('click', stopPlayback);
// Video controls
currentVideo.addEventListener('timeupdate', updateVideoProgress);
currentVideo.addEventListener('play', () => {
playIcon.className = 'fas fa-pause';
playIconBottom.className = 'fas fa-pause';
});
currentVideo.addEventListener('pause', () => {
playIcon.className = 'fas fa-play';
playIconBottom.className = 'fas fa-play';
});
currentVideo.addEventListener('ended', goToNext);
volumeSlider.addEventListener('input', () => { // Keep this for slider input
currentVideo.volume = volumeSlider.value;
});
// Video Scrubbing Listeners
progressBarContainer.addEventListener('mousedown', startVideoScrub);
document.addEventListener('mousemove', videoScrub);
document.addEventListener('mouseup', stopVideoScrub);
document.addEventListener('mouseleave', stopVideoScrub); // Stop if mouse leaves window
// Thumbnail Drag Scrolling Listeners
thumbnailSidebar.addEventListener('mousedown', startThumbnailDrag);
document.addEventListener('mousemove', thumbnailDrag); // Listen on document for wider drag area
document.addEventListener('mouseup', stopThumbnailDrag);
document.addEventListener('mouseleave', stopThumbnailDrag); // Stop if mouse leaves window
// Slideshow controls
toggleSlideshowBtn.addEventListener('click', toggleSlideshow);
// Display options
toggleSidebarBtn.addEventListener('click', toggleThumbnailSidebar);
imageSizeSelect.addEventListener('change', () => {
if (mediaFiles.length > 0) {
applyImageSize();
}
});
// Fullscreen controls
toggleFullscreenBtn.addEventListener('click', toggleFullScreen);
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange); // Safari
document.addEventListener('mozfullscreenchange', handleFullscreenChange); // Firefox
document.addEventListener('MSFullscreenChange', handleFullscreenChange); // IE/Edge
// Mouse wheel behavior
mediaDisplay.addEventListener('wheel', handleWheelEvent);
// Mouse drag for panning zoomed images
imageContainer.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', dragImage);
document.addEventListener('mouseup', endDrag);
document.addEventListener('mouseleave', endDrag);
// Right mouse drag for panning
imageContainer.addEventListener('contextmenu', (e) => e.preventDefault());
imageContainer.addEventListener('mousedown', (e) => {
if (e.button === 2 && zoomLevel > 1) { // Right click
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
imageContainer.style.cursor = 'grabbing';
}
});
// Keyboard shortcuts
document.addEventListener('keydown', handleKeyboardShortcuts);
// Settings
settingsBtn.addEventListener('click', () => {
settingsPanel.classList.add('active');
settingsOverlay.classList.add('active');
});
closeSettingsBtn.addEventListener('click', closeSettings);
settingsOverlay.addEventListener('click', closeSettings);
saveSettingsBtn.addEventListener('click', saveSettings);
// Matrix rain toggle
matrixRainToggle.addEventListener('change', (e) => {
matrixRainEnabled = e.target.checked;
toggleMatrixRain(matrixRainEnabled);
});
// Metadata Panel Listeners
viewMetadataBtn.addEventListener('click', showMetadataPanel);
closeMetadataBtn.addEventListener('click', hideMetadataPanel);
metadataOverlay.addEventListener('click', hideMetadataPanel);
copyMetadataBtn.addEventListener('click', copyFullMetadata);
// Functions
function handleFileSelection(e) {
const files = Array.from(e.target.files);
if (files.length > 0) {
mediaFiles = files.filter(file =>
file.type.startsWith('image/') || file.type.startsWith('video/')
);
currentIndex = 0;
displayCurrentMedia();
updateThumbnails();
showStatusMessage(`LOADED ${mediaFiles.length} FILES`);
}
e.target.value = ''; // Reset input to allow selecting same files again
}
function displayCurrentMedia() {
if (mediaFiles.length === 0) {
noMedia.classList.remove('hidden');
imageContainer.classList.add('hidden');
videoContainer.classList.add('hidden');
zoomControls.classList.add('hidden');
videoControls.classList.add('hidden');
currentIndexDisplay.textContent = '0/0';
mediaNameDisplay.textContent = '';
mediaDimensionsDisplay.textContent = ''; // Clear dimensions
mediaMetadataDisplay.textContent = ''; // Clear metadata
viewMetadataBtn.classList.add('hidden'); // Hide view button
return;
}
noMedia.classList.add('hidden');
const file = mediaFiles[currentIndex];
if (file.type.startsWith('image/')) {
// Display image
const reader = new FileReader();
reader.onload = function(e) {
currentImage.src = e.target.result;
imageContainer.classList.remove('hidden');
videoContainer.classList.add('hidden');
zoomControls.classList.remove('hidden');
videoControls.classList.add('hidden');
resetZoom(); // Reset zoom/pan first
applyImageSize(); // Apply selected size mode
// Get dimensions after image loads
currentImage.onload = () => {
mediaDimensionsDisplay.textContent = `Dimensions: ${currentImage.naturalWidth} x ${currentImage.naturalHeight}`;
// Attempt to extract metadata for PNGs
if (file.type === 'image/png') {
extractAndDisplayPngMetadata(file);
} else {
mediaMetadataDisplay.textContent = '';
viewMetadataBtn.classList.add('hidden'); // Hide view button
}
};
// Clear dimensions/metadata if the image fails to load
currentImage.onerror = () => {
mediaDimensionsDisplay.textContent = 'Dimensions: Error';
mediaMetadataDisplay.textContent = '';
viewMetadataBtn.classList.add('hidden'); // Hide view button
}
};
reader.readAsDataURL(file); // Read the file to trigger onload/onerror
} else if (file.type.startsWith('video/')) {
// Display video
const reader = new FileReader();
reader.onload = function(e) {
currentVideo.src = e.target.result;
imageContainer.classList.add('hidden');
videoContainer.classList.remove('hidden');
zoomControls.classList.add('hidden');
videoControls.classList.remove('hidden');
currentVideo.volume = volumeSlider.value;
playIcon.className = 'fas fa-play';
playIconBottom.className = 'fas fa-play';
mediaDimensionsDisplay.textContent = '';
mediaMetadataDisplay.textContent = '';
viewMetadataBtn.classList.add('hidden'); // Hide view button
};
reader.readAsDataURL(file);
}
// Update current index display
currentIndexDisplay.textContent = `${currentIndex + 1}/${mediaFiles.length}`;
mediaNameDisplay.textContent = file.name;
// Highlight active thumbnail
const thumbnails = document.querySelectorAll('.thumbnail');
thumbnails.forEach((thumb, index) => {
if (index === currentIndex) {
thumb.classList.add('active');
// Scroll thumbnail into view
thumb.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
} else {
thumb.classList.remove('active');
}
});
}
function updateThumbnails() {
thumbnailContainer.innerHTML = '';
if (mediaFiles.length === 0) {
thumbnailSidebar.classList.add('hidden');
return;
}
thumbnailSidebar.classList.remove('hidden');
mediaFiles.forEach((file, index) => {
const thumbnail = document.createElement('div');
thumbnail.className = `thumbnail flex-shrink-0 w-20 h-20 bg-cyber-darker rounded overflow-hidden cursor-pointer ${index === currentIndex ? 'active' : ''}`;
thumbnail.dataset.index = index;
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
thumbnail.innerHTML = `<img src="${e.target.result}" class="w-full h-full object-cover" alt="${file.name}">`;
};
reader.readAsDataURL(file);
} else if (file.type.startsWith('video/')) {
thumbnail.innerHTML = `
<div class="relative w-full h-full bg-cyber-dark flex items-center justify-center">
<i class="fas fa-play text-cyber-accent text-xl"></i>
<div class="absolute bottom-0 left-0 right-0 bg-cyber-primary bg-opacity-70 text-cyber-dark text-xs p-1 truncate">${file.name}</div>
</div>
`;
}
thumbnail.addEventListener('click', () => {
currentIndex = index;
displayCurrentMedia();
});
thumbnailContainer.appendChild(thumbnail);
});
}
function goToPrevious() {
if (mediaFiles.length === 0) return;
let prevIndex = currentIndex;
let attempts = 0;
const maxAttempts = mediaFiles.length;
do {
prevIndex = (prevIndex - 1 + mediaFiles.length) % mediaFiles.length;
attempts++;
// Stop if we've looped through all files or found an image (or if not skipping videos)
if (attempts >= maxAttempts || !isSlideshowRunning || !skipVideosInSlideshow || mediaFiles[prevIndex].type.startsWith('image/')) {
break;
}
} while (true);
// Only update if we found a valid index (prevents infinite loop if only videos exist and skipping is on)
if (attempts < maxAttempts || !mediaFiles[prevIndex].type.startsWith('video/')) {
currentIndex = prevIndex;
displayCurrentMedia();
} else if (isSlideshowRunning) {
// If only videos exist and we're skipping, stop the slideshow
stopSlideshow();
showStatusMessage("Slideshow stopped: No images found to display.");
}
}
function goToNext() {
if (mediaFiles.length === 0) return;
let nextIndex = currentIndex;
let attempts = 0;
const maxAttempts = mediaFiles.length;
do {
nextIndex = (nextIndex + 1) % mediaFiles.length;
attempts++;
// Stop if we've looped through all files or found an image (or if not skipping videos)
if (attempts >= maxAttempts || !isSlideshowRunning || !skipVideosInSlideshow || mediaFiles[nextIndex].type.startsWith('image/')) {
break;
}
} while (true);
// Only update if we found a valid index (prevents infinite loop if only videos exist and skipping is on)
if (attempts < maxAttempts || !mediaFiles[nextIndex].type.startsWith('video/')) {
currentIndex = nextIndex;
displayCurrentMedia();
} else if (isSlideshowRunning) {
// If only videos exist and we're skipping, stop the slideshow
stopSlideshow();
showStatusMessage("Slideshow stopped: No images found to display.");
}
}
function zoomImage(factor) {
if (!mediaFiles[currentIndex]?.type.startsWith('image/')) return;
zoomLevel *= factor;
const imageWrapper = document.getElementById('image-wrapper');
imageWrapper.style.transform = `scale(${zoomLevel}) translate(${translateX}px, ${translateY}px)`;
}
function resetZoom() {
if (!mediaFiles[currentIndex]?.type.startsWith('image/')) return;
zoomLevel = 1;
translateX = 0;
translateY = 0;
const imageWrapper = document.getElementById('image-wrapper');
imageWrapper.style.transform = `scale(${zoomLevel}) translate(${translateX}px, ${translateY}px)`;
}
function applyImageSize() {
if (!mediaFiles[currentIndex]?.type.startsWith('image/')) return;
const size = imageSizeSelect.value;
const img = document.getElementById('current-image');
switch (size) {
case 'contain':
img.style.objectFit = 'contain';
img.style.width = 'auto';
img.style.height = 'auto';
img.style.maxWidth = '100%';
img.style.maxHeight = '100%';
break;
case 'cover':
img.style.objectFit = 'cover';
img.style.width = '100%';
img.style.height = '100%';
break;
case 'original':
img.style.objectFit = 'none';
img.style.width = 'auto';
img.style.height = 'auto';
img.style.maxWidth = 'none';
img.style.maxHeight = 'none';
break;
}
resetZoom();
}
function togglePlayPause() {
const currentFile = mediaFiles[currentIndex];
// If it's an image, toggle slideshow
if (currentFile?.type.startsWith('image/')) {
if (!isSlideshowRunning) {
startSlideshow(); // Explicitly start
} else {
stopSlideshow();
}
return;
}
// If it's a video, toggle play/pause
if (currentFile?.type.startsWith('video/')) {
// Check if video source is actually loaded before trying to play
if (currentVideo.readyState >= 1) { // HAVE_METADATA or higher
if (currentVideo.paused) {
currentVideo.play().catch(e => console.error("Video play error:", e)); // Add catch for potential errors
playIcon.className = 'fas fa-pause';
playIconBottom.className = 'fas fa-pause';
} else {
currentVideo.pause();
playIcon.className = 'fas fa-play';
playIconBottom.className = 'fas fa-play';
}
} else {
console.warn("Video not ready to play.");
// Optionally show a status message to the user
}
}
}
function stopPlayback() {
if (isSlideshowRunning) {
stopSlideshow();
}
if (mediaFiles[currentIndex]?.type.startsWith('video/')) {
currentVideo.pause();
currentVideo.currentTime = 0;
playIcon.className = 'fas fa-play';
playIconBottom.className = 'fas fa-play';
}
}
function updateVideoProgress() {
// Only update if not currently scrubbing
if (!isScrubbingVideo && currentVideo.duration) {
const percent = (currentVideo.currentTime / currentVideo.duration) * 100;
progressFill.style.width = `${percent}%`;
}
}
function toggleSlideshow() {
if (isSlideshowRunning) {
stopSlideshow();
} else {
startSlideshow();
}
}
function startSlideshow() {
if (mediaFiles.length === 0) return;
isSlideshowRunning = true;
toggleSlideshowBtn.textContent = 'STOP SLIDESHOW';
toggleSlideshowBtn.classList.remove('bg-cyber-dark');
toggleSlideshowBtn.classList.add('bg-cyber-accent', 'text-cyber-dark');
const speed = parseInt(slideshowSpeedSelect.value);
slideshowInterval = setInterval(goToNext, speed);
showStatusMessage('SLIDESHOW STARTED');
}
function stopSlideshow() {
isSlideshowRunning = false;
clearInterval(slideshowInterval);
toggleSlideshowBtn.textContent = 'SLIDESHOW';
toggleSlideshowBtn.classList.remove('bg-cyber-accent', 'text-cyber-dark');
toggleSlideshowBtn.classList.add('bg-cyber-dark');
showStatusMessage('SLIDESHOW STOPPED');
}
function toggleThumbnailSidebar() {
thumbnailSidebar.classList.toggle('hidden');
}
function handleWheelEvent(e) {
e.preventDefault();
if (wheelBehavior === 'zoom' && mediaFiles[currentIndex]?.type.startsWith('image/')) {
// Zoom with wheel
const delta = -e.deltaY;
const factor = delta > 0 ? 1.1 : 0.9;
zoomImage(factor);
} else if (wheelBehavior === 'navigate') {
// Navigate with wheel
if (e.deltaY > 0) {
goToNext();
} else {
goToPrevious();
}
}
}
function startDrag(e) {
if (zoomLevel <= 1 || !mediaFiles[currentIndex]?.type.startsWith('image/')) return;
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
imageContainer.style.cursor = 'grabbing';
}
function dragImage(e) {
if (!isDragging) return;
e.preventDefault();
translateX = e.clientX - startX;
translateY = e.clientY - startY;
const imageWrapper = document.getElementById('image-wrapper');
imageWrapper.style.transform = `scale(${zoomLevel}) translate(${translateX}px, ${translateY}px)`;
}
function endDrag() {
isDragging = false;
imageContainer.style.cursor = 'grab';
}
function handleKeyboardShortcuts(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return;
switch (e.key) {
case 'ArrowLeft':
goToPrevious();
break;
case 'ArrowRight':
goToNext();
break;
case '+':
case '=':
zoomImage(1.2);
break;
case '-':
case '_':
zoomImage(0.8);
break;
case '0':
resetZoom();
break;
case ' ':
togglePlayPause();
break;
case 'ArrowUp':
if (mediaFiles[currentIndex]?.type.startsWith('video/')) {
currentVideo.volume = Math.min(1, currentVideo.volume + 0.1);
volumeSlider.value = currentVideo.volume;
}
break;
case 'ArrowDown':
if (mediaFiles[currentIndex]?.type.startsWith('video/')) {
currentVideo.volume = Math.max(0, currentVideo.volume - 0.1);
volumeSlider.value = currentVideo.volume;
}
break; // Fixed missing break
case 't':
case 'T':
toggleThumbnailSidebar();
break;
case 'c':
case 'C':
copyImageToClipboard();
break;
case 'f':
case 'F':
toggleFullScreen();
break;
case 's':
case 'S':
toggleSlideshow();
break;
case 'Escape':
if (metadataPanel.classList.contains('active')) {
hideMetadataPanel();
} else {
closeSettings();
}
break;
}
}
function showStatusMessage(message) {
statusMessage.textContent = message;
setTimeout(() => {
statusMessage.textContent = "";
}, 3000);
}
function closeSettings() {
settingsPanel.classList.remove('active');
settingsOverlay.classList.remove('active');
}
function saveSettings() {
wheelBehavior = document.querySelector('input[name="wheel-behavior"]:checked').value;
skipVideosInSlideshow = document.getElementById('skip-videos').checked;
cyberEffectsEnabled = document.getElementById('cyber-effects').checked;
matrixRainEnabled = document.getElementById('matrix-rain-toggle').checked;
localStorage.setItem('wheelBehavior', wheelBehavior);
localStorage.setItem('skipVideosInSlideshow', skipVideosInSlideshow);
localStorage.setItem('cyberEffectsEnabled', cyberEffectsEnabled);
localStorage.setItem('matrixRainEnabled', matrixRainEnabled);
applyCyberEffects();
toggleMatrixRain(matrixRainEnabled);
closeSettings();
showStatusMessage('SETTINGS SAVED');
}
function loadSettings() {
const savedWheelBehavior = localStorage.getItem('wheelBehavior');
const savedSkipVideos = localStorage.getItem('skipVideosInSlideshow');
const savedCyberEffects = localStorage.getItem('cyberEffectsEnabled');
const savedMatrixRain = localStorage.getItem('matrixRainEnabled');
if (savedWheelBehavior) {
wheelBehavior = savedWheelBehavior;
document.querySelector(`input[name="wheel-behavior"][value="${savedWheelBehavior}"]`).checked = true;
}
if (savedSkipVideos !== null) {
skipVideosInSlideshow = savedSkipVideos === 'true';
document.getElementById('skip-videos').checked = skipVideosInSlideshow;
}
if (savedCyberEffects !== null) {
cyberEffectsEnabled = savedCyberEffects === 'true';
document.getElementById('cyber-effects').checked = cyberEffectsEnabled;
}
if (savedMatrixRain !== null) {
matrixRainEnabled = savedMatrixRain === 'true';
document.getElementById('matrix-rain-toggle').checked = matrixRainEnabled;
toggleMatrixRain(matrixRainEnabled);
}
applyCyberEffects();
}
function applyCyberEffects() {
if (cyberEffectsEnabled) {
document.body.classList.add('glow');
document.querySelectorAll('.cyber-text').forEach(el => el.classList.add('cyber-text'));
} else {
document.body.classList.remove('glow');
document.querySelectorAll('.cyber-text').forEach(el => el.classList.remove('cyber-text'));
}
}
function initMatrixRain() {
const chars = "01アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン";
const columns = Math.floor(window.innerWidth / 20);
for (let i = 0; i < columns; i++) {
const column = document.createElement('div');
column.className = 'matrix-column';
column.style.left = `${i * 20}px`;
column.style.animationDelay = `${Math.random() * 2}s`;
// Create random characters
let content = '';
const rows = Math.floor(window.innerHeight / 24) + 1;
for (let j = 0; j < rows; j++) {
content += chars[Math.floor(Math.random() * chars.length)];
}
column.textContent = content;
matrixRainContainer.appendChild(column);
}
toggleMatrixRain(matrixRainEnabled);
}
function toggleMatrixRain(enabled) {
if (enabled) {
matrixRainContainer.style.display = 'block';
} else {
matrixRainContainer.style.display = 'none';
}
}
function toggleFullScreen() {
if (!document.fullscreenElement && // Standard syntax
!document.mozFullScreenElement && // Firefox
!document.webkitFullscreenElement && // Chrome, Safari and Opera
!document.msFullscreenElement) { // IE/Edge
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) { /* Firefox */
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (document.documentElement.msRequestFullscreen) { /* IE/Edge */
document.documentElement.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) { /* Firefox */
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { /* IE/Edge */
document.msExitFullscreen();
}
}
}
function handleFullscreenChange() {
const button = toggleFullscreenBtn; // Use the variable directly
const icon = button.querySelector('i');
const textSpan = button.querySelector('span.button-text'); // Target the span
if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) {
icon.className = 'fas fa-compress mr-1';
if (textSpan) textSpan.textContent = ' EXIT FS'; // Update span text
button.setAttribute('data-tooltip', 'EXIT FULLSCREEN (F)');
} else {
icon.className = 'fas fa-expand mr-1';
if (textSpan) textSpan.textContent = ' FULLSCREEN'; // Update span text
button.setAttribute('data-tooltip', 'FULLSCREEN (F)');
}
}
// --- New Functions ---
async function copyImageToClipboard() {
if (!mediaFiles[currentIndex]?.type.startsWith('image/')) {
showStatusMessage('COPY FAILED: NO IMAGE LOADED');
return;
}
try {
const response = await fetch(currentImage.src);
const blob = await response.blob();
// Ensure blob type is correct, default to png if needed
let imageType = blob.type;
if (!imageType.startsWith('image/')) {
console.warn('Blob type unknown, assuming image/png for clipboard');
imageType = 'image/png';
}
await navigator.clipboard.write([
new ClipboardItem({
[imageType]: blob
})
]);
showStatusMessage('IMAGE COPIED TO CLIPBOARD');
} catch (err) {
console.error('Failed to copy image:', err);
showStatusMessage('COPY FAILED: SEE CONSOLE');
}
}
// --- Video Scrubbing Functions ---
function startVideoScrub(e) {
if (!mediaFiles[currentIndex]?.type.startsWith('video/')) return;
isScrubbingVideo = true;
updateVideoTimeFromScrub(e);
}
function videoScrub(e) {
if (!isScrubbingVideo) return;
updateVideoTimeFromScrub(e);
}
function stopVideoScrub() {
if (isScrubbingVideo) {
isScrubbingVideo = false;
// Optional: Resume playback if it was playing before scrub
// if (!currentVideo.paused) currentVideo.play();
}
}
function updateVideoTimeFromScrub(e) {
const rect = progressBarContainer.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const width = rect.width;
let percentage = offsetX / width;
percentage = Math.max(0, Math.min(1, percentage)); // Clamp between 0 and 1
if (currentVideo.duration) {
currentVideo.currentTime = percentage * currentVideo.duration;
// Manually update progress fill during scrub
progressFill.style.width = `${percentage * 100}%`;
}
}
// --- Thumbnail Drag Scrolling Functions ---
function startThumbnailDrag(e) {
isDraggingThumbnails = true;
thumbnailDragStartX = e.pageX - thumbnailSidebar.offsetLeft;
thumbnailScrollLeftStart = thumbnailSidebar.scrollLeft;
thumbnailSidebar.style.cursor = 'grabbing'; // Change cursor
thumbnailSidebar.style.userSelect = 'none'; // Prevent text selection
}
function thumbnailDrag(e) {
if (!isDraggingThumbnails) return;
e.preventDefault(); // Prevent default drag behavior
const x = e.pageX - thumbnailSidebar.offsetLeft;
const walk = (x - thumbnailDragStartX) * 1.5; // Multiplier for faster scroll
thumbnailSidebar.scrollLeft = thumbnailScrollLeftStart - walk;
}
function stopThumbnailDrag() {
if (isDraggingThumbnails) {
isDraggingThumbnails = false;
thumbnailSidebar.style.cursor = 'grab'; // Reset cursor
thumbnailSidebar.style.removeProperty('user-select');
}
}
// --- Metadata Panel Functions ---
function showMetadataPanel() {
fullMetadataContent.textContent = fullMetadataText; // Populate with stored full text
metadataPanel.classList.add('active');
metadataOverlay.classList.add('active');
}
function hideMetadataPanel() {
metadataPanel.classList.remove('active');
metadataOverlay.classList.remove('active');
}
function copyFullMetadata() {
navigator.clipboard.writeText(fullMetadataText)
.then(() => showStatusMessage('METADATA COPIED'))
.catch(err => {
console.error('Failed to copy metadata:', err);
showStatusMessage('METADATA COPY FAILED');
});
}
// --- Update Metadata Display Logic ---
function updateMetadataDisplay(metadata) {
fullMetadataText = Object.entries(metadata).map(([k, v]) => `${k}: ${v}`).join('\n'); // Store full text
let displayText = metadata['parameters'] || metadata['Comment'] || metadata['Description'] || Object.entries(metadata).slice(0, 2).map(([k, v]) => `${k}: ${v.substring(0, 50)}...`).join('; ');
let isTruncated = false;
if (displayText.length > 100) {
displayText = displayText.substring(0, 97) + '...';
isTruncated = true;
}
mediaMetadataDisplay.textContent = `Metadata: ${displayText}`;
mediaMetadataDisplay.title = fullMetadataText; // Full metadata in tooltip
if (isTruncated || Object.keys(metadata).length > 0) { // Show button if truncated or if any metadata exists
viewMetadataBtn.classList.remove('hidden');
} else {
viewMetadataBtn.classList.add('hidden');
}
}
// Modify extractAndDisplayPngMetadata to use the new update function
async function extractAndDisplayPngMetadata(file) {
mediaMetadataDisplay.textContent = 'Metadata: Reading...';
mediaMetadataDisplay.title = '';
viewMetadataBtn.classList.add('hidden'); // Hide button initially
fullMetadataText = ''; // Clear stored text
try {
const buffer = await file.arrayBuffer();
const dataView = new DataView(buffer);
if (dataView.getUint32(0) !== 0x89504E47 || dataView.getUint32(4) !== 0x0D0A1A0A) {
mediaMetadataDisplay.textContent = 'Metadata: Not a valid PNG';
return;
}
let offset = 8;
let metadata = {};
let foundMetadata = false;
while (offset < buffer.byteLength) {
const length = dataView.getUint32(offset);
const typeCode = dataView.getUint32(offset + 4);
const type = String.fromCharCode(
(typeCode >> 24) & 0xFF, (typeCode >> 16) & 0xFF, (typeCode >> 8) & 0xFF, typeCode & 0xFF
);
if (type === 'tEXt') {
const chunkDataOffset = offset + 8;
const chunkData = new Uint8Array(buffer, chunkDataOffset, length);
const separatorIndex = chunkData.findIndex(byte => byte === 0);
if (separatorIndex !== -1) {
const decoder = new TextDecoder("iso-8859-1");
const keyword = decoder.decode(chunkData.slice(0, separatorIndex));
const text = decoder.decode(chunkData.slice(separatorIndex + 1));
metadata[keyword] = text;
foundMetadata = true;
}
}
// TODO: Add iTXt/zTXt parsing if needed
if (type === 'IEND') break;
offset += 12 + length;
}
if (foundMetadata) {
updateMetadataDisplay(metadata); // Use the new function to display/store
} else {
mediaMetadataDisplay.textContent = 'Metadata: None found';
}
} catch (error) {
console.error("Error reading PNG metadata:", error);
mediaMetadataDisplay.textContent = 'Metadata: Error reading';
}
}
}); // End of DOMContentLoaded listener
</script>
</body>
</html>