Add user authentication and registration features
- Implement login and registration HTML templates for user authentication. - Create user management functions in user.py for handling user data, including adding, deleting, and updating users. - Introduce password hashing and strength checking for secure user credentials. - Add terms of service and privacy policy pages to the website. - Enhance main page with dynamic content based on user session status.
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Mobile Compatibility Utilities
|
||||
*
|
||||
* This module provides utility functions for better mobile device compatibility,
|
||||
* especially for iOS devices where certain browser APIs have limited support.
|
||||
*
|
||||
* Copyright 2025 Maximilian Gründinger
|
||||
* Licensed under the Apache License, Version 2.0
|
||||
*/
|
||||
|
||||
// Detect mobile devices including tablets
|
||||
function isMobileDevice() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
// Specifically detect iOS devices
|
||||
function isIOSDevice() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
}
|
||||
|
||||
// Mobile-compatible file tracker to replace DataTransfer API
|
||||
class MobileCompatFileTracker {
|
||||
constructor() {
|
||||
this.files = [];
|
||||
this.removedIndices = new Set();
|
||||
}
|
||||
|
||||
// Add files from a FileList
|
||||
addFiles(fileList) {
|
||||
// Convert FileList to Array for easier handling
|
||||
Array.from(fileList).forEach(file => {
|
||||
this.files.push(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove a file by index
|
||||
removeFile(index) {
|
||||
if (index >= 0 && index < this.files.length) {
|
||||
this.removedIndices.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all non-removed files
|
||||
getFiles() {
|
||||
return this.files.filter((_, index) => !this.removedIndices.has(index));
|
||||
}
|
||||
|
||||
// Convert to FormData for upload
|
||||
appendToFormData(formData, fieldName) {
|
||||
this.getFiles().forEach(file => {
|
||||
formData.append(fieldName, file);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear all files
|
||||
clear() {
|
||||
this.files = [];
|
||||
this.removedIndices.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile optimized image preview generator
|
||||
class ImagePreviewGenerator {
|
||||
constructor(options = {}) {
|
||||
this.maxWidth = options.maxWidth || 800;
|
||||
this.maxHeight = options.maxHeight || 600;
|
||||
this.quality = options.quality || 0.8;
|
||||
this.maxPreviewsOnMobile = options.maxPreviewsOnMobile || 3;
|
||||
}
|
||||
|
||||
// Create optimized image preview element
|
||||
createPreview(file, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!file) {
|
||||
reject(new Error('No file provided'));
|
||||
return;
|
||||
}
|
||||
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
|
||||
if (isVideo) {
|
||||
this._createVideoPreview(file, index).then(resolve).catch(reject);
|
||||
} else if (file.type.startsWith('image/')) {
|
||||
this._createImagePreview(file, index).then(resolve).catch(reject);
|
||||
} else {
|
||||
reject(new Error('Unsupported file type'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create image preview with optimization for mobile
|
||||
_createImagePreview(file, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
// For mobile, we'll compress the image before creating the preview
|
||||
if (isMobileDevice()) {
|
||||
this._compressImage(e.target.result)
|
||||
.then(compressedDataUrl => {
|
||||
const previewElement = this._createPreviewElement(compressedDataUrl, file.name, index, false);
|
||||
resolve(previewElement);
|
||||
})
|
||||
.catch(err => {
|
||||
// Fallback to original if compression fails
|
||||
console.warn('Image compression failed, using original:', err);
|
||||
const previewElement = this._createPreviewElement(e.target.result, file.name, index, false);
|
||||
resolve(previewElement);
|
||||
});
|
||||
} else {
|
||||
// For desktop, use the original image
|
||||
const previewElement = this._createPreviewElement(e.target.result, file.name, index, false);
|
||||
resolve(previewElement);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Create video preview with thumbnail generation
|
||||
_createVideoPreview(file, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const videoUrl = URL.createObjectURL(file);
|
||||
const video = document.createElement('video');
|
||||
|
||||
video.src = videoUrl;
|
||||
video.onloadedmetadata = () => {
|
||||
// Create preview element
|
||||
const previewElement = this._createPreviewElement(videoUrl, file.name, index, true);
|
||||
resolve(previewElement);
|
||||
|
||||
// Revoke object URL after a delay to ensure it's loaded
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(videoUrl);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
video.onerror = () => {
|
||||
URL.revokeObjectURL(videoUrl);
|
||||
reject(new Error('Failed to load video'));
|
||||
};
|
||||
|
||||
// Set a timeout in case the video never loads
|
||||
setTimeout(() => {
|
||||
if (!video.videoWidth) {
|
||||
URL.revokeObjectURL(videoUrl);
|
||||
reject(new Error('Video load timeout'));
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// Create HTML element for preview
|
||||
_createPreviewElement(src, fileName, index, isVideo) {
|
||||
const previewDiv = document.createElement('div');
|
||||
previewDiv.className = 'image-preview-item';
|
||||
|
||||
if (isVideo) {
|
||||
const video = document.createElement('video');
|
||||
video.src = src;
|
||||
video.controls = true;
|
||||
video.preload = 'metadata';
|
||||
video.style.maxWidth = '150px';
|
||||
video.style.maxHeight = '150px';
|
||||
video.style.objectFit = 'cover';
|
||||
previewDiv.appendChild(video);
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
img.alt = fileName;
|
||||
img.style.maxWidth = '150px';
|
||||
img.style.maxHeight = '150px';
|
||||
img.style.objectFit = 'cover';
|
||||
previewDiv.appendChild(img);
|
||||
}
|
||||
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'image-controls';
|
||||
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.type = 'button';
|
||||
removeButton.textContent = 'Entfernen';
|
||||
removeButton.style.background = '#dc3545';
|
||||
removeButton.style.color = 'white';
|
||||
removeButton.style.border = 'none';
|
||||
removeButton.style.padding = '5px 10px';
|
||||
removeButton.style.borderRadius = '3px';
|
||||
removeButton.style.cursor = 'pointer';
|
||||
removeButton.style.marginLeft = '10px';
|
||||
removeButton.dataset.index = index;
|
||||
removeButton.addEventListener('click', function() {
|
||||
const indexToRemove = parseInt(this.dataset.index, 10);
|
||||
// We'll define removeImagePreview elsewhere
|
||||
if (typeof window.removeImagePreview === 'function') {
|
||||
window.removeImagePreview(indexToRemove);
|
||||
}
|
||||
});
|
||||
|
||||
controlsDiv.appendChild(removeButton);
|
||||
previewDiv.appendChild(controlsDiv);
|
||||
|
||||
return previewDiv;
|
||||
}
|
||||
|
||||
// Compress image for mobile devices
|
||||
_compressImage(dataUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
// Scale down if needed
|
||||
if (width > this.maxWidth || height > this.maxHeight) {
|
||||
const ratio = Math.min(this.maxWidth / width, this.maxHeight / height);
|
||||
width *= ratio;
|
||||
height *= ratio;
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Get compressed data URL
|
||||
const compressedDataUrl = canvas.toDataURL('image/jpeg', this.quality);
|
||||
|
||||
// Clean up
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
|
||||
resolve(compressedDataUrl);
|
||||
};
|
||||
|
||||
img.onerror = () => reject(new Error('Failed to load image for compression'));
|
||||
img.src = dataUrl;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile-optimized debounce function for event handlers
|
||||
function debounce(func, wait = 300) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Mobile-safe promise-based setTimeout
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Log mobile-specific issues to console or server
|
||||
function logMobileIssue(action, error) {
|
||||
if (isMobileDevice()) {
|
||||
const diagnosticInfo = {
|
||||
action: action,
|
||||
error: error.message,
|
||||
browser: navigator.userAgent,
|
||||
timestamp: new Date().toISOString(),
|
||||
memoryInfo: performance?.memory ? {
|
||||
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
||||
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||
usedJSHeapSize: performance.memory.usedJSHeapSize
|
||||
} : 'Not available'
|
||||
};
|
||||
|
||||
console.error('Mobile Issue:', diagnosticInfo);
|
||||
|
||||
// Optionally send to server
|
||||
fetch('/log_mobile_issue', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(diagnosticInfo)
|
||||
}).catch(() => {
|
||||
// Silently fail if logging fails
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user