Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-10 19:36:09 +02:00
Commit 3f2a2fc4dc
16 geänderte Dateien mit 6621 neuen und 0 gelöschten Zeilen

11
.claude/settings.local.json Normale Datei
Datei anzeigen

@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(npm install)",
"Bash(npm run build-win:*)",
"Bash(chmod:*)"
],
"deny": []
}
}

5
.gitignore vendored Normale Datei
Datei anzeigen

@ -0,0 +1,5 @@
node_modules/
dist/
*.log
.DS_Store
Thumbs.db

89
CLAUDE_PROJECT_README.md Normale Datei
Datei anzeigen

@ -0,0 +1,89 @@
# Toolbox-Metadaten-Crawler
*This README was automatically generated by Claude Project Manager*
## Project Overview
- **Path**: `C:/Users/hendr/Desktop/IntelSight/Projektablage/Toolbox-Metadaten-Crawler`
- **Files**: 88 files
- **Size**: 275.2 MB
- **Last Modified**: 2025-07-10 19:19
## Technology Stack
### Languages
- Batch
- JavaScript
- Python
### Frameworks & Libraries
- React
## Project Structure
```
app.js
build.bat
CLAUDE_PROJECT_README.md
gitea_push_debug.txt
index.html
main.js
main.py
package-lock.json
package.json
assets
dist/
├── builder-debug.yml
├── builder-effective-config.yaml
└── win-unpacked/
├── chrome_100_percent.pak
├── chrome_200_percent.pak
├── d3dcompiler_47.dll
├── ffmpeg.dll
├── icudtl.dat
├── libEGL.dll
├── libGLESv2.dll
├── LICENSE.electron.txt
├── LICENSES.chromium.html
├── Metadaten-Crawler.exe
├── locales/
│ ├── af.pak
│ ├── am.pak
│ ├── ar.pak
│ ├── bg.pak
│ ├── bn.pak
│ ├── ca.pak
│ ├── cs.pak
│ ├── da.pak
│ ├── de.pak
│ └── el.pak
└── resources/
└── app.asar
```
## Key Files
- `package.json`
- `README.md`
- `requirements.txt`
## Claude Integration
This project is managed with Claude Project Manager. To work with this project:
1. Open Claude Project Manager
2. Click on this project's tile
3. Claude will open in the project directory
## Notes
*Add your project-specific notes here*
---
## Development Log
- README generated on 2025-07-08 13:51:49
- README updated on 2025-07-08 20:45:50
- README updated on 2025-07-10 16:11:59
- README updated on 2025-07-10 19:34:36

57
README.md Normale Datei
Datei anzeigen

@ -0,0 +1,57 @@
# Metadaten-Crawler (MC)
Ein professionelles Desktop-Tool zur Extraktion von Metadaten aus Bild- und Videodateien.
## Features
- Drag & Drop Unterstützung für Dateien
- Extraktion von EXIF-, IPTC- und XMP-Metadaten
- Unterstützung für Bild- und Videoformate
- Dark/Light Mode mit professionellem Design
- Native Desktop-Anwendung für Windows
## Installation
### Voraussetzungen
- Node.js (v16 oder höher)
- npm
### Entwicklung
```bash
# Abhängigkeiten installieren
npm install
# Anwendung starten
npm start
```
### Build
```bash
# Windows Executable erstellen
npm run build-win
```
Die ausführbare Datei finden Sie im `dist` Verzeichnis.
## Unterstützte Formate
### Bilder
- JPEG/JPG
- PNG
- GIF
- BMP
- WEBP
### Videos
- MP4
- AVI
- MOV
- MKV
## Design
Die Anwendung folgt dem IntelSight Corporate Design System mit:
- Dark Mode als Standard
- Professionellen Farbpaletten
- Poppins Font für UI-Elemente
- Minimalistischem, modernen Design

914
app.js Normale Datei
Datei anzeigen

