fix: deduplicate icons in browser by preferring highest resolution version
This commit is contained in:
+63
-16
@@ -25,28 +25,75 @@ impl CompositeIcon {
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
if let Some(emblem) = &self.emblem {
|
||||
fn strip_xml(svg: &str) -> &str {
|
||||
if let Some(idx) = svg.find("<svg") {
|
||||
&svg[idx..]
|
||||
} else {
|
||||
svg
|
||||
use regex::Regex;
|
||||
|
||||
fn parse_dims(svg: &str) -> (f64, f64) {
|
||||
if let Some(re) = Regex::new(r#"(?i)<svg[^>]*viewBox\s*=\s*"([^"]+)""#).ok() {
|
||||
if let Some(caps) = re.captures(svg) {
|
||||
let parts: Vec<&str> = caps.get(1).unwrap().as_str().split_whitespace().collect();
|
||||
if parts.len() == 4 {
|
||||
if let (Ok(w), Ok(h)) = (parts[2].parse::<f64>(), parts[3].parse::<f64>()) {
|
||||
return (w, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut w = 64.0;
|
||||
let mut h = 64.0;
|
||||
if let Some(re_w) = Regex::new(r#"(?i)<svg[^>]*width\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#).ok() {
|
||||
if let Some(caps) = re_w.captures(svg) {
|
||||
if let Ok(val) = caps.get(1).unwrap().as_str().trim().parse::<f64>() {
|
||||
w = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(re_h) = Regex::new(r#"(?i)<svg[^>]*height\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#).ok() {
|
||||
if let Some(caps) = re_h.captures(svg) {
|
||||
if let Ok(val) = caps.get(1).unwrap().as_str().trim().parse::<f64>() {
|
||||
h = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
(w, h)
|
||||
}
|
||||
|
||||
let bg_svg = strip_xml(&self.bg.content);
|
||||
let emblem_svg = strip_xml(&emblem.content);
|
||||
fn extract_inner(svg: &str) -> &str {
|
||||
let start = if let Some(idx) = svg.find("<svg") {
|
||||
if let Some(close_idx) = svg[idx..].find('>') {
|
||||
idx + close_idx + 1
|
||||
} else { 0 }
|
||||
} else { 0 };
|
||||
let end = if let Some(idx) = svg.rfind("</svg>") { idx } else { svg.len() };
|
||||
if start < end { &svg[start..end] } else { svg }
|
||||
}
|
||||
|
||||
let (bg_w, bg_h) = parse_dims(&self.bg.content);
|
||||
let (em_w, em_h) = parse_dims(&emblem.content);
|
||||
|
||||
format!(
|
||||
r#"<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg x="0" y="0" width="64" height="64">
|
||||
let mapped_x = self.emblem_x * bg_w / 64.0;
|
||||
let mapped_y = self.emblem_y * bg_h / 64.0;
|
||||
let mapped_size = self.emblem_size * bg_w / 64.0; // Assuming uniform coordinate mapping scaling based on width
|
||||
|
||||
let scale_x = mapped_size / em_w;
|
||||
let scale_y = mapped_size / em_h; // Keep aspect ratio mapping relative to its own viewBox
|
||||
|
||||
let emblem_inner = extract_inner(&emblem.content);
|
||||
|
||||
let emblem_group = format!(
|
||||
r#"<g transform="translate({}, {}) scale({}, {})">
|
||||
{}
|
||||
</svg>
|
||||
<svg x="{}" y="{}" width="{}" height="{}">
|
||||
{}
|
||||
</svg>
|
||||
</g>
|
||||
</svg>"#,
|
||||
bg_svg, self.emblem_x, self.emblem_y, self.emblem_size, self.emblem_size, emblem_svg
|
||||
)
|
||||
mapped_x, mapped_y, scale_x, scale_y, emblem_inner
|
||||
);
|
||||
|
||||
if let Some(idx) = self.bg.content.rfind("</svg>") {
|
||||
let mut new_svg = self.bg.content[..idx].to_string();
|
||||
new_svg.push_str(&emblem_group);
|
||||
new_svg
|
||||
} else {
|
||||
self.bg.content.clone() // fallback
|
||||
}
|
||||
} else {
|
||||
self.bg.content.clone()
|
||||
}
|
||||
|
||||
+39
-4
@@ -2,13 +2,48 @@ use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub fn list_svg_icons(root: &Path) -> Vec<PathBuf> {
|
||||
WalkDir::new(root)
|
||||
use std::collections::HashMap;
|
||||
let mut best_icons: HashMap<String, (u32, PathBuf)> = HashMap::new();
|
||||
|
||||
let walker = WalkDir::new(root)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_file())
|
||||
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("svg"))
|
||||
.map(|e| e.path().to_path_buf())
|
||||
.collect()
|
||||
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("svg"));
|
||||
|
||||
for entry in walker {
|
||||
let path = entry.path().to_path_buf();
|
||||
let mut components = path.components().rev();
|
||||
|
||||
let filename = if let Some(c) = components.next() {
|
||||
c.as_os_str().to_string_lossy().into_owned()
|
||||
} else { continue; };
|
||||
|
||||
let size_str = if let Some(c) = components.next() {
|
||||
c.as_os_str().to_string_lossy().into_owned()
|
||||
} else { continue; };
|
||||
|
||||
let category = if let Some(c) = components.next() {
|
||||
c.as_os_str().to_string_lossy().into_owned()
|
||||
} else { continue; };
|
||||
|
||||
let score = if size_str == "scalable" {
|
||||
10000
|
||||
} else if size_str.ends_with("x2") || size_str.ends_with("@2x") {
|
||||
size_str.trim_matches(|c: char| !c.is_numeric()).parse::<u32>().unwrap_or(0) * 2
|
||||
} else {
|
||||
size_str.parse::<u32>().unwrap_or(0)
|
||||
};
|
||||
|
||||
let key = format!("{}/{}", category, filename);
|
||||
|
||||
let entry = best_icons.entry(key).or_insert((score, path.clone()));
|
||||
if score > entry.0 {
|
||||
*entry = (score, path);
|
||||
}
|
||||
}
|
||||
|
||||
best_icons.into_values().map(|(_, p)| p).collect()
|
||||
}
|
||||
|
||||
pub fn fuzzy_match(query: &str, candidate: &str) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user