267 lines
13 KiB
HTML
267 lines
13 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="theme-color" content="#121820">
|
||
<title>Photo Date Editor</title>
|
||
<link rel="manifest" href="/manifest.webmanifest">
|
||
<link rel="stylesheet" href="/vendor/leaflet/leaflet.css">
|
||
<link rel="stylesheet" href="/styles.css">
|
||
</head>
|
||
<body>
|
||
<div class="app-shell">
|
||
<header class="topbar">
|
||
<div class="brand">
|
||
<div class="brand-mark" aria-hidden="true">◫</div>
|
||
<div>
|
||
<h1 id="app-name">Photo Date Editor</h1>
|
||
<p>Local folder · in-place JPEG metadata</p>
|
||
</div>
|
||
</div>
|
||
<div class="top-actions">
|
||
<button id="shortcuts-button" class="button ghost">⌨ Keyboard shortcuts</button>
|
||
<button id="open-folder-top" class="button primary">Open photo folder</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="workspace">
|
||
<aside class="sidebar left-panel">
|
||
<section class="panel-section folder-section">
|
||
<div class="section-label">Folder</div>
|
||
<div class="folder-row">
|
||
<div>
|
||
<strong id="folder-name">No folder selected</strong>
|
||
<span id="folder-access">Choose a local folder</span>
|
||
</div>
|
||
<button id="open-folder" class="button compact">Change</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-section progress-section">
|
||
<div class="section-heading">
|
||
<span>Progress</span>
|
||
<span id="progress-count">0 / 0</span>
|
||
</div>
|
||
<div class="progress-track" role="progressbar" aria-label="Photo processing progress" aria-valuemin="0" aria-valuemax="100">
|
||
<div id="progress-saved" class="progress-segment saved" title="Saved"></div>
|
||
<div id="progress-skipped" class="progress-segment skipped" title="Skipped"></div>
|
||
<div id="progress-failed" class="progress-segment failed" title="Failed"></div>
|
||
</div>
|
||
<div class="status-legend">
|
||
<span><i class="dot saved"></i>Saved</span>
|
||
<span><i class="dot skipped"></i>Skipped</span>
|
||
<span><i class="dot pending"></i>Pending</span>
|
||
<span><i class="dot failed"></i>Failed</span>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel-section file-section">
|
||
<div class="section-heading"><span>Photos</span><span id="photo-count">0</span></div>
|
||
<div class="file-controls">
|
||
<input id="filter-input" class="input filter filename-filter" type="search" placeholder="Filter filenames…" aria-label="Filter photos by filename" disabled>
|
||
<select id="status-filter" class="input filter" aria-label="Filter photos by status" disabled>
|
||
<option value="all">All statuses</option>
|
||
<option value="pending">Pending</option>
|
||
<option value="saved">Saved</option>
|
||
<option value="skipped">Skipped</option>
|
||
<option value="failed">Failed</option>
|
||
</select>
|
||
<select id="sort-select" class="input filter" aria-label="Sort photos" disabled>
|
||
<option value="filename">Sort: Filename</option>
|
||
<option value="status">Sort: Status</option>
|
||
</select>
|
||
</div>
|
||
<div id="file-list" class="file-list" aria-live="polite">
|
||
<div class="empty-list">Choose a folder containing JPG or JPEG photos.</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="sidebar-footer">
|
||
<button id="rescan-folder" class="button wide" disabled>↻ Rescan folder</button>
|
||
</section>
|
||
</aside>
|
||
|
||
<section class="viewer-panel">
|
||
<div id="viewer-empty" class="viewer-empty">
|
||
<div class="empty-icon">▧</div>
|
||
<h2>Choose your scanned-photo folder</h2>
|
||
<p>The site will request read/write access. Photos stay on this device.</p>
|
||
<button id="open-folder-center" class="button primary large">Open photo folder</button>
|
||
</div>
|
||
|
||
<div id="viewer-content" class="viewer-content hidden">
|
||
<div class="image-stage">
|
||
<img id="photo-preview" alt="Current scanned photograph">
|
||
</div>
|
||
<div class="viewer-tools">
|
||
<div class="tool-group">
|
||
<button id="zoom-out" class="icon-button" title="Zoom out">−</button>
|
||
<button id="zoom-in" class="icon-button" title="Zoom in">+</button>
|
||
<button id="fit-image" class="button compact">Fit</button>
|
||
</div>
|
||
<div class="filename-block">
|
||
<strong id="current-filename">—</strong>
|
||
<span id="current-filesize">—</span>
|
||
</div>
|
||
<div class="tool-group">
|
||
<button id="rotate-left" class="icon-button" title="Rotate preview left">↶</button>
|
||
<button id="rotate-right" class="icon-button" title="Rotate preview right">↷</button>
|
||
<button id="reset-view" class="icon-button" title="Reset preview">⟳</button>
|
||
</div>
|
||
</div>
|
||
<div class="navigation-bar">
|
||
<button id="previous-button" class="button">← Previous</button>
|
||
<div id="viewer-position" class="position">0 / 0</div>
|
||
<div class="navigation-actions">
|
||
<button id="skip-button" class="button ghost">Skip</button>
|
||
<button id="save-button" class="button">Save current</button>
|
||
<button id="save-next-button" class="button primary">Save & Next →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<aside class="sidebar right-panel">
|
||
<div class="tabs" role="tablist">
|
||
<button class="tab active" data-tab="date" role="tab">Date & time</button>
|
||
<button class="tab" data-tab="details" role="tab">Details</button>
|
||
<button class="tab" data-tab="keywords" role="tab">Keywords</button>
|
||
<button class="tab" data-tab="location" role="tab">Location</button>
|
||
</div>
|
||
|
||
<form id="metadata-form" autocomplete="off">
|
||
<section class="tab-panel active" data-panel="date">
|
||
<fieldset>
|
||
<legend>Date precision</legend>
|
||
<label class="radio-row"><input type="radio" name="precision" value="exact" checked> <span>Exact date</span></label>
|
||
<label class="radio-row"><input type="radio" name="precision" value="month"> <span>Month and year</span></label>
|
||
<label class="radio-row"><input type="radio" name="precision" value="year"> <span>Year only</span></label>
|
||
<label class="radio-row"><input type="radio" name="precision" value="approximate"> <span>Approximate year</span></label>
|
||
</fieldset>
|
||
|
||
<div class="field-grid date-grid">
|
||
<label class="field">
|
||
<span>Day</span>
|
||
<input id="day-input" class="input" type="number" min="1" max="31" value="1">
|
||
</label>
|
||
<label class="field month-field">
|
||
<span>Month</span>
|
||
<select id="month-input" class="input">
|
||
<option value="1">January</option><option value="2">February</option>
|
||
<option value="3">March</option><option value="4">April</option>
|
||
<option value="5">May</option><option value="6">June</option>
|
||
<option value="7">July</option><option value="8">August</option>
|
||
<option value="9">September</option><option value="10">October</option>
|
||
<option value="11">November</option><option value="12">December</option>
|
||
</select>
|
||
</label>
|
||
<label class="field">
|
||
<span>Year</span>
|
||
<input id="year-input" class="input" type="number" min="1800" max="2200" placeholder="1987" required>
|
||
</label>
|
||
</div>
|
||
|
||
<label class="field">
|
||
<span>Time</span>
|
||
<select id="time-mode" class="input">
|
||
<option value="unknown">Unknown — use 12:00:00</option>
|
||
<option value="known">Enter a time</option>
|
||
</select>
|
||
</label>
|
||
<label id="time-field" class="field hidden">
|
||
<span>Time value</span>
|
||
<input id="time-input" class="input" type="time" step="1" value="12:00:00">
|
||
</label>
|
||
|
||
<div class="info-card">
|
||
<strong>Partial dates</strong>
|
||
<p>EXIF requires a full timestamp. Month/year uses day 1; year-only uses January 1. XMP also stores the reduced precision.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="tab-panel" data-panel="details">
|
||
<label class="field">
|
||
<span>Description</span>
|
||
<textarea id="description-input" class="input textarea" rows="7" maxlength="2000" placeholder="Who, where, occasion, or other context…"></textarea>
|
||
</label>
|
||
<div class="helper-row"><span>Written to EXIF, XMP and IPTC</span><span id="description-count">0 / 2000</span></div>
|
||
</section>
|
||
|
||
<section class="tab-panel" data-panel="keywords">
|
||
<label class="field">
|
||
<span>People and keywords</span>
|
||
<textarea id="keywords-input" class="input textarea" rows="7" placeholder="Daniel, family, birthday, Bergen"></textarea>
|
||
</label>
|
||
<p class="field-help">Separate entries with commas or new lines. Written as XMP Subject and IPTC Keywords.</p>
|
||
</section>
|
||
|
||
<section class="tab-panel" data-panel="location">
|
||
<div class="location-search">
|
||
<label class="field location-search-field">
|
||
<span>Search for a place or address</span>
|
||
<input id="address-search" class="input" type="search" placeholder="Cabin address, town, landmark…">
|
||
</label>
|
||
<button id="address-search-button" class="button" type="button">Search</button>
|
||
</div>
|
||
<p class="field-help search-privacy">Search queries are sent to the configured geocoder. You can place a pin manually instead.</p>
|
||
<div id="address-results" class="address-results" aria-live="polite"></div>
|
||
|
||
<div id="location-map" class="location-map" aria-label="Map for selecting photo location"></div>
|
||
<p class="field-help map-help">Click the map to place a pin, or drag the pin to fine-tune the position.</p>
|
||
|
||
<div class="field-grid coordinate-grid">
|
||
<label class="field">
|
||
<span>Latitude</span>
|
||
<input id="latitude-input" class="input" type="number" min="-90" max="90" step="0.000001" placeholder="60.391263">
|
||
</label>
|
||
<label class="field">
|
||
<span>Longitude</span>
|
||
<input id="longitude-input" class="input" type="number" min="-180" max="180" step="0.000001" placeholder="5.322054">
|
||
</label>
|
||
</div>
|
||
|
||
<label class="field">
|
||
<span>Location label</span>
|
||
<input id="location-name-input" class="input" type="text" maxlength="500" placeholder="Cabin, Voss, Norway">
|
||
</label>
|
||
|
||
<div class="location-actions">
|
||
<button id="clear-location" class="button ghost" type="button">Clear location</button>
|
||
</div>
|
||
<div class="info-card location-info">
|
||
<strong>What gets written</strong>
|
||
<p>Coordinates are written to EXIF and XMP GPS fields. The optional label is written to XMP Location.</p>
|
||
</div>
|
||
</section>
|
||
</form>
|
||
|
||
<section class="right-footer">
|
||
<div id="save-status" class="save-status idle">
|
||
<span class="status-icon">●</span>
|
||
<div><strong>No photo selected</strong><span>Open a folder to begin.</span></div>
|
||
</div>
|
||
<button id="copy-previous" class="button wide" disabled>Copy previous values</button>
|
||
</section>
|
||
</aside>
|
||
</main>
|
||
</div>
|
||
|
||
<dialog id="shortcuts-dialog">
|
||
<div class="dialog-heading"><h2>Keyboard shortcuts</h2><button id="close-shortcuts" class="icon-button">×</button></div>
|
||
<dl class="shortcut-list">
|
||
<div><dt>Enter / Ctrl+Enter</dt><dd>Save and open next photo</dd></div>
|
||
<div><dt>Ctrl+S</dt><dd>Save current photo and stay on it</dd></div>
|
||
<div><dt>← / →</dt><dd>Previous or next photo</dd></div>
|
||
<div><dt>S</dt><dd>Mark skipped and open next photo</dd></div>
|
||
<div><dt>C</dt><dd>Copy previous photo's values</dd></div>
|
||
<div><dt>Shift+Tab</dt><dd>Switch to the previous right-side tab</dd></div>
|
||
</dl>
|
||
</dialog>
|
||
|
||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||
<script src="/vendor/leaflet/leaflet.js"></script>
|
||
<script src="/app.js" type="module"></script>
|
||
</body>
|
||
</html>
|