Redesign EXIF viewer and compact tools UI

EXIF Viewer:
- Card-based grid layout with categories (Camera, Image, Date/Time, Exposure, GPS, Other)
- Icons for each category
- Truncation for long values with full value on hover

Tools UI:
- Reduced padding from 1.25rem to 0.5rem on all tool panels
- Smaller fonts for labels (0.55rem) and values (0.7rem)
- Compact headers and action buttons
- Tighter grid gaps and card padding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-11 16:50:59 -05:00
parent 4e3acfca20
commit 38bef32750
4 changed files with 143 additions and 31 deletions

View File

@@ -1,7 +1,9 @@
# Stegasoo 4.2.1 Plan # Stegasoo 4.2.1 Plan
## Bugs ## Bugs
- [ ] Fix EXIF viewer panel not loading metadata in Web UI - [x] Fix EXIF viewer panel not loading metadata in Web UI
- Redesigned with card-based grid layout and categories
- Compact styling for better space usage
- [x] DCT mode: portrait photos export rotated 90° (EXIF orientation not handled) - [x] DCT mode: portrait photos export rotated 90° (EXIF orientation not handled)
- Added `_apply_exif_orientation()` to apply EXIF rotation before embedding - Added `_apply_exif_orientation()` to apply EXIF rotation before embedding
- [x] DCT mode: add rotation fallback (try as-is, rotate 90°, retry on failure) - [x] DCT mode: add rotation fallback (try as-is, rotate 90°, retry on failure)
@@ -12,7 +14,10 @@
- Dynamic UI shows "DCT Safe" for JPEGs, warning for other formats - Dynamic UI shows "DCT Safe" for JPEGs, warning for other formats
## Tools Audit ## Tools Audit
- [ ] Web UI tools - full shakedown and fixes - [x] Web UI tools - full shakedown and fixes
- Compress, Rotate, Strip, EXIF viewer all working
- Rotate uses jpegtran for lossless JPEG rotation
- Compact UI styling
- [ ] CLI tools - full shakedown and fixes - [ ] CLI tools - full shakedown and fixes
## AUR Packages ## AUR Packages

View File

@@ -1,6 +1,6 @@
# Maintainer: Aaron D. Lee <your-email@example.com> # Maintainer: Aaron D. Lee <your-email@example.com>
pkgname=stegasoo-git pkgname=stegasoo-git
pkgver=4.2.0.r0.g530e5de pkgver=4.2.0.r0.g2ebc42f
pkgrel=1 pkgrel=1
pkgdesc="Secure steganography with hybrid photo + passphrase + PIN authentication" pkgdesc="Secure steganography with hybrid photo + passphrase + PIN authentication"
arch=('x86_64') arch=('x86_64')

View File

@@ -2247,7 +2247,7 @@ footer {
display: none; display: none;
width: 100%; width: 100%;
flex: 1; flex: 1;
padding: 1.25rem; padding: 0.5rem;
} }
.tool-section.active { .tool-section.active {
@@ -2255,33 +2255,92 @@ footer {
flex-direction: column; flex-direction: column;
} }
/* EXIF Table in Results */ /* EXIF Grid Layout */
.tool-exif-table { .exif-grid {
font-size: 0.8rem; display: grid;
max-height: 250px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.3rem;
max-height: 280px;
overflow-y: auto; overflow-y: auto;
padding: 0.15rem;
} }
.tool-exif-table table { .exif-card {
width: 100%; background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 4px;
padding: 0.25rem 0.4rem;
} }
.tool-exif-table th, .exif-card:hover {
.tool-exif-table td { background: rgba(255, 255, 255, 0.06);
padding: 0.35rem 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
} }
.tool-exif-table th { .exif-card-label {
font-size: 0.55rem;
font-weight: 500; font-weight: 500;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.4);
text-align: left; text-transform: uppercase;
width: 40%; letter-spacing: 0.02em;
margin-bottom: 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.tool-exif-table td { .exif-card-value {
font-size: 0.7rem;
font-family: 'SF Mono', 'Consolas', monospace; font-family: 'SF Mono', 'Consolas', monospace;
word-break: break-all; color: rgba(255, 255, 255, 0.85);
word-break: break-word;
line-height: 1.2;
}
.exif-card-value.truncated {
max-height: 2.4em;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* Category headers */
.exif-category {
grid-column: 1 / -1;
font-size: 0.6rem;
font-weight: 600;
color: var(--bs-primary);
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.35rem 0 0.15rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-top: 0.15rem;
}
.exif-category:first-child {
margin-top: 0;
padding-top: 0;
}
/* Compact tool headers and actions */
.tool-results-header {
padding-bottom: 0.35rem;
margin-bottom: 0.35rem;
}
.tool-results-header h6 {
font-size: 0.8rem;
margin-bottom: 0;
}
.tool-results-header small {
font-size: 0.65rem;
}
.tool-results-actions {
padding-top: 0.35rem;
margin-top: 0.35rem;
} }
/* Loading State */ /* Loading State */

View File

@@ -283,10 +283,8 @@
<span>Drop an image to view metadata</span> <span>Drop an image to view metadata</span>
</div> </div>
<div id="exifData" class="d-none"> <div id="exifData" class="d-none">
<div class="tool-exif-table"> <div class="exif-grid" id="exifGrid">
<table> <!-- Cards populated by JS -->
<tbody id="exifTable"></tbody>
</table>
</div> </div>
<div id="exifNoData" class="text-muted text-center py-3 d-none"> <div id="exifNoData" class="text-muted text-center py-3 d-none">
<i class="bi bi-inbox d-block mb-2"></i> <i class="bi bi-inbox d-block mb-2"></i>
@@ -657,20 +655,70 @@ setupDropZone('exifZone', 'exifFile', async (file) => {
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
const tbody = document.getElementById('exifTable'); const grid = document.getElementById('exifGrid');
const entries = Object.entries(data.exif).sort((a, b) => a[0].localeCompare(b[0])); const entries = Object.entries(data.exif);
if (entries.length === 0) { if (entries.length === 0) {
tbody.innerHTML = ''; grid.innerHTML = '';
document.getElementById('exifNoData').classList.remove('d-none'); document.getElementById('exifNoData').classList.remove('d-none');
document.getElementById('exifNoData').innerHTML = '<i class="bi bi-inbox d-block mb-2"></i>No metadata found'; document.getElementById('exifNoData').innerHTML = '<i class="bi bi-inbox d-block mb-2"></i>No metadata found';
} else { } else {
document.getElementById('exifNoData').classList.add('d-none'); document.getElementById('exifNoData').classList.add('d-none');
tbody.innerHTML = entries.map(([key, value]) => {
// Categorize EXIF fields
const categories = {
'Camera': ['Make', 'Model', 'Software', 'LensMake', 'LensModel', 'BodySerialNumber'],
'Image': ['ImageWidth', 'ImageLength', 'Orientation', 'ResolutionUnit', 'XResolution', 'YResolution', 'ColorSpace', 'ExifImageWidth', 'ExifImageHeight'],
'Date/Time': ['DateTime', 'DateTimeOriginal', 'DateTimeDigitized', 'SubsecTime', 'SubsecTimeOriginal', 'SubsecTimeDigitized', 'OffsetTime', 'OffsetTimeOriginal'],
'Exposure': ['ExposureTime', 'FNumber', 'ExposureProgram', 'ISOSpeedRatings', 'ExposureBiasValue', 'MaxApertureValue', 'MeteringMode', 'Flash', 'FocalLength', 'FocalLengthIn35mmFilm', 'WhiteBalance', 'ExposureMode', 'DigitalZoomRatio', 'SceneCaptureType', 'Contrast', 'Saturation', 'Sharpness'],
'GPS': ['GPSInfo', 'GPSLatitude', 'GPSLatitudeRef', 'GPSLongitude', 'GPSLongitudeRef', 'GPSAltitude', 'GPSAltitudeRef', 'GPSTimeStamp', 'GPSDateStamp'],
};
const categorized = {};
const other = [];
const allCategoryFields = new Set(Object.values(categories).flat());
entries.forEach(([key, value]) => {
let found = false;
for (const [cat, fields] of Object.entries(categories)) {
if (fields.includes(key)) {
if (!categorized[cat]) categorized[cat] = [];
categorized[cat].push([key, value]);
found = true;
break;
}
}
if (!found) other.push([key, value]);
});
// Render cards
let html = '';
const renderCard = ([key, value]) => {
let displayVal = typeof value === 'object' ? JSON.stringify(value) : String(value); let displayVal = typeof value === 'object' ? JSON.stringify(value) : String(value);
if (displayVal.length > 40) displayVal = displayVal.substring(0, 37) + '...'; const needsTruncate = displayVal.length > 60;
return `<tr><th>${key}</th><td title="${String(value)}">${displayVal}</td></tr>`; if (needsTruncate) displayVal = displayVal.substring(0, 57) + '...';
}).join(''); const fullVal = typeof value === 'object' ? JSON.stringify(value) : String(value);
return `<div class="exif-card" title="${fullVal.replace(/"/g, '&quot;')}">
<div class="exif-card-label">${key}</div>
<div class="exif-card-value${needsTruncate ? ' truncated' : ''}">${displayVal}</div>
</div>`;
};
// Render each category
for (const [cat, fields] of Object.entries(categories)) {
if (categorized[cat] && categorized[cat].length > 0) {
html += `<div class="exif-category"><i class="bi bi-${cat === 'Camera' ? 'camera' : cat === 'Image' ? 'image' : cat === 'Date/Time' ? 'clock' : cat === 'Exposure' ? 'aperture' : cat === 'GPS' ? 'geo-alt' : 'tag'} me-1"></i>${cat}</div>`;
html += categorized[cat].map(renderCard).join('');
}
}
// Render other fields
if (other.length > 0) {
html += `<div class="exif-category"><i class="bi bi-three-dots me-1"></i>Other</div>`;
html += other.map(renderCard).join('');
}
grid.innerHTML = html;
} }
document.getElementById('exifEmpty').classList.add('d-none'); document.getElementById('exifEmpty').classList.add('d-none');