inital commit

This commit is contained in:
2025-12-22 19:16:14 +01:00
parent d1ecd61f74
commit 0a9e63443d
3 changed files with 1000 additions and 2 deletions

86
.gitignore vendored Normal file
View File

@ -0,0 +1,86 @@
# Media Slideshow - .gitignore
# Exclude all media files to avoid committing large binary files to the repository
# Image files
*.jpg
*.jpeg
*.JPG
*.JPEG
*.png
*.PNG
*.gif
*.GIF
*.webp
*.WEBP
*.avif
*.AVIF
*.heic
*.HEIC
*.heif
*.HEIF
*.bmp
*.BMP
*.tiff
*.TIFF
*.tif
*.TIF
*.svg
*.SVG
# Video files
*.mp4
*.MP4
*.mov
*.MOV
*.avi
*.AVI
*.mkv
*.MKV
*.webm
*.WEBM
*.m4v
*.M4V
*.wmv
*.WMV
*.flv
*.FLV
*.mpeg
*.MPEG
*.mpg
*.MPG
*.3gp
*.3GP
*.ogv
*.OGV
# Common media-related folders
src/
media/
images/
videos/
photos/
pictures/
# OS-specific files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Desktop.ini
# Editor-specific files
.vscode/
.idea/
*.swp
*.swo
*~
.project
.settings/
# Temporary files
*.tmp
*.temp
*.log

116
README.md
View File

@ -1,3 +1,115 @@
# media-slideshow # 📸 Media Slideshow
A client-side web application for viewing photos and videos A lightweight, privacy-focused web application for viewing photos and videos in a beautiful slideshow format. All processing happens locally in your browser - no uploads, no servers, no tracking.
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=flat&logo=html5&logoColor=white)
![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=flat&logo=javascript&logoColor=%23F7DF1E)
## ✨ Features
- 🖼️ **Fullscreen Display** - Images and videos fill your entire screen
- 📅 **EXIF Data Extraction** - Automatically reads capture date and time from photos
- 📍 **GPS Location Links** - Click to view photo locations on Google Maps (satellite view)
- 🔍 **Image Zoom** - Mouse wheel zoom (1x - 5x) with pan support
- ⌨️ **Keyboard Navigation** - Arrow keys or `<` `>` keys to navigate
- 📂 **Folder Overview** - Virtual screens for easy navigation between albums
- 🔒 **100% Private** - All processing happens locally in your browser
- 💾 **Offline Ready** - Download and use completely offline
- 📱 **Responsive** - Works on desktop, tablet, and mobile devices
## 🎨 Supported Formats
### Images (12 formats)
- **JPEG** - `.jpg`, `.jpeg`
- **PNG** - `.png`
- **GIF** - `.gif`
- **WebP** - `.webp`
- **AVIF** - `.avif`
- **HEIC/HEIF** - `.heic`, `.heif` (Apple format)
- **BMP** - `.bmp`
- **TIFF** - `.tiff`, `.tif`
- **SVG** - `.svg`
### Videos (12 formats)
- **MP4** - `.mp4`
- **MOV** - `.mov` (QuickTime)
- **AVI** - `.avi`
- **MKV** - `.mkv` (Matroska)
- **WebM** - `.webm`
- **M4V** - `.m4v`
- **WMV** - `.wmv` (Windows Media)
- **FLV** - `.flv` (Flash Video)
- **MPEG** - `.mpeg`, `.mpg`
- **3GP** - `.3gp` (Mobile)
- **OGV** - `.ogv` (Ogg Video)
*Note: Both uppercase and lowercase file extensions are supported*
## 📁 Folder Structure
Organize your media files with numeric prefixes for automatic sorting:
```
media/
├── 01_Vacation_Hawaii/
│ ├── IMG_001.jpg
│ ├── IMG_002.jpg
│ └── video_001.mp4
├── 02_Family_Reunion/
│ ├── photo1.jpg
│ ├── photo2.jpg
│ └── celebration.mov
└── 03_Weekend_Trip/
├── DSC_0001.jpg
└── sunset.mp4
```
**Rules:**
- Folders should have numeric prefixes: `01_`, `02_`, `03_`, etc.
- Files within folders are sorted alphabetically by filename
- The prefix is removed in the display (e.g., `01_Vacation_Hawaii``Vacation Hawaii`)
## 🎮 Controls
| Key | Action |
|-----|--------|
| `←` or `<` | Previous media |
| `→` or `>` | Next media |
| **Mouse Wheel** | Zoom in/out (images only) |
| **Click Image** | Toggle zoom on/off |
## 🛠️ Technical Details
### Architecture
- **Single HTML File** - Entire application in one self-contained file
- **No Dependencies** - Only uses EXIF.js CDN for metadata extraction
- **Client-Side Only** - No backend, no server, no data transmission
- **File System Access API** - Uses browser's native folder selection
### Browser Compatibility
- ✅ Chrome/Edge (recommended)
- ✅ Firefox
- ✅ Safari
- ✅ Opera
*Note: Requires a modern browser with File System Access support*
### Privacy & Security
- 🔒 All files stay on your computer
- 🚫 No data is uploaded to any server
- 🔐 No tracking or analytics
- 💾 Can be used completely offline
- 🌐 No external dependencies (except EXIF.js CDN)
## 📋 Use Cases
- **Photo Albums** - View vacation photos with date and location info
- **Video Collections** - Watch family videos in sequence
- **Photography Review** - Review EXIF data and GPS locations
- **Offline Presentations** - Present media without internet connection
- **Privacy-Conscious Viewing** - Keep your media completely private
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

