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