Initial commit
Dieser Commit ist enthalten in:
11
.claude/settings.local.json
Normale Datei
11
.claude/settings.local.json
Normale Datei
@ -0,0 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(npm install)",
|
||||
"Bash(npm run build-win:*)",
|
||||
"Bash(chmod:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
5
.gitignore
vendored
Normale Datei
5
.gitignore
vendored
Normale Datei
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
89
CLAUDE_PROJECT_README.md
Normale Datei
89
CLAUDE_PROJECT_README.md
Normale Datei
@ -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
57
README.md
Normale Datei
@ -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
914
app.js
Normale Datei
@ -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
11
build.bat
Normale Datei
@ -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
22
gitea_push_debug.txt
Normale Datei
@ -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
103
index.html
Normale Datei
@ -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
131
main.js
Normale Datei
@ -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
202
main.py
Normale Datei
@ -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
4326
package-lock.json
generiert
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
39
package.json
Normale Datei
39
package.json
Normale Datei
@ -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
10
preload.js
Normale Datei
@ -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
79
renderer.js
Normale Datei
@ -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
2
requirements.txt
Normale Datei
@ -0,0 +1,2 @@
|
||||
# Keine Python-Dependencies erforderlich
|
||||
# Die main.py nutzt nur Standard-Python-Module
|
||||
620
styles.css
Normale Datei
620
styles.css
Normale Datei
@ -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);
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren