feat: add fullscreen photo viewer overlay on click & resolve appearance compat warnings

This commit is contained in:
2026-06-06 20:40:13 +02:00
parent 318f5e65da
commit 878be33b7c
2 changed files with 129 additions and 3 deletions
+74
View File
@@ -3184,6 +3184,7 @@ html.theme-cupertino .events-scroll-container {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
background: #0b0c10; background: #0b0c10;
overflow: hidden; overflow: hidden;
cursor: pointer;
} }
.photo-container img { .photo-container img {
@@ -3230,6 +3231,78 @@ html.theme-cupertino .events-scroll-container {
white-space: nowrap; white-space: nowrap;
} }
/* Photo Maximized Overlay */
.photo-maximized-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(11, 12, 16, 0.9);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
display: flex;
align-items: center;
justify-content: center;
z-index: 11000;
}
.photo-maximized-container {
position: relative;
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.photo-maximized-img {
max-width: 90vw;
max-height: 80vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.photo-maximized-close {
position: absolute;
top: -48px;
right: 0;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #f1f5f9;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.photo-maximized-close:hover {
background: rgba(255, 255, 255, 0.2);
border-color: #ffffff;
transform: scale(1.08);
}
.photo-maximized-caption {
font-size: 15px;
color: #f1f5f9;
background: rgba(15, 23, 42, 0.75);
padding: 8px 16px;
border-radius: 20px;
max-width: 80%;
text-align: center;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.05);
word-break: break-word;
}
/* Custom Dialog Modals Styling */ /* Custom Dialog Modals Styling */
.custom-dialog-overlay { .custom-dialog-overlay {
position: fixed; position: fixed;
@@ -4364,6 +4437,7 @@ html.theme-cupertino .events-scroll-container {
.consumption-grid .input-group .input-text { .consumption-grid .input-group .input-text {
flex-shrink: 0; flex-shrink: 0;
-moz-appearance: textfield; -moz-appearance: textfield;
appearance: textfield;
} }
.consumption-grid .input-text::-webkit-outer-spin-button, .consumption-grid .input-text::-webkit-outer-spin-button,
+55 -3
View File
@@ -8,7 +8,7 @@ import { saveEntryPhoto, deleteEntryPhoto } from '../services/photoAttachments.j
import { fileToCompressedJpegDataUrl } from '../utils/imageCompress.js' import { fileToCompressedJpegDataUrl } from '../utils/imageCompress.js'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { useDialog } from './ModalDialog.tsx' import { useDialog } from './ModalDialog.tsx'
import { Camera, Image, Trash2 } from 'lucide-react' import { Camera, Image, Trash2, X } from 'lucide-react'
import { probeCameraAvailability } from '../utils/cameraAvailability.js' import { probeCameraAvailability } from '../utils/cameraAvailability.js'
interface PhotoCaptureProps { interface PhotoCaptureProps {
@@ -33,10 +33,26 @@ export default function PhotoCapture({ entryId, logbookId, readOnly = false, pre
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [decryptedPhotos, setDecryptedPhotos] = useState<DecryptedPhoto[]>([]) const [decryptedPhotos, setDecryptedPhotos] = useState<DecryptedPhoto[]>([])
const [hasCamera, setHasCamera] = useState(false) const [hasCamera, setHasCamera] = useState(false)
const [maximizedPhoto, setMaximizedPhoto] = useState<DecryptedPhoto | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const cameraInputRef = useRef<HTMLInputElement>(null) const cameraInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (!maximizedPhoto) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setMaximizedPhoto(null)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
}
}, [maximizedPhoto])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
probeCameraAvailability().then((avail) => { probeCameraAvailability().then((avail) => {
@@ -246,14 +262,22 @@ export default function PhotoCapture({ entryId, logbookId, readOnly = false, pre
) : ( ) : (
<div className="photo-attachments-grid"> <div className="photo-attachments-grid">
{decryptedPhotos.map((photo) => ( {decryptedPhotos.map((photo) => (
<div key={photo.payloadId} className="photo-card glass"> <div
key={photo.payloadId}
className="photo-card glass"
onClick={() => setMaximizedPhoto(photo)}
style={{ cursor: 'pointer' }}
>
<div className="photo-container"> <div className="photo-container">
<img src={photo.image} alt={photo.caption || 'Attachment'} loading="lazy" /> <img src={photo.image} alt={photo.caption || 'Attachment'} loading="lazy" />
{!readOnly && ( {!readOnly && (
<button <button
type="button" type="button"
className="photo-btn-delete" className="photo-btn-delete"
onClick={() => handleDelete(photo.payloadId)} onClick={(e) => {
e.stopPropagation()
handleDelete(photo.payloadId)
}}
title="Remove photo" title="Remove photo"
> >
<Trash2 size={16} /> <Trash2 size={16} />
@@ -269,6 +293,34 @@ export default function PhotoCapture({ entryId, logbookId, readOnly = false, pre
))} ))}
</div> </div>
)} )}
{maximizedPhoto && (
<div
className="photo-maximized-overlay"
onClick={() => setMaximizedPhoto(null)}
>
<div className="photo-maximized-container" onClick={(e) => e.stopPropagation()}>
<button
type="button"
className="photo-maximized-close"
onClick={() => setMaximizedPhoto(null)}
aria-label={t('common.close') || 'Close'}
>
<X size={24} />
</button>
<img
src={maximizedPhoto.image}
alt={maximizedPhoto.caption || 'Maximized Attachment'}
className="photo-maximized-img"
/>
{maximizedPhoto.caption && (
<div className="photo-maximized-caption">
{maximizedPhoto.caption}
</div>
)}
</div>
</div>
)}
</div> </div>
) )
} }