@ -0,0 +1,914 @@
// === METADATEN-CRAWLER JAVASCRIPT ===
// DOM Elements
const fileDropZone = document.getElementById('fileDropZone');
const fileInput = document.getElementById('fileInput');
const loading = document.getElementById('loading');
const metadataContainer = document.getElementById('metadataContainer');
const fileInfo = document.getElementById('fileInfo');
const imagePreview = document.getElementById('imagePreview');
const metadataContent = document.getElementById('metadataContent');
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
fileDropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight drop zone when item is dragged over it
['dragenter', 'dragover'].forEach(eventName => {
fileDropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
fileDropZone.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
fileDropZone.classList.add('drag-over');
}
function unhighlight(e) {
fileDropZone.classList.remove('drag-over');
}
// Handle dropped files
fileDropZone.addEventListener('drop', handleFileDrop, false);
function handleFileDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
handleFile(files[0]);
}
}
// Handle file selection via click
fileDropZone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
// Main file handler
async function handleFile(file) {
// Show loading
loading.classList.add('active');
metadataContainer.classList.remove('active');
// Display basic file info
const basicInfo = {
name: file.name,
size: formatFileSize(file.size),
type: file.type,
lastModified: new Date(file.lastModified).toLocaleString('de-DE')
};
// Check if it's an image or video
if (file.type.startsWith('image/')) {
await processImage(file, basicInfo);
} else if (file.type.startsWith('video/')) {
await processVideo(file, basicInfo);
} else {
alert('Bitte wählen Sie eine Bild- oder Videodatei aus.');
loading.classList.remove('active');
return;
}
}
// Process image files
async function processImage(file, basicInfo) {
try {
// Read file as ArrayBuffer for ExifReader
const arrayBuffer = await file.arrayBuffer();
// Extract EXIF data
let exifData = {};
try {
const tags = ExifReader.load(arrayBuffer);
exifData = tags;
} catch (error) {
console.log('Keine EXIF-Daten gefunden oder Fehler beim Lesen:', error);
}
// Create object URL for preview
const objectUrl = URL.createObjectURL(file);
// Get image dimensions
const img = new Image();
img.onload = function() {
basicInfo.width = this.width + ' px';
basicInfo.height = this.height + ' px';
basicInfo.aspect = (this.width / this.height).toFixed(2);
displayImageMetadata(basicInfo, exifData, objectUrl);
URL.revokeObjectURL(objectUrl);
};
img.src = objectUrl;
} catch (error) {
console.error('Fehler beim Verarbeiten der Bilddatei:', error);
alert('Fehler beim Lesen der Metadaten.');
loading.classList.remove('active');
}
}
// Process video files
async function processVideo(file, basicInfo) {
try {
// Create video element to extract basic metadata
const video = document.createElement('video');
const objectUrl = URL.createObjectURL(file);
video.preload = 'metadata';
video.onloadedmetadata = function() {
basicInfo.duration = formatDuration(video.duration);
basicInfo.width = video.videoWidth + ' px';
basicInfo.height = video.videoHeight + ' px';
basicInfo.aspect = (video.videoWidth / video.videoHeight).toFixed(2);
displayVideoMetadata(basicInfo, objectUrl);
URL.revokeObjectURL(objectUrl);
};
video.src = objectUrl;
} catch (error) {
console.error('Fehler beim Verarbeiten der Videodatei:', error);
alert('Fehler beim Lesen der Video-Metadaten.');
loading.classList.remove('active');
}
}
// Display image metadata
function displayImageMetadata(basicInfo, exifData, previewUrl) {
// Store data for report generation
currentFileData = basicInfo;
currentPreviewUrl = previewUrl;
currentMetadata = {};
// Update file info
fileInfo.innerHTML = `
<h3>${basicInfo.name}</h3>
<div class="basic-info">
<span class="label">Größe:</span><span>${basicInfo.size}</span>
<span class="label">Typ:</span><span>${basicInfo.type}</span>
<span class="label">Zuletzt geändert:</span><span>${basicInfo.lastModified}</span>
<span class="label">Auflösung:</span><span>${basicInfo.width} × ${basicInfo.height}</span>
<span class="label">Seitenverhältnis:</span><span>${basicInfo.aspect}:1</span>
</div>
`;
// Show preview
imagePreview.innerHTML = `<img src="${previewUrl}" alt="Vorschau">`;
// Organize metadata into sections
const sections = {
exif: {
title: 'EXIF-Daten',
data: {},
icon: '📷'
},
camera: {
title: 'Kamera-Informationen',
data: {},
icon: '📸'
},
location: {
title: 'Standort-Informationen',
data: {},
icon: '📍'
},
technical: {
title: 'Technische Details',
data: {},
icon: '⚙️'
},
other: {
title: 'Weitere Metadaten',
data: {},
icon: '📋'
}
};
// Process EXIF data and categorize
if (exifData) {
for (const [key, value] of Object.entries(exifData)) {
if (value && value.description !== undefined) {
const desc = value.description;
// Categorize metadata
if (key.includes('Make') || key.includes('Model') || key.includes('Lens')) {
sections.camera.data[key] = desc;
} else if (key.includes('GPS')) {
sections.location.data[key] = desc;
} else if (key.includes('DateTime') || key.includes('Date')) {
sections.exif.data[key] = desc;
} else if (key.includes('ISO') || key.includes('Exposure') || key.includes('Aperture') ||
key.includes('FocalLength') || key.includes('Flash')) {
sections.technical.data[key] = desc;
} else {
sections.other.data[key] = desc;
}
}
}
}
// Build metadata display
let metadataHTML = '';
for (const [sectionKey, section] of Object.entries(sections)) {
if (Object.keys(section.data).length > 0) {
metadataHTML += createMetadataSection(section.title, section.data, section.icon);
// Store for report
currentMetadata[section.title] = section.data;
}
}
if (metadataHTML === '') {
metadataHTML = '<div class="no-metadata">Keine zusätzlichen Metadaten gefunden.</div>';
}
metadataContent.innerHTML = metadataHTML;
// Add click handlers for collapsible sections
addSectionToggleHandlers();
// Hide loading and show metadata
loading.classList.remove('active');
metadataContainer.classList.add('active');
}
// Display video metadata
function displayVideoMetadata(basicInfo, previewUrl) {
// Store data for report generation
currentFileData = basicInfo;
currentPreviewUrl = previewUrl;
currentMetadata = {};
// Update file info
fileInfo.innerHTML = `
<h3>${basicInfo.name}</h3>
<div class="basic-info">
<span class="label">Größe:</span><span>${basicInfo.size}</span>
<span class="label">Typ:</span><span>${basicInfo.type}</span>
<span class="label">Zuletzt geändert:</span><span>${basicInfo.lastModified}</span>
<span class="label">Dauer:</span><span>${basicInfo.duration}</span>
<span class="label">Auflösung:</span><span>${basicInfo.width} × ${basicInfo.height}</span>
<span class="label">Seitenverhältnis:</span><span>${basicInfo.aspect}:1</span>
</div>
`;
// Show video preview (poster frame)
imagePreview.innerHTML = `
<video controls style="max-width: 300px; max-height: 300px; border-radius: 8px;">
<source src="${previewUrl}" type="${basicInfo.type}">
Ihr Browser unterstützt das Video-Tag nicht.
</video>
`;
// For videos, we have limited metadata access from browser
const videoMetadata = {
'Format': basicInfo.type,
'Dauer': basicInfo.duration,
'Auflösung': `${basicInfo.width} × ${basicInfo.height}`,
'Seitenverhältnis': `${basicInfo.aspect}:1`,
'Dateigröße': basicInfo.size
};
let metadataHTML = createMetadataSection('Video-Informationen', videoMetadata, '🎬', 'video');
metadataHTML += '<div class="no-metadata">Erweiterte Video-Metadaten können im Browser nur begrenzt ausgelesen werden.</div>';
// Store for report
currentMetadata['Video-Informationen'] = videoMetadata;
metadataContent.innerHTML = metadataHTML;
// Add click handlers for collapsible sections
addSectionToggleHandlers();
// Hide loading and show metadata
loading.classList.remove('active');
metadataContainer.classList.add('active');
}
// Create metadata section HTML
function createMetadataSection(title, data, icon = '📋', cssClass = '') {
let html = `
<div class="metadata-section ${cssClass}">
<div class="metadata-section-header ${cssClass}">
<span>${icon} ${title}</span>
<span class="toggle-icon">▼</span>
</div>
<div class="metadata-content">
<table class="metadata-table">
`;
for (const [key, value] of Object.entries(data)) {
const formattedKey = formatMetadataKey(key);
const formattedValue = formatMetadataValue(key, value);
html += `
<tr>
<td>${formattedKey}</td>
<td>${formattedValue}</td>
</tr>
`;
}
html += `
</table>
</div>
</div>
`;
return html;
}
// Device model mapping for cameras
const deviceModelMapping = {
// Apple iPhone models
'iPhone 15 Pro Max': 'Apple iPhone 15 Pro Max',
'iPhone 15 Pro': 'Apple iPhone 15 Pro',
'iPhone 15 Plus': 'Apple iPhone 15 Plus',
'iPhone 15': 'Apple iPhone 15',
'iPhone 14 Pro Max': 'Apple iPhone 14 Pro Max',
'iPhone 14 Pro': 'Apple iPhone 14 Pro',
'iPhone 14 Plus': 'Apple iPhone 14 Plus',
'iPhone 14': 'Apple iPhone 14',
'iPhone 13 Pro Max': 'Apple iPhone 13 Pro Max',
'iPhone 13 Pro': 'Apple iPhone 13 Pro',
'iPhone 13': 'Apple iPhone 13',
'iPhone 13 mini': 'Apple iPhone 13 mini',
'iPhone 12 Pro Max': 'Apple iPhone 12 Pro Max',
'iPhone 12 Pro': 'Apple iPhone 12 Pro',
'iPhone 12': 'Apple iPhone 12',
'iPhone 12 mini': 'Apple iPhone 12 mini',
'iPhone SE (3rd generation)': 'Apple iPhone SE (2022)',
'iPhone SE (2nd generation)': 'Apple iPhone SE (2020)',
'iPhone 11 Pro Max': 'Apple iPhone 11 Pro Max',
'iPhone 11 Pro': 'Apple iPhone 11 Pro',
'iPhone 11': 'Apple iPhone 11',
'iPhone XS Max': 'Apple iPhone XS Max',
'iPhone XS': 'Apple iPhone XS',
'iPhone XR': 'Apple iPhone XR',
'iPhone X': 'Apple iPhone X',
'iPhone 8 Plus': 'Apple iPhone 8 Plus',
'iPhone 8': 'Apple iPhone 8',
'iPhone 7 Plus': 'Apple iPhone 7 Plus',
'iPhone 7': 'Apple iPhone 7',
'iPhone 6s Plus': 'Apple iPhone 6s Plus',
'iPhone 6s': 'Apple iPhone 6s',
'iPhone 6 Plus': 'Apple iPhone 6 Plus',
'iPhone 6': 'Apple iPhone 6',
// Apple iPad models
'iPad Pro (12.9-inch) (6th generation)': 'Apple iPad Pro 12.9" (2022)',
'iPad Pro (11-inch) (4th generation)': 'Apple iPad Pro 11" (2022)',
'iPad Air (5th generation)': 'Apple iPad Air (2022)',
'iPad (10th generation)': 'Apple iPad (2022)',
'iPad mini (6th generation)': 'Apple iPad mini (2021)',
// Samsung Galaxy models
'SM-S928B': 'Samsung Galaxy S24 Ultra',
'SM-S926B': 'Samsung Galaxy S24+',
'SM-S921B': 'Samsung Galaxy S24',
'SM-S918B': 'Samsung Galaxy S23 Ultra',
'SM-S916B': 'Samsung Galaxy S23+',
'SM-S911B': 'Samsung Galaxy S23',
'SM-S908B': 'Samsung Galaxy S22 Ultra',
'SM-S906B': 'Samsung Galaxy S22+',
'SM-S901B': 'Samsung Galaxy S22',
'SM-G998B': 'Samsung Galaxy S21 Ultra',
'SM-G996B': 'Samsung Galaxy S21+',
'SM-G991B': 'Samsung Galaxy S21',
'SM-G988B': 'Samsung Galaxy S20 Ultra',
'SM-G986B': 'Samsung Galaxy S20+',
'SM-G981B': 'Samsung Galaxy S20',
'SM-A546B': 'Samsung Galaxy A54',
'SM-A346B': 'Samsung Galaxy A34',
'SM-A536B': 'Samsung Galaxy A53',
'SM-A336B': 'Samsung Galaxy A33',
'SM-A526B': 'Samsung Galaxy A52',
'SM-A326B': 'Samsung Galaxy A32',
// Google Pixel models
'Pixel 8 Pro': 'Google Pixel 8 Pro',
'Pixel 8': 'Google Pixel 8',
'Pixel 7a': 'Google Pixel 7a',
'Pixel 7 Pro': 'Google Pixel 7 Pro',
'Pixel 7': 'Google Pixel 7',
'Pixel 6a': 'Google Pixel 6a',
'Pixel 6 Pro': 'Google Pixel 6 Pro',
'Pixel 6': 'Google Pixel 6',
'Pixel 5a': 'Google Pixel 5a',
'Pixel 5': 'Google Pixel 5',
'Pixel 4a': 'Google Pixel 4a',
'Pixel 4 XL': 'Google Pixel 4 XL',
'Pixel 4': 'Google Pixel 4',
// Xiaomi models
'Mi 13 Ultra': 'Xiaomi Mi 13 Ultra',
'Mi 13 Pro': 'Xiaomi Mi 13 Pro',
'Mi 13': 'Xiaomi Mi 13',
'Mi 12 Ultra': 'Xiaomi Mi 12 Ultra',
'Mi 12 Pro': 'Xiaomi Mi 12 Pro',
'Mi 12': 'Xiaomi Mi 12',
'Mi 11 Ultra': 'Xiaomi Mi 11 Ultra',
'Mi 11 Pro': 'Xiaomi Mi 11 Pro',
'Mi 11': 'Xiaomi Mi 11',
'Redmi Note 13 Pro+': 'Xiaomi Redmi Note 13 Pro+',
'Redmi Note 13 Pro': 'Xiaomi Redmi Note 13 Pro',
'Redmi Note 12 Pro+': 'Xiaomi Redmi Note 12 Pro+',
'Redmi Note 12 Pro': 'Xiaomi Redmi Note 12 Pro',
'POCO X6 Pro': 'Xiaomi POCO X6 Pro',
'POCO F5': 'Xiaomi POCO F5',
// OnePlus models
'LE2125': 'OnePlus 9 Pro',
'LE2123': 'OnePlus 9',
'IN2023': 'OnePlus 8 Pro',
'IN2013': 'OnePlus 8',
'HD1913': 'OnePlus 7T Pro',
'HD1903': 'OnePlus 7T',
'GM1913': 'OnePlus 7 Pro',
'GM1903': 'OnePlus 7',
'CPH2423': 'OnePlus 11',
'CPH2449': 'OnePlus 11R',
'CPH2411': 'OnePlus 10 Pro',
'CPH2413': 'OnePlus 10T',
// Huawei models (newer models might not have Google services)
'VOG-L29': 'Huawei P30 Pro',
'ELE-L29': 'Huawei P30',
'CLT-L29': 'Huawei P20 Pro',
'EML-L29': 'Huawei P20',
'LYA-L29': 'Huawei Mate 20 Pro',
'HMA-L29': 'Huawei Mate 20',
'NOH-NX9': 'Huawei Mate 60 Pro',
'BRA-NX9': 'Huawei Mate 60',
// Sony models
'XQ-DQ54': 'Sony Xperia 1 V',
'XQ-CQ54': 'Sony Xperia 5 V',
'XQ-BQ52': 'Sony Xperia 1 IV',
'XQ-AS52': 'Sony Xperia 5 IV',
'XQ-BC52': 'Sony Xperia 1 III',
'XQ-AS62': 'Sony Xperia 5 III',
// OPPO models
'CPH2437': 'OPPO Find X6 Pro',
'CPH2305': 'OPPO Find X5 Pro',
'CPH2173': 'OPPO Find X3 Pro',
'CPH2487': 'OPPO Reno 11 Pro',
'CPH2481': 'OPPO Reno 10 Pro+',
// Vivo models
'V2302': 'Vivo X100 Pro',
'V2250': 'Vivo X90 Pro',
'V2145': 'Vivo X80 Pro',
'V2183A': 'Vivo X70 Pro+',
// Realme models
'RMX3771': 'Realme GT 5',
'RMX3708': 'Realme GT 3',
'RMX3300': 'Realme GT 2 Pro',
'RMX3360': 'Realme GT Neo 3',
// Nothing models
'A063': 'Nothing Phone (2)',
'A013': 'Nothing Phone (1)',
// ASUS models
'ASUS_AI2302': 'ASUS Zenfone 10',
'ASUS_AI2202': 'ASUS Zenfone 9',
'ASUS_I006D': 'ASUS ROG Phone 7',
'ASUS_I005D': 'ASUS ROG Phone 6'
};
// Function to get device name from model
function getDeviceFromModel(model) {
if (!model) return null;
// Direct match
if (deviceModelMapping[model]) {
return deviceModelMapping[model];
}
// Try to match partial model names
for (const [key, value] of Object.entries(deviceModelMapping)) {
if (model.includes(key) || key.includes(model)) {
return value;
}
}
// Check if it's a generic Apple device
if (model.includes('iPhone') || model.includes('iPad')) {
return model; // Return as is, it's already descriptive
}
return null;
}
// Format metadata keys for display
function formatMetadataKey(key) {
// Remove common prefixes and format
key = key.replace(/^(EXIF|GPS|IPTC|XMP)/, '');
// Add spaces before capital letters
key = key.replace(/([A-Z])/g, ' $1').trim();
// Special formatting for known keys
const keyMap = {
'Make': 'Kamera-Hersteller',
'Model': 'Kamera-Modell',
'DateTime Original': 'Aufnahmedatum',
'Exposure Time': 'Belichtungszeit',
'F Number': 'Blende',
'ISO Speed Ratings': 'ISO-Wert',
'Focal Length': 'Brennweite',
'Flash': 'Blitz',
'GPS Latitude': 'Breitengrad',
'GPS Longitude': 'Längengrad',
'GPS Altitude': 'Höhe',
'Lens Model': 'Objektiv',
'White Balance': 'Weißabgleich',
'Exposure Mode': 'Belichtungsmodus',
'Color Space': 'Farbraum'
};
return keyMap[key.trim()] || key;
}
// Format metadata values for display
function formatMetadataValue(key, value) {
// Handle camera model with device mapping
if (key.includes('Model') && !key.includes('Lens')) {
const device = getDeviceFromModel(value);
if (device && device !== value) {
return `${value} (${device})`;
}
}
// Format GPS coordinates
if (key.includes('GPS') && (key.includes('Latitude') || key.includes('Longitude'))) {
if (typeof value === 'number') {
return value.toFixed(6) + '°';
}
}
// Format altitude
if (key.includes('Altitude') && typeof value === 'number') {
return value.toFixed(2) + ' m';
}
// Format focal length
if (key.includes('FocalLength') && typeof value === 'number') {
return value + ' mm';
}
// Format dates
if (key.includes('Date') && value.includes(':')) {
try {
const date = new Date(value.replace(/^(\d{4}):(\d{2}):(\d{2})/, '$1-$2-$3'));
if (!isNaN(date)) {
return date.toLocaleString('de-DE');
}
} catch (e) {}
}
return value;
}
// Add toggle handlers for collapsible sections
function addSectionToggleHandlers() {
const headers = document.querySelectorAll('.metadata-section-header');
headers.forEach(header => {
header.addEventListener('click', function() {
const section = this.parentElement;
section.classList.toggle('collapsed');
});
});
}
// Clear metadata and reset
function clearMetadata() {
metadataContainer.classList.remove('active');
fileInput.value = '';
imagePreview.innerHTML = '';
metadataContent.innerHTML = '';
// Clear stored data
currentFileData = null;
currentMetadata = null;
currentPreviewUrl = null;
}
// Utility functions
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDuration(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) {
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
return `${m}:${s.toString().padStart(2, '0')}`;
}
// Store current file data for report generation
let currentFileData = null;
let currentMetadata = null;
let currentPreviewUrl = null;
// Generate PDF report
async function generateReport() {
if (!currentFileData) {
alert('Keine Datei zum Erstellen eines Berichts vorhanden.');
return;
}
try {
// Create loading indicator
const originalText = document.getElementById('reportBtn').textContent;
document.getElementById('reportBtn').textContent = 'Bericht wird erstellt...';
document.getElementById('reportBtn').disabled = true;
// Import jsPDF and AutoTable dynamically
const jsPDFScript = document.createElement('script');
jsPDFScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
document.head.appendChild(jsPDFScript);
jsPDFScript.onload = async () => {
// Load AutoTable plugin
const autoTableScript = document.createElement('script');
autoTableScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.1/jspdf.plugin.autotable.min.js';
document.head.appendChild(autoTableScript);
autoTableScript.onload = async () => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Set fonts
doc.setFont('helvetica');
// Add header
doc.setFontSize(20);
doc.setTextColor(35, 45, 83); // Primary color
doc.text('Metadaten-Bericht', 105, 20, { align: 'center' });
// Add generation date
doc.setFontSize(10);
doc.setTextColor(100);
const now = new Date();
doc.text(`Erstellt am: ${now.toLocaleString('de-DE')}`, 105, 30, { align: 'center' });
// Add file info section
let yPosition = 50;
doc.setFontSize(14);
doc.setTextColor(35, 45, 83);
doc.text('Datei-Informationen', 20, yPosition);
yPosition += 5;
// Basic file info table data
const fileInfoData = [
['Dateiname', currentFileData.name],
['Größe', currentFileData.size],
['Typ', currentFileData.type],
['Zuletzt geändert', currentFileData.lastModified]
];
if (currentFileData.width) {
fileInfoData.push(['Auflösung', `${currentFileData.width} × ${currentFileData.height}`]);
fileInfoData.push(['Seitenverhältnis', `${currentFileData.aspect}:1`]);
}
if (currentFileData.duration) {
fileInfoData.push(['Dauer', currentFileData.duration]);
}
// Create table for file info
doc.autoTable({
startY: yPosition,
head: [],
body: fileInfoData,
theme: 'grid',
styles: {
fontSize: 10,
cellPadding: 5
},
columnStyles: {
0: {
fontStyle: 'bold',
fillColor: [245, 245, 245],
textColor: [35, 45, 83],
cellWidth: 50
}
},
margin: { left: 20, right: 20 }
});
yPosition = doc.lastAutoTable.finalY + 10;
// Add thumbnail if it's an image
if (currentPreviewUrl && currentFileData.type.startsWith('image/')) {
try {
// Check if we have enough space on current page
const remainingSpace = doc.internal.pageSize.getHeight() - yPosition - 40;
if (remainingSpace < 100) { // If less than 100 units remaining, start new page
doc.addPage();
yPosition = 20;
}
yPosition += 10;
doc.setFontSize(14);
doc.setTextColor(35, 45, 83);
// Center the "Vorschau" text
const pageWidth = doc.internal.pageSize.getWidth();
doc.text('Vorschau', pageWidth / 2, yPosition, { align: 'center' });
yPosition += 10;
// Get image element
const img = document.querySelector('.image-preview img');
if (img) {
// Convert image to base64 with high quality
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Use higher resolution for better quality
const maxWidth = 800;
const maxHeight = 800;
let width = img.naturalWidth;
let height = img.naturalHeight;
// Calculate aspect ratio
const ratio = Math.min(maxWidth / width, maxHeight / height);
if (ratio < 1) {
width = width * ratio;
height = height * ratio;
}
canvas.width = width;
canvas.height = height;
// Enable image smoothing for better quality
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, width, height);
// Use higher quality JPEG compression
const dataUrl = canvas.toDataURL('image/jpeg', 0.95);
// Calculate display size to fit on page
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const maxDisplayWidth = pageWidth - 40; // 20px margin on each side
const maxDisplayHeight = pageHeight - yPosition - 40; // Leave space for footer
// Start with desired width
let displayWidth = 80; // Smaller default size
let displayHeight = (height / width) * displayWidth;
// Check if it fits vertically, if not scale down
if (displayHeight > maxDisplayHeight) {
displayHeight = maxDisplayHeight;
displayWidth = (width / height) * displayHeight;
}
// Check if it fits horizontally, if not scale down
if (displayWidth > maxDisplayWidth) {
displayWidth = maxDisplayWidth;
displayHeight = (height / width) * displayWidth;
}
// Center the image horizontally
const xPosition = (pageWidth - displayWidth) / 2;
doc.addImage(dataUrl, 'JPEG', xPosition, yPosition, displayWidth, displayHeight);
yPosition += displayHeight + 10;
}
} catch (error) {
console.error('Fehler beim Hinzufügen des Thumbnails:', error);
}
}
// Add metadata sections
if (currentMetadata && Object.keys(currentMetadata).length > 0) {
// Check if we need a new page
if (yPosition > 200) {
doc.addPage();
yPosition = 20;
}
Object.entries(currentMetadata).forEach(([section, data]) => {
if (Object.keys(data).length > 0) {
// Check if we need a new page
if (yPosition > 240) {
doc.addPage();
yPosition = 20;
}
// Section header
doc.setFontSize(12);
doc.setTextColor(35, 45, 83);
doc.text(section, 20, yPosition);
yPosition += 5;
// Convert data to table format
const tableData = Object.entries(data).map(([key, value]) => {
// Format the key
const formattedKey = formatMetadataKey(key);
// Ensure value is string and not too long
const displayValue = String(value).length > 100 ? String(value).substring(0, 100) + '...' : String(value);
return [formattedKey, displayValue];
});
// Create table for this section
doc.autoTable({
startY: yPosition,
head: [],
body: tableData,
theme: 'grid',
styles: {
fontSize: 9,
cellPadding: 3
},
columnStyles: {
0: {
fontStyle: 'bold',
fillColor: [245, 245, 245],
textColor: [35, 45, 83],
cellWidth: 60
},
1: {
cellWidth: 'auto'
}
},
margin: { left: 20, right: 20 },
didDrawPage: function(data) {
// Header on new pages
if (data.pageNumber > 1) {
doc.setFontSize(12);
doc.setTextColor(35, 45, 83);
doc.text(section + ' (Fortsetzung)', 20, 15);
}
}
});
yPosition = doc.lastAutoTable.finalY + 10;
}
});
}
// Add footer
const pageCount = doc.internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.setTextColor(150);
doc.text(`Seite ${i} von ${pageCount}`, 105, 285, { align: 'center' });
doc.text('Metadaten-Crawler © 2025 IntelSight', 105, 290, { align: 'center' });
}
// Save the PDF
doc.save(`Metadaten-Bericht_${currentFileData.name.replace(/\.[^/.]+$/, '')}_${now.getTime()}.pdf`);
// Restore button
document.getElementById('reportBtn').textContent = originalText;
document.getElementById('reportBtn').disabled = false;
};
autoTableScript.onerror = () => {
alert('Fehler beim Laden der PDF-Tabellen-Bibliothek.');
document.getElementById('reportBtn').textContent = originalText;
document.getElementById('reportBtn').disabled = false;
};
};
jsPDFScript.onerror = () => {
alert('Fehler beim Laden der PDF-Bibliothek.');
document.getElementById('reportBtn').textContent = originalText;
document.getElementById('reportBtn').disabled = false;
};
} catch (error) {
console.error('Fehler beim Erstellen des Berichts:', error);
alert('Fehler beim Erstellen des PDF-Berichts.');
document.getElementById('reportBtn').textContent = 'Bericht erstellen';
document.getElementById('reportBtn').disabled = false;
}
}

11
build.bat Normale Datei
Datei anzeigen

@ -0,0 +1,11 @@
@echo off
echo Building Metadaten-Crawler...
echo.
echo Installing dependencies...
call npm install
echo.
echo Creating Windows executable...
call npm run build-win
echo.
echo Build complete! Check the dist folder for the executable.
pause

22
gitea_push_debug.txt Normale Datei
Datei anzeigen

@ -0,0 +1,22 @@
Push Debug Info - 2025-07-10 19:11:49.841754
Repository: Metadaten-Crawler
Owner: IntelSight
Path: C:\Users\hendr\Desktop\IntelSight\Projektablage\Metadaten-Crawler
Current branch: master
Git remotes:
origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/Metadaten-Crawler.git (fetch)
origin https://IntelSight_Admin:3b4a6ba1ade3f34640f3c85d2333b4a3a0627471@gitea-undso.intelsight.de/IntelSight/Metadaten-Crawler.git (push)
Git status before push:
Clean
Push command: git push --set-upstream origin master:main -v
Push result: Success
Push stdout:
branch 'master' set up to track 'origin/main'.
Push stderr:
POST git-receive-pack (52750 bytes)
remote: . Processing 1 references
remote: Processed 1 references in total
Pushing to https://gitea-undso.intelsight.de/IntelSight/Metadaten-Crawler.git
To https://gitea-undso.intelsight.de/IntelSight/Metadaten-Crawler.git
* [new branch] master -> main
updating local tracking ref 'refs/remotes/origin/main'

103
index.html Normale Datei
Datei anzeigen

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Metadaten-Crawler</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="title-bar">
<div class="title-bar-content">
<div class="app-title">
<span class="title-text">Metadaten-Crawler</span>
</div>
<div class="title-bar-actions">
<button class="window-control minimize" id="minimizeBtn"></button>
<button class="window-control maximize" id="maximizeBtn"></button>
<button class="window-control close" id="closeBtn">×</button>
</div>
</div>
</div>
<!-- Settings Dialog -->
<div class="settings-dialog" id="settingsDialog">
<div class="settings-content">
<div class="settings-header">
<h2>Einstellungen</h2>
<button class="settings-close" id="settingsCloseBtn">×</button>
</div>
<div class="settings-body">
<div class="settings-section">
<h3>Erscheinungsbild</h3>
<div class="theme-selector">
<label class="theme-option">
<input type="radio" name="theme" value="dark" id="darkTheme" checked>
<span class="theme-label">
<span class="theme-icon">🌙</span>
<span>Dark Mode</span>
</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="light" id="lightTheme">
<span class="theme-label">
<span class="theme-icon">☀️</span>
<span>Light Mode</span>
</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="app-header">
<div class="app-header-content">
<h1>Metadaten-Crawler</h1>
<button class="settings-btn" id="settingsBtn" title="Einstellungen">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M12 1v6m0 6v6m9-9h-6m-6 0H3m16.83-4.24l-4.24 4.24m-7.18 0L4.17 4.76m16.66 14.48l-4.24-4.24m-7.18 0L4.17 19.24"></path>
</svg>
</button>
</div>
</div>
<div class="container">
<!-- File Drop Zone -->
<div class="file-drop-zone" id="fileDropZone">
<div class="file-icon">📁</div>
<p>Datei hier ablegen</p>
<p>oder <strong>klicken</strong> zum Durchsuchen</p>
<div class="supported-formats">
Unterstützte Formate: JPEG, PNG, GIF, BMP, WEBP, MP4, AVI, MOV, MKV
</div>
<input type="file" id="fileInput" accept="image/*,video/*" style="display: none;">
</div>
<!-- Loading Spinner -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Metadaten werden ausgelesen...</p>
</div>
<!-- Metadata Display -->
<div class="metadata-container" id="metadataContainer">
<div class="file-info" id="fileInfo"></div>
<div class="image-preview" id="imagePreview"></div>
<div id="metadataContent"></div>
<div class="action-buttons">
<button class="clear-btn" onclick="clearMetadata()">Neue Datei analysieren</button>
<button class="report-btn" id="reportBtn" onclick="generateReport()">Bericht erstellen</button>
</div>
</div>
</div>
<!-- ExifReader library -->
<script src="https://cdn.jsdelivr.net/npm/exifreader@4.13.0/dist/exif-reader.min.js"></script>
<script src="app.js"></script>
<script src="renderer.js"></script>
</body>
</html>

131
main.js Normale Datei
Datei anzeigen

@ -0,0 +1,131 @@
const { app, BrowserWindow, Menu, ipcMain, nativeTheme } = require('electron');
const path = require('path');
let mainWindow;
let isDarkMode = true;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1000,
minHeight: 700,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__dirname, 'assets', 'icon.png'),
backgroundColor: '#000000',
resizable: true,
frame: false
});
mainWindow.loadFile('index.html');
const menu = Menu.buildFromTemplate([
{
label: 'Datei',
submenu: [
{
label: 'Neue Datei analysieren',
accelerator: 'CmdOrCtrl+N',
click: () => mainWindow.webContents.send('new-file')
},
{ type: 'separator' },
{
label: 'Beenden',
accelerator: 'CmdOrCtrl+Q',
click: () => app.quit()
}
]
},
{
label: 'Ansicht',
submenu: [
{
label: 'Dark Mode',
type: 'checkbox',
checked: isDarkMode,
click: (menuItem) => {
isDarkMode = menuItem.checked;
mainWindow.webContents.send('toggle-theme', isDarkMode);
nativeTheme.themeSource = isDarkMode ? 'dark' : 'light';
}
},
{ type: 'separator' },
{
label: 'Vollbild',
accelerator: 'F11',
click: () => {
mainWindow.setFullScreen(!mainWindow.isFullScreen());
}
},
{
label: 'Entwicklertools',
accelerator: 'F12',
click: () => mainWindow.webContents.toggleDevTools()
}
]
},
{
label: 'Hilfe',
submenu: [
{
label: 'Über Metadaten-Crawler',
click: () => {
const { dialog } = require('electron');
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Über Metadaten-Crawler',
message: 'Metadaten-Crawler (MC)',
detail: 'Version 1.0.0\n\nEin professionelles Tool zur Extraktion von Metadaten aus Bild- und Videodateien.\n\n© 2025 IntelSight',
buttons: ['OK']
});
}
}
]
}
]);
Menu.setApplicationMenu(menu);
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// IPC handlers
ipcMain.handle('get-theme', () => isDarkMode);
ipcMain.on('minimize-window', () => {
mainWindow.minimize();
});
ipcMain.on('maximize-window', () => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
});
ipcMain.on('close-window', () => {
mainWindow.close();
});

202
main.py Normale Datei
Datei anzeigen

@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
Metadaten-Crawler Launcher
==========================
Startet die Metadaten-Crawler Electron-Anwendung.
"""
import os
import sys
import subprocess
import platform
from pathlib import Path
class MetadatenCrawlerLauncher:
def __init__(self):
self.app_dir = Path(__file__).parent.absolute()
self.system = platform.system()
def check_node_installed(self):
"""Prüft ob Node.js installiert ist"""
try:
result = subprocess.run(['node', '--version'],
capture_output=True,
text=True)
if result.returncode == 0:
print(f"✓ Node.js gefunden: {result.stdout.strip()}")
return True
except FileNotFoundError:
pass
print("✗ Node.js ist nicht installiert!")
print(" Bitte installieren Sie Node.js von: https://nodejs.org/")
return False
def check_dependencies(self):
"""Prüft ob npm Pakete installiert sind"""
node_modules = self.app_dir / 'node_modules'
if not node_modules.exists():
print("⚠ Dependencies nicht gefunden. Installiere npm Pakete...")
return self.install_dependencies()
print("✓ Dependencies gefunden")
return True
def install_dependencies(self):
"""Installiert npm Dependencies"""
try:
print(" Führe 'npm install' aus...")
result = subprocess.run(['npm', 'install'],
cwd=self.app_dir,
capture_output=True,
text=True)
if result.returncode == 0:
print("✓ Dependencies erfolgreich installiert")
return True
else:
print(f"✗ Fehler bei der Installation: {result.stderr}")
return False
except Exception as e:
print(f"✗ Fehler: {str(e)}")
return False
def check_executable(self):
"""Prüft ob eine ausführbare Datei existiert"""
if self.system == "Windows":
exe_path = self.app_dir / 'dist' / 'Metadaten-Crawler.exe'
if exe_path.exists():
return str(exe_path)
elif self.system == "Darwin": # macOS
app_path = self.app_dir / 'dist' / 'Metadaten-Crawler.app'
if app_path.exists():
return str(app_path)
elif self.system == "Linux":
app_path = self.app_dir / 'dist' / 'Metadaten-Crawler'
if app_path.exists():
return str(app_path)
return None
def start_development(self):
"""Startet die Anwendung im Entwicklungsmodus"""
print("\n🚀 Starte Metadaten-Crawler im Entwicklungsmodus...")
try:
subprocess.run(['npm', 'start'], cwd=self.app_dir)
except KeyboardInterrupt:
print("\n\nAnwendung beendet.")
except Exception as e:
print(f"✗ Fehler beim Starten: {str(e)}")
return False
return True
def start_executable(self, exe_path):
"""Startet die kompilierte Anwendung"""
print(f"\n🚀 Starte Metadaten-Crawler von: {exe_path}")
try:
if self.system == "Windows":
subprocess.run([exe_path])
elif self.system == "Darwin":
subprocess.run(['open', exe_path])
else:
subprocess.run([exe_path])
except Exception as e:
print(f"✗ Fehler beim Starten: {str(e)}")
return False
return True
def build_application(self):
"""Baut die Anwendung"""
print("\n🔨 Baue Metadaten-Crawler...")
try:
if self.system == "Windows":
cmd = ['npm', 'run', 'build-win']
elif self.system == "Darwin":
cmd = ['npm', 'run', 'build-mac']
else:
cmd = ['npm', 'run', 'build-linux']
result = subprocess.run(cmd, cwd=self.app_dir)
if result.returncode == 0:
print("✓ Build erfolgreich abgeschlossen")
return True
else:
print("✗ Build fehlgeschlagen")
return False
except Exception as e:
print(f"✗ Fehler beim Build: {str(e)}")
return False
def run(self):
"""Hauptmethode zum Starten der Anwendung"""
print("=" * 50)
print(" Metadaten-Crawler Launcher")
print("=" * 50)
print(f"System: {self.system}")
print(f"Arbeitsverzeichnis: {self.app_dir}")
print()
# Prüfe Node.js
if not self.check_node_installed():
return False
# Prüfe Dependencies
if not self.check_dependencies():
return False
# Prüfe ob ausführbare Datei existiert
exe_path = self.check_executable()
if len(sys.argv) > 1:
# Command line arguments
if sys.argv[1] == '--dev':
return self.start_development()
elif sys.argv[1] == '--build':
return self.build_application()
elif sys.argv[1] == '--exe' and exe_path:
return self.start_executable(exe_path)
# Interaktives Menü
print("\nWas möchten Sie tun?")
print("1. Entwicklungsmodus starten (npm start)")
if exe_path:
print("2. Kompilierte Anwendung starten")
print("3. Anwendung neu bauen")
else:
print("2. Anwendung bauen (erstellt .exe)")
print("0. Beenden")
while True:
try:
choice = input("\nIhre Wahl: ").strip()
if choice == '0':
print("Auf Wiedersehen!")
return True
elif choice == '1':
return self.start_development()
elif choice == '2':
if exe_path:
return self.start_executable(exe_path)
else:
if self.build_application():
exe_path = self.check_executable()
if exe_path:
print("\n✓ Build erfolgreich! Möchten Sie die Anwendung starten? (j/n)")
if input().lower() == 'j':
return self.start_executable(exe_path)
elif choice == '3' and exe_path:
return self.build_application()
else:
print("Ungültige Eingabe. Bitte erneut versuchen.")
except KeyboardInterrupt:
print("\n\nProgramm beendet.")
return True
def main():
"""Hauptfunktion"""
launcher = MetadatenCrawlerLauncher()
launcher.run()
if __name__ == "__main__":
main()

4326
package-lock.json generiert Normale Datei

Datei-Diff unterdrückt, da er zu groß ist Diff laden

39
package.json Normale Datei
Datei anzeigen

@ -0,0 +1,39 @@
{
"name": "metadaten-crawler",
"version": "1.0.0",
"description": "Metadaten-Crawler (MC) - Professional metadata extraction tool",
"main": "main.js",
"scripts": {
"start": "electron .",
"build-win": "electron-builder --win",
"build": "electron-builder",
"dist": "electron-builder"
},
"keywords": ["metadata", "exif", "crawler"],
"author": "IntelSight",
"license": "MIT",
"dependencies": {
"jspdf": "^2.5.1",
"html2canvas": "^1.4.1"
},
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.6.4"
},
"build": {
"appId": "com.intelsight.metadatencrawler",
"productName": "Metadaten-Crawler",
"directories": {
"output": "dist"
},
"win": {
"target": "nsis",
"icon": "assets/icon.ico"
},
"nsis": {
"oneClick": false,
"perMachine": true,
"allowToChangeInstallationDirectory": true
}
}
}

10
preload.js Normale Datei
Datei anzeigen

@ -0,0 +1,10 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onNewFile: (callback) => ipcRenderer.on('new-file', callback),
onToggleTheme: (callback) => ipcRenderer.on('toggle-theme', callback),
getTheme: () => ipcRenderer.invoke('get-theme'),
minimizeWindow: () => ipcRenderer.send('minimize-window'),
maximizeWindow: () => ipcRenderer.send('maximize-window'),
closeWindow: () => ipcRenderer.send('close-window')
});

79
renderer.js Normale Datei
Datei anzeigen

@ -0,0 +1,79 @@
// Theme management
let isDarkMode = true;
// Initialize theme
async function initializeTheme() {
if (window.electronAPI) {
isDarkMode = await window.electronAPI.getTheme();
applyTheme(isDarkMode);
}
}
// Apply theme
function applyTheme(darkMode) {
isDarkMode = darkMode;
if (darkMode) {
document.body.classList.remove('light-mode');
document.getElementById('darkTheme').checked = true;
} else {
document.body.classList.add('light-mode');
document.getElementById('lightTheme').checked = true;
}
}
// Window controls
document.getElementById('minimizeBtn').addEventListener('click', () => {
if (window.electronAPI) window.electronAPI.minimizeWindow();
});
document.getElementById('maximizeBtn').addEventListener('click', () => {
if (window.electronAPI) window.electronAPI.maximizeWindow();
});
document.getElementById('closeBtn').addEventListener('click', () => {
if (window.electronAPI) window.electronAPI.closeWindow();
});
// Settings dialog
const settingsDialog = document.getElementById('settingsDialog');
const settingsBtn = document.getElementById('settingsBtn');
const settingsCloseBtn = document.getElementById('settingsCloseBtn');
settingsBtn.addEventListener('click', () => {
settingsDialog.classList.add('show');
});
settingsCloseBtn.addEventListener('click', () => {
settingsDialog.classList.remove('show');
});
settingsDialog.addEventListener('click', (e) => {
if (e.target === settingsDialog) {
settingsDialog.classList.remove('show');
}
});
// Theme radio buttons
document.getElementById('darkTheme').addEventListener('change', () => {
applyTheme(true);
});
document.getElementById('lightTheme').addEventListener('change', () => {
applyTheme(false);
});
// Listen for theme changes from menu
if (window.electronAPI) {
window.electronAPI.onToggleTheme((event, darkMode) => {
applyTheme(darkMode);
});
window.electronAPI.onNewFile(() => {
clearMetadata();
});
}
// Initialize on load
window.addEventListener('DOMContentLoaded', () => {
initializeTheme();
});

2
requirements.txt Normale Datei
Datei anzeigen

@ -0,0 +1,2 @@
# Keine Python-Dependencies erforderlich
# Die main.py nutzt nur Standard-Python-Module

620
styles.css Normale Datei
Datei anzeigen

@ -0,0 +1,620 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
:root {
--primary: #232D53;
--accent: #00D4FF;
--accent-hover: #00B8E6;
--background: #000000;
--secondary-bg: #1A1F3A;
--secondary-bg-hover: #232D53;
--text-primary: #FFFFFF;
--text-secondary: rgba(255, 255, 255, 0.7);
--text-tertiary: rgba(255, 255, 255, 0.6);
--error: #FF4444;
--success: #4CAF50;
--warning: #FFC107;
--info: #2196F3;
--sidebar-bg: #0A0A0A;
--border-color: rgba(255, 255, 255, 0.1);
--input-focus: #2A3560;
}
body.light-mode {
--primary: #3182CE;
--accent: #3182CE;
--accent-hover: #2563EB;
--background: #FFFFFF;
--secondary-bg: #F8FAFC;
--secondary-bg-hover: #E1E8F0;
--text-primary: #1A202C;
--text-secondary: #4A5568;
--text-tertiary: #718096;
--error: #E53E3E;
--success: #38A169;
--warning: #D69E2E;
--info: #3182CE;
--sidebar-bg: #F7FAFC;
--border-color: #E1E8F0;
--input-focus: #EBF8FF;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
background-color: var(--background);
color: var(--text-primary);
margin: 0;
padding: 0;
transition: background-color 0.3s ease, color 0.3s ease;
overflow-x: hidden;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
}
/* App Header */
.app-header {
background-color: var(--secondary-bg);
border-bottom: 2px solid var(--accent);
margin-top: 40px;
padding: 20px 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.app-header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
h1 {
font-family: 'Poppins', sans-serif;
font-weight: 700;
font-size: 32px;
letter-spacing: 1px;
margin: 0;
color: var(--text-primary);
}
.file-drop-zone {
min-height: 280px;
background-color: var(--secondary-bg);
border: 3px dashed var(--primary);
border-radius: 16px;
padding: 60px 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 30px;
margin-top: 30px;
position: relative;
overflow: hidden;
}
.file-drop-zone:hover {
border-color: var(--accent);
background-color: var(--secondary-bg-hover);
transform: translateY(-2px);
}
.file-drop-zone.drag-over {
background-color: var(--secondary-bg-hover);
border-color: var(--accent);
transform: scale(1.02);
}
.file-drop-zone p {
margin: 0 0 10px 0;
font-size: 18px;
color: var(--text-primary);
font-family: 'Poppins', sans-serif;
font-weight: 500;
}
.file-drop-zone .file-icon {
font-size: 64px;
color: var(--accent);
margin-bottom: 20px;
filter: drop-shadow(0 0 20px rgba(0, 212, 255, 0.3));
}
.supported-formats {
font-size: 14px;
color: var(--text-tertiary);
margin-top: 20px;
}
.metadata-container {
background-color: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 32px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
display: none;
animation: fadeIn 0.3s ease;
}
.metadata-container.active {
display: block;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.file-info {
background-color: var(--primary);
padding: 24px;
border-radius: 12px;
margin-bottom: 24px;
}
.file-info h3 {
margin: 0 0 16px 0;
color: var(--text-primary);
font-family: 'Poppins', sans-serif;
font-weight: 600;
font-size: 20px;
}
.file-info .basic-info {
display: grid;
grid-template-columns: auto 1fr;
gap: 12px;
font-size: 14px;
}
.file-info .label {
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 12px;
}
.metadata-section {
margin-bottom: 24px;
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
}
.metadata-section:hover {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent);
}
.metadata-section-header {
background-color: var(--primary);
color: var(--text-primary);
padding: 16px 20px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.3s;
font-family: 'Poppins', sans-serif;
font-weight: 600;
letter-spacing: 0.5px;
}
.metadata-section-header:hover {
background-color: var(--secondary-bg-hover);
}
.metadata-section-header.video {
background-color: var(--info);
}
.toggle-icon {
transition: transform 0.3s;
color: var(--accent);
}
.metadata-section.collapsed .toggle-icon {
transform: rotate(-90deg);
}
.metadata-content {
padding: 20px;
background-color: rgba(26, 31, 58, 0.3);
max-height: 500px;
overflow-y: auto;
}
body.light-mode .metadata-content {
background-color: rgba(248, 250, 252, 0.5);
}
.metadata-section.collapsed .metadata-content {
display: none;
}
.metadata-table {
width: 100%;
border-collapse: collapse;
}
.metadata-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
vertical-align: top;
}
.metadata-table tr:last-child td {
border-bottom: none;
}
.metadata-table td:first-child {
font-weight: 600;
color: var(--text-secondary);
width: 40%;
background-color: rgba(35, 45, 83, 0.2);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
body.light-mode .metadata-table td:first-child {
background-color: rgba(49, 130, 206, 0.05);
}
.metadata-table td:last-child {
word-break: break-word;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
color: var(--text-primary);
}
.image-preview {
text-align: center;
margin-bottom: 24px;
}
.image-preview img {
max-width: 400px;
max-height: 400px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
border: 1px solid var(--border-color);
}
.loading {
display: none;
text-align: center;
padding: 40px;
}
.loading.active {
display: block;
}
.spinner {
border: 4px solid var(--secondary-bg);
border-top: 4px solid var(--accent);
border-radius: 50%;
width: 48px;
height: 48px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.clear-btn {
background-color: var(--accent);
color: var(--background);
border: none;
padding: 12px 32px;
border-radius: 24px;
cursor: pointer;
font-size: 16px;
font-family: 'Poppins', sans-serif;
font-weight: 600;
margin-top: 24px;
transition: all 0.3s ease;
letter-spacing: 0.5px;
}
.clear-btn:hover {
background-color: var(--accent-hover);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 212, 255, 0.3);
}
.no-metadata {
text-align: center;
color: var(--text-tertiary);
padding: 40px;
font-style: italic;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--secondary-bg);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--accent);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-hover);
}
/* Title Bar */
.title-bar {
height: 40px;
background: linear-gradient(90deg, #00D4FF 0%, #232D53 100%);
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
-webkit-app-region: drag;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
body.light-mode .title-bar {
background: linear-gradient(90deg, #3182CE 0%, #2563EB 100%);
}
.title-bar-content {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
}
.app-title {
display: flex;
align-items: center;
gap: 10px;
font-family: 'Poppins', sans-serif;
font-weight: 700;
font-size: 16px;
color: #FFFFFF;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
letter-spacing: 1px;
}
.title-bar-actions {
display: flex;
align-items: center;
gap: 8px;
-webkit-app-region: no-drag;
}
.settings-btn {
background-color: var(--secondary-bg);
border: 1px solid var(--border-color);
color: var(--accent);
cursor: pointer;
padding: 10px;
border-radius: 12px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.settings-btn:hover {
background-color: var(--secondary-bg-hover);
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.2);
}
.window-control {
background: transparent;
border: none;
color: #FFFFFF;
cursor: pointer;
padding: 0;
width: 46px;
height: 40px;
font-size: 16px;
transition: all 0.3s ease;
-webkit-app-region: no-drag;
}
.window-control:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.window-control.close:hover {
background-color: var(--error);
color: white;
}
/* Settings Dialog */
.settings-dialog {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
z-index: 2000;
align-items: center;
justify-content: center;
}
.settings-dialog.show {
display: flex;
}
.settings-content {
background-color: var(--secondary-bg);
border-radius: 16px;
width: 500px;
max-width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
overflow: hidden;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.settings-header {
background-color: var(--primary);
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.settings-header h2 {
margin: 0;
font-family: 'Poppins', sans-serif;
font-weight: 600;
font-size: 20px;
color: var(--text-primary);
}
.settings-close {
background: transparent;
border: none;
color: var(--text-primary);
font-size: 24px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
border-radius: 8px;
transition: all 0.3s ease;
}
.settings-close:hover {
background-color: var(--error);
}
.settings-body {
padding: 24px;
}
.settings-section {
margin-bottom: 24px;
}
.settings-section h3 {
margin: 0 0 16px 0;
font-family: 'Poppins', sans-serif;
font-weight: 600;
font-size: 16px;
color: var(--text-primary);
}
.theme-selector {
display: flex;
gap: 16px;
}
.theme-option {
flex: 1;
cursor: pointer;
}
.theme-option input[type="radio"] {
display: none;
}
.theme-label {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
background-color: var(--background);
border: 2px solid var(--border-color);
border-radius: 12px;
transition: all 0.3s ease;
font-weight: 500;
}
.theme-option input[type="radio"]:checked + .theme-label {
border-color: var(--accent);
background-color: var(--secondary-bg-hover);
}
.theme-label:hover {
border-color: var(--accent);
}
.theme-icon {
font-size: 24px;
}
/* Adjust container for spacing */
.container {
padding-top: 20px;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 16px;
margin-top: 24px;
}
.report-btn {
background-color: var(--info);
color: white;
border: none;
padding: 12px 32px;
border-radius: 24px;
cursor: pointer;
font-size: 16px;
font-family: 'Poppins', sans-serif;
font-weight: 600;
transition: all 0.3s ease;
letter-spacing: 0.5px;
}
.report-btn:hover {
background-color: #1976D2;
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(33, 150, 243, 0.3);
}