800
index.html Normal file
View File

@ -0,0 +1,800 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Media Slideshow</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #000;
color: #fff;
overflow: hidden;
height: 100vh;
}
#container {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
#media-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
#media-container img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.1s ease-out;
cursor: zoom-in;
}
#media-container img.zoomed {
cursor: zoom-out;
}
#media-container video {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
#virtual-screen {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
padding: 60px;
border-radius: 20px;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
#virtual-screen h1 {
font-size: 3em;
margin-bottom: 40px;
text-align: center;
color: #fff;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
#virtual-screen h2 {
font-size: 2em;
margin-bottom: 20px;
color: #fff;
}
.folder-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.folder-item {
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 15px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.folder-item:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-5px);
}
.folder-name {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 10px;
}
.folder-count {
font-size: 1.1em;
opacity: 0.9;
}
#info-overlay {
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 15px 20px;
border-radius: 10px;
font-size: 16px;
display: flex;
align-items: center;
gap: 15px;
backdrop-filter: blur(10px);
}
#info-overlay a {
color: #4CAF50;
text-decoration: none;
font-weight: bold;
transition: color 0.3s;
}
#info-overlay a:hover {
color: #66BB6A;
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: #fff;
background: rgba(0, 0, 0, 0.8);
padding: 30px 50px;
border-radius: 15px;
z-index: 1000;
}
.hidden {
display: none !important;
}
#progress {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
}
/* Scrollbar styling for virtual screen */
#virtual-screen::-webkit-scrollbar {
width: 10px;
}
#virtual-screen::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
#virtual-screen::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
#virtual-screen::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
#file-selector {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.selector-content {
text-align: center;
background: rgba(255, 255, 255, 0.1);
padding: 60px 80px;
border-radius: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-height: 90vh;
overflow-y: auto;
}
.selector-content h1 {
font-size: 3em;
margin-bottom: 20px;
color: #fff;
}
.selector-content p {
font-size: 1.2em;
margin-bottom: 30px;
opacity: 0.9;
}
.instructions {
background: rgba(255, 255, 255, 0.15);
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
text-align: left;
max-width: 700px;
}
.instructions h2 {
font-size: 1.5em;
margin-bottom: 15px;
color: #fff;
}
.instructions h3 {
font-size: 1.2em;
margin-top: 20px;
margin-bottom: 10px;
color: #fff;
}
.instructions ul {
margin-left: 20px;
line-height: 1.8;
}
.instructions li {
margin-bottom: 8px;
}
.instructions code {
background: rgba(0, 0, 0, 0.3);
padding: 2px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.instructions .note {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 8px;
margin-top: 15px;
border-left: 4px solid #4CAF50;
}
.button-group {
display: flex;
gap: 15px;
justify-content: center;
align-items: center;
}
#select-button {
background: #fff;
color: #667eea;
border: none;
padding: 15px 40px;
font-size: 1.2em;
font-weight: bold;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
#select-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
#select-button:active {
transform: translateY(0);
}
#download-button {
background: rgba(255, 255, 255, 0.2);
color: #fff;
border: 2px solid #fff;
padding: 13px 30px;
font-size: 1em;
font-weight: bold;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
#download-button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.github-link {
margin-top: 20px;
font-size: 0.9em;
}
.github-link a {
color: #fff;
text-decoration: underline;
}
.github-link a:hover {
opacity: 0.8;
}
/* Scrollbar styling for selector content */
.selector-content::-webkit-scrollbar {
width: 10px;
}
.selector-content::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.selector-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
.selector-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
</head>
<body>
<div id="file-selector">
<div class="selector-content">
<h1>📸 Media Slideshow</h1>
<p>A client-side web application for viewing photos and videos</p>
<div class="instructions">
<h2>📖 How to Use</h2>
<h3>Folder Structure</h3>
<ul>
<li>Create folders with numeric prefixes: <code>01_Vacation</code>, <code>02_Family</code>, etc.
</li>
<li>Place your media files inside these folders</li>
<li>Files are displayed in alphabetical order by filename</li>
</ul>
<h3>Supported Formats</h3>
<ul>
<li><strong>Images:</strong> JPEG, PNG, GIF, WebP, AVIF, HEIC/HEIF, BMP, TIFF, SVG</li>
<li><strong>Videos:</strong> MP4, MOV, AVI, MKV, WebM, M4V, WMV, FLV, MPEG, 3GP, OGV</li>
<li>Both uppercase and lowercase file extensions are supported</li>
</ul>
<h3>Features</h3>
<ul>
<li>🖼️ Fullscreen display of images and videos</li>
<li>📅 Automatic EXIF date extraction from photos</li>
<li>📍 GPS location links to Google Maps (when data present)</li>
<li>🔍 Mouse wheel to zoom images (1x - 5x)</li>
<li>⌨️ Arrow keys or <code>&lt;</code> <code>&gt;</code> keys to navigate</li>
<li>📂 Virtual overview screens for easy folder navigation</li>
</ul>
<div class="note">
<strong>🔒 Privacy Note:</strong> All files are processed locally in your browser. Nothing is
uploaded to any server. Your media files never leave your computer. You can download this app and
use it completely offline.
</div>
</div>
<div class="button-group">
<input type="file" id="folder-input" webkitdirectory directory multiple style="display: none;">
<button id="select-button">Select Folder</button>
<button id="download-button" onclick="downloadHTML()">⬇ Download App</button>
</div>
<div class="github-link">
<a href="https://git.mosad.xyz/localhorst/media-slideshow" target="_blank">View on Git</a>
</div>
</div>
</div>
<div id="loading" class="hidden">Processing media files...</div>
<div id="progress" class="hidden"></div>
<div id="container" class="hidden">
<div id="media-container"></div>
</div>
<div id="info-overlay" class="hidden"></div>
<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
<script>
class MediaSlideshow {
constructor() {
this.mediaFiles = [];
this.currentIndex = 0;
this.zoom = 1;
this.isPanning = false;
this.panStart = { x: 0, y: 0 };
this.panOffset = { x: 0, y: 0 };
this.container = document.getElementById('media-container');
this.infoOverlay = document.getElementById('info-overlay');
this.loading = document.getElementById('loading');
this.progress = document.getElementById('progress');
this.fileSelector = document.getElementById('file-selector');
this.setupFileSelector();
}
setupFileSelector() {
const selectButton = document.getElementById('select-button');
const folderInput = document.getElementById('folder-input');
selectButton.addEventListener('click', () => {
folderInput.click();
});
folderInput.addEventListener('change', async (e) => {
if (e.target.files.length > 0) {
this.fileSelector.classList.add('hidden');
this.loading.classList.remove('hidden');
await this.processFiles(e.target.files);
this.setupEventListeners();
this.loading.classList.add('hidden');
document.getElementById('container').classList.remove('hidden');
this.showMedia(0);
}
});
}
async processFiles(files) {
// Group files by folder
const folderMap = new Map();
// Image formats: JPEG, PNG, GIF, WebP, AVIF, HEIC, BMP, TIFF
// Video formats: MP4, MOV, AVI, MKV, WebM, AVIF (video), M4V, WMV, FLV
const supportedExtensions = [
// Image formats
'jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'heic', 'heif', 'bmp', 'tiff', 'tif', 'svg',
// Video formats
'mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v', 'wmv', 'flv', 'mpeg', 'mpg', '3gp', 'ogv'
];
for (const file of files) {
const pathParts = file.webkitRelativePath.split('/');
// Check if file is in a subfolder and is a supported type
if (pathParts.length >= 2) {
const folderName = pathParts[pathParts.length - 2];
const extension = file.name.split('.').pop().toLowerCase();
if (supportedExtensions.includes(extension)) {
if (!folderMap.has(folderName)) {
folderMap.set(folderName, []);
}
folderMap.get(folderName).push(file);
}
}
}
// Sort folders by prefix (01_, 02_, etc.)
const sortedFolders = Array.from(folderMap.entries()).sort((a, b) => {
const aMatch = a[0].match(/^(\d+)_/);
const bMatch = b[0].match(/^(\d+)_/);
if (aMatch && bMatch) {
return parseInt(aMatch[1]) - parseInt(bMatch[1]);
}
return a[0].localeCompare(b[0]);
});
// Create overview virtual screen
const folderData = sortedFolders.map(([folderName, files]) => ({
name: folderName.replace(/^\d+_/, ''),
fullName: folderName,
count: files.length
}));
this.mediaFiles.push({
type: 'virtual-overview',
folders: folderData
});
// Process each folder
for (const [folderName, files] of sortedFolders) {
// Sort files by name
files.sort((a, b) => a.name.localeCompare(b.name));
// Add folder virtual screen
this.mediaFiles.push({
type: 'virtual-folder',
name: folderName.replace(/^\d+_/, ''),
fullName: folderName,
count: files.length
});
// Add media files
for (const file of files) {
this.mediaFiles.push({
type: this.getFileType(file.name),
file: file,
folder: folderName
});
}
}
}
getFileType(filename) {
const extension = filename.split('.').pop().toLowerCase();
// Video formats
const videoFormats = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v', 'wmv', 'flv', 'mpeg', 'mpg', '3gp', 'ogv'];
// Image formats (including AVIF which can be both image and video)
const imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'heic', 'heif', 'bmp', 'tiff', 'tif', 'svg'];
if (videoFormats.includes(extension)) return 'video';
if (imageFormats.includes(extension)) return 'image';
return 'unknown';
}
setupEventListeners() {
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === '<') {
this.previous();
} else if (e.key === 'ArrowRight' || e.key === '>') {
this.next();
}
});
// Mouse wheel for zoom on images
this.container.addEventListener('wheel', (e) => {
const currentMedia = this.mediaFiles[this.currentIndex];
if (currentMedia && currentMedia.type === 'image') {
e.preventDefault();
const img = this.container.querySelector('img');
if (img) {
const delta = e.deltaY > 0 ? 0.9 : 1.1;
this.zoom *= delta;
this.zoom = Math.max(1, Math.min(5, this.zoom));
if (this.zoom > 1) {
img.classList.add('zoomed');
} else {
img.classList.remove('zoomed');
this.panOffset = { x: 0, y: 0 };
}
img.style.transform = `scale(${this.zoom}) translate(${this.panOffset.x}px, ${this.panOffset.y}px)`;
}
}
});
// Click to zoom toggle
this.container.addEventListener('click', (e) => {
if (e.target.tagName === 'IMG' && !e.target.closest('#virtual-screen')) {
if (this.zoom > 1) {
this.zoom = 1;
this.panOffset = { x: 0, y: 0 };
e.target.classList.remove('zoomed');
e.target.style.transform = 'scale(1) translate(0, 0)';
} else {
this.zoom = 2;
e.target.classList.add('zoomed');
e.target.style.transform = `scale(${this.zoom})`;
}
}
});
}
previous() {
this.currentIndex = (this.currentIndex - 1 + this.mediaFiles.length) % this.mediaFiles.length;
this.showMedia(this.currentIndex);
}
next() {
this.currentIndex = (this.currentIndex + 1) % this.mediaFiles.length;
this.showMedia(this.currentIndex);
}
async showMedia(index) {
this.currentIndex = index;
this.zoom = 1;
this.panOffset = { x: 0, y: 0 };
this.container.innerHTML = '';
this.infoOverlay.classList.add('hidden');
this.progress.classList.remove('hidden');
this.progress.textContent = `${index + 1} / ${this.mediaFiles.length}`;
const media = this.mediaFiles[index];
if (media.type === 'virtual-overview') {
this.showVirtualOverview(media);
} else if (media.type === 'virtual-folder') {
this.showVirtualFolder(media);
} else if (media.type === 'image') {
await this.showImage(media);
} else if (media.type === 'video') {
this.showVideo(media);
}
}
showVirtualOverview(media) {
const virtualScreen = document.createElement('div');
virtualScreen.id = 'virtual-screen';
let html = '<h1>Media Slideshow</h1>';
html += '<div class="folder-list">';
media.folders.forEach((folder, idx) => {
// Find the index of this folder's virtual screen
const folderIndex = this.mediaFiles.findIndex(m =>
m.type === 'virtual-folder' && m.fullName === folder.fullName
);
html += `
<div class="folder-item" onclick="slideshow.showMedia(${folderIndex})">
<div class="folder-name">${folder.name}</div>
<div class="folder-count">${folder.count} media file${folder.count !== 1 ? 's' : ''}</div>
</div>
`;
});
html += '</div>';
virtualScreen.innerHTML = html;
this.container.appendChild(virtualScreen);
}
showVirtualFolder(media) {
const virtualScreen = document.createElement('div');
virtualScreen.id = 'virtual-screen';
virtualScreen.innerHTML = `
<h1>${media.name}</h1>
<h2>${media.count} media file${media.count !== 1 ? 's' : ''}</h2>
`;
this.container.appendChild(virtualScreen);
}
async showImage(media) {
const img = document.createElement('img');
const url = URL.createObjectURL(media.file);
img.onload = async () => {
// Extract EXIF data
EXIF.getData(img, async () => {
const exifData = EXIF.getAllTags(img);
let dateStr = '';
let geoLink = '';
// Try to get date from EXIF
if (exifData.DateTimeOriginal) {
dateStr = this.formatExifDate(exifData.DateTimeOriginal);
} else if (exifData.DateTime) {
dateStr = this.formatExifDate(exifData.DateTime);
} else {
// Use file modification date
dateStr = this.formatFileDate(media.file.lastModified);
}
// Check for GPS data
const lat = exifData.GPSLatitude;
const lon = exifData.GPSLongitude;
const latRef = exifData.GPSLatitudeRef;
const lonRef = exifData.GPSLongitudeRef;
if (lat && lon) {
const latDecimal = this.convertDMSToDD(lat, latRef);
const lonDecimal = this.convertDMSToDD(lon, lonRef);
geoLink = `https://www.google.com/maps/place/${latDecimal},${lonDecimal}/@${latDecimal},${lonDecimal},15z/data=!3m1!1e3`;
}
// Show info overlay
this.showInfoOverlay(dateStr, geoLink);
});
};
img.src = url;
this.container.appendChild(img);
}
showVideo(media) {
const video = document.createElement('video');
video.controls = true;
video.autoplay = true;
video.src = URL.createObjectURL(media.file);
// Show file date for videos
const dateStr = this.formatFileDate(media.file.lastModified);
this.showInfoOverlay(dateStr, null);
this.container.appendChild(video);
}
showInfoOverlay(dateStr, geoLink) {
this.infoOverlay.classList.remove('hidden');
let html = `<span>${dateStr}</span>`;
if (geoLink) {
html += `<a href="${geoLink}" target="_blank">📍 View on Google Maps</a>`;
}
this.infoOverlay.innerHTML = html;
}
formatExifDate(exifDate) {
// EXIF date format: "YYYY:MM:DD HH:MM:SS"
const parts = exifDate.split(' ');
const dateParts = parts[0].split(':');
const timeParts = parts[1] ? parts[1].split(':') : ['00', '00', '00'];
const date = new Date(
parseInt(dateParts[0]),
parseInt(dateParts[1]) - 1,
parseInt(dateParts[2]),
parseInt(timeParts[0]),
parseInt(timeParts[1]),
parseInt(timeParts[2])
);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
formatFileDate(timestamp) {
const date = new Date(timestamp);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
convertDMSToDD(dms, ref) {
// DMS format: [degrees, minutes, seconds]
const degrees = dms[0];
const minutes = dms[1];
const seconds = dms[2];
let dd = degrees + minutes / 60 + seconds / 3600;
if (ref === 'S' || ref === 'W') {
dd = dd * -1;
}
return dd;
}
}
// Start the slideshow when page loads
let slideshow;
window.addEventListener('load', () => {
slideshow = new MediaSlideshow();
});
// Function to download the current HTML file
function downloadHTML() {
const htmlContent = document.documentElement.outerHTML;
const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'media-slideshow.html';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
</script>
</body>
</html>