chore: post-review hardening + changelog link tooltip

Security / robustness / a11y polish on top of the color-search + tooltips work:

- URL opening: route through validated backend commands (open_map_location
  with lat/lon bounds-checking, open_changelog_url with a fixed URL) instead
  of the frontend opener:allow-open-url capability, which is now removed.
- EXIF GPS parsing: validate coordinate ranges and require the correct
  N/S/E/W hemisphere ref byte, then clamp.
- Guard double-submit on album create / add-to-album (Lightbox, BulkActionBar,
  Sidebar) and discard stale autocomplete responses in the bulk tag editor.
- Gallery tile: stop nesting <button>s — non-interactive tile div with an
  overlay button for open/toggle; checkbox and Similar promoted with z-index
  + focus rings.
- Accessibility: keyboard handlers, focus-visible rings, and aria on album
  rows, tag-manage actions, and tile controls; Tooltip uses a block <div>
  wrapper in block mode and aria-hidden when hidden.
- Show the destination URL in a tooltip on the "Full changelog" link so the
  user can see where it goes before clicking.
- Toolbar "All" filter also clears the color filter; color-filtered views no
  longer get unfiltered newly-indexed images injected.
- Sidebar reorder: bail if the album set changed mid-drag.
- Tooling: add cargo fmt scripts; alphabetize package.json scripts.
This commit is contained in:
2026-06-28 14:17:58 +01:00
parent 90dec3b212
commit bb0038e0a1
14 changed files with 211 additions and 78 deletions
+66 -13
View File
@@ -1702,25 +1702,37 @@ pub struct ImageExif {
}
fn gps_coord(exif: &exif::Exif, coord: exif::Tag, reference: exif::Tag) -> Option<f64> {
let (max_abs, positive_ref, negative_ref) = match (coord, reference) {
(exif::Tag::GPSLatitude, exif::Tag::GPSLatitudeRef) => (90.0, b'N', b'S'),
(exif::Tag::GPSLongitude, exif::Tag::GPSLongitudeRef) => (180.0, b'E', b'W'),
_ => return None,
};
let field = exif.get_field(coord, exif::In::PRIMARY)?;
if let exif::Value::Rational(ref parts) = field.value {
if parts.len() >= 3 {
let degrees = parts[0].to_f64() + parts[1].to_f64() / 60.0 + parts[2].to_f64() / 3600.0;
if !degrees.is_finite() || degrees < 0.0 || degrees > max_abs {
return None;
}
// Read the hemisphere straight from the ref tag's ASCII bytes
// ("N"/"S"/"E"/"W") rather than its formatted display string.
let negative = exif
.get_field(reference, exif::In::PRIMARY)
.map(|f| match &f.value {
exif::Value::Ascii(values) => values
.iter()
.flatten()
.next()
.map(|&byte| byte == b'S' || byte == b'W')
.unwrap_or(false),
_ => false,
})
.unwrap_or(false);
return Some(if negative { -degrees } else { degrees });
let ref_byte =
exif.get_field(reference, exif::In::PRIMARY)
.and_then(|f| match &f.value {
exif::Value::Ascii(values) => values.iter().flatten().next().copied(),
_ => None,
})?;
if ref_byte != positive_ref && ref_byte != negative_ref {
return None;
}
let signed = if ref_byte == negative_ref {
-degrees
} else {
degrees
};
return signed
.is_finite()
.then_some(signed.clamp(-max_abs, max_abs));
}
}
None
@@ -2500,6 +2512,47 @@ pub async fn open_app_data_folder(app: AppHandle) -> Result<(), String> {
.map_err(|e| e.to_string())
}
#[derive(Deserialize)]
pub struct OpenMapLocationParams {
pub lat: f64,
pub lon: f64,
}
#[tauri::command]
pub async fn open_map_location(
app: AppHandle,
params: OpenMapLocationParams,
) -> Result<(), String> {
use tauri_plugin_opener::OpenerExt;
if !params.lat.is_finite()
|| !params.lon.is_finite()
|| !(-90.0..=90.0).contains(&params.lat)
|| !(-180.0..=180.0).contains(&params.lon)
{
return Err("Invalid map coordinates".to_string());
}
let url = format!(
"https://www.openstreetmap.org/?mlat={lat:.6}&mlon={lon:.6}#map=15/{lat:.6}/{lon:.6}",
lat = params.lat,
lon = params.lon,
);
app.opener()
.open_url(url, None::<&str>)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn open_changelog_url(app: AppHandle) -> Result<(), String> {
use tauri_plugin_opener::OpenerExt;
app.opener()
.open_url(
"https://github.com/JezzWTF/phokus/blob/main/CHANGELOG.md",
None::<&str>,
)
.map_err(|e| e.to_string())
}
// ---------------------------------------------------------------------------
// Database maintenance
// ---------------------------------------------------------------------------
+2
View File
@@ -216,6 +216,8 @@ pub fn run() {
commands::get_tagging_queue_folder_ids,
commands::set_tagging_queue_folder_ids,
commands::open_app_data_folder,
commands::open_map_location,
commands::open_changelog_url,
commands::get_database_info,
commands::vacuum_database,
commands::rebuild_semantic_index,