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:
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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, '"')}">
|
||||||
|
<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');
|
||||||
|
|||||||
Reference in New Issue
Block a user