Spaces:
Running
Running
<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 ; | |
} | |
.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> |