@@ -13,7 +13,7 @@ use tauri::{
1313 menu:: { Menu , MenuItem } ,
1414 path:: BaseDirectory ,
1515 tray:: TrayIconBuilder ,
16- AppHandle , Emitter , Manager ,
16+ AppHandle , Emitter , Manager , WindowEvent ,
1717} ;
1818
1919const DEFAULT_PORT : u16 = 17321 ;
@@ -983,8 +983,10 @@ struct PetdexManifest {
983983struct PetdexPet {
984984 slug : String ,
985985 display_name : String ,
986- description : String ,
987- page_url : String ,
986+ #[ serde( default ) ]
987+ description : Option < String > ,
988+ #[ serde( default ) ]
989+ page_url : Option < String > ,
988990 spritesheet_url : String ,
989991}
990992
@@ -1195,14 +1197,22 @@ async fn resolve_petdex_source(
11951197 . find ( |pet| pet. slug == slug)
11961198 . ok_or_else ( || format ! ( "Petdex pet '{slug}' was not found in the public manifest." ) ) ?;
11971199 let spritesheet_url = parse_safe_import_url ( & pet. spritesheet_url ) ?;
1200+ let description = match option_trimmed ( & pet. description ) {
1201+ Some ( description) => description,
1202+ None => fetch_page_description ( client, source_url)
1203+ . await
1204+ . unwrap_or_else ( || "Imported from Petdex." . to_string ( ) ) ,
1205+ } ;
1206+ let source_url =
1207+ option_trimmed ( & pet. page_url ) . unwrap_or_else ( || source_url. as_str ( ) . to_string ( ) ) ;
11981208
11991209 Ok ( ResolvedPetSource {
12001210 id : sanitize_pet_id ( & pet. slug ) ,
12011211 display_name : truncate_chars ( & pet. display_name , 96 ) ,
1202- description : truncate_chars ( & pet . description , 280 ) ,
1212+ description : truncate_chars ( & description, 280 ) ,
12031213 spritesheet_url,
12041214 source_name : "Petdex" . to_string ( ) ,
1205- source_url : pet . page_url ,
1215+ source_url,
12061216 } )
12071217}
12081218
@@ -1232,34 +1242,27 @@ async fn resolve_codex_pets_source(
12321242 } )
12331243}
12341244
1245+ async fn fetch_page_description ( client : & reqwest:: Client , source_url : & url:: Url ) -> Option < String > {
1246+ let html = fetch_text ( client, source_url. clone ( ) , MAX_HTML_BYTES )
1247+ . await
1248+ . ok ( ) ?;
1249+ extract_page_description ( & html)
1250+ }
1251+
12351252async fn resolve_generic_pet_page (
12361253 client : & reqwest:: Client ,
12371254 source_url : & url:: Url ,
12381255) -> Result < ResolvedPetSource , String > {
12391256 let html = fetch_text ( client, source_url. clone ( ) , MAX_HTML_BYTES ) . await ?;
1240- let json_ld = extract_json_ld_values ( & html) ;
1241- let display_name = json_ld
1242- . iter ( )
1243- . find_map ( |value| find_json_string ( value, "name" ) )
1244- . or_else ( || extract_meta_content ( & html, "og:title" ) )
1245- . or_else ( || extract_title ( & html) )
1257+ let display_name = extract_page_display_name ( & html)
12461258 . map ( |value| clean_title ( & value) )
12471259 . filter ( |value| !value. trim ( ) . is_empty ( ) )
12481260 . unwrap_or_else ( || "Imported Pet" . to_string ( ) ) ;
1249- let description = json_ld
1250- . iter ( )
1251- . find_map ( |value| find_json_string ( value, "description" ) )
1252- . or_else ( || extract_meta_content ( & html, "description" ) )
1261+ let description = extract_page_description ( & html)
12531262 . unwrap_or_else ( || "Imported Codex-compatible pet." . to_string ( ) ) ;
1254- let spritesheet_url = json_ld
1255- . iter ( )
1256- . filter_map ( find_json_webp)
1257- . find ( |candidate| is_likely_spritesheet ( candidate) )
1258- . and_then ( |candidate| source_url. join ( & candidate) . ok ( ) )
1259- . or_else ( || extract_webp_url ( & html, source_url) )
1260- . ok_or_else ( || {
1261- "Could not find a Codex-compatible spritesheet.webp on this page." . to_string ( )
1262- } ) ?;
1263+ let spritesheet_url = extract_page_spritesheet_url ( & html, source_url) . ok_or_else ( || {
1264+ "Could not find a Codex-compatible spritesheet.webp on this page." . to_string ( )
1265+ } ) ?;
12631266 let id_hint = source_url
12641267 . path_segments ( )
12651268 . and_then ( |segments| segments. filter ( |segment| !segment. is_empty ( ) ) . last ( ) )
@@ -1279,6 +1282,33 @@ async fn resolve_generic_pet_page(
12791282 } )
12801283}
12811284
1285+ fn extract_page_display_name ( html : & str ) -> Option < String > {
1286+ let json_ld = extract_json_ld_values ( html) ;
1287+ json_ld
1288+ . iter ( )
1289+ . find_map ( |value| find_json_string ( value, "name" ) )
1290+ . or_else ( || extract_meta_content ( html, "og:title" ) )
1291+ . or_else ( || extract_title ( html) )
1292+ }
1293+
1294+ fn extract_page_description ( html : & str ) -> Option < String > {
1295+ let json_ld = extract_json_ld_values ( html) ;
1296+ json_ld
1297+ . iter ( )
1298+ . find_map ( |value| find_json_string ( value, "description" ) )
1299+ . or_else ( || extract_meta_content ( html, "description" ) )
1300+ }
1301+
1302+ fn extract_page_spritesheet_url ( html : & str , base_url : & url:: Url ) -> Option < url:: Url > {
1303+ let json_ld = extract_json_ld_values ( html) ;
1304+ json_ld
1305+ . iter ( )
1306+ . filter_map ( find_json_webp)
1307+ . find ( |candidate| is_likely_spritesheet ( candidate) )
1308+ . and_then ( |candidate| base_url. join ( & candidate) . ok ( ) )
1309+ . or_else ( || extract_webp_url ( html, base_url) )
1310+ }
1311+
12821312fn path_segment_after ( url : & url:: Url , prefix : & str ) -> Option < String > {
12831313 let mut segments = url. path_segments ( ) ?;
12841314 while let Some ( segment) = segments. next ( ) {
@@ -1720,15 +1750,35 @@ pub(crate) async fn import_website_pet(
17201750 payload : WebsiteImportPayload ,
17211751) -> Result < RuntimeSnapshot , String > {
17221752 let url = parse_safe_import_url ( & payload. url ) ?;
1723- let client = reqwest:: Client :: builder ( )
1724- . user_agent ( "OpenPet/0.1 website-import" )
1725- . timeout ( Duration :: from_secs ( 25 ) )
1726- . build ( )
1727- . map_err ( |error| format ! ( "failed to create HTTP client: {error}" ) ) ?;
1753+ let client = website_import_client ( ) ?;
17281754 let resolved = resolve_pet_source ( & client, & url) . await ?;
17291755 install_resolved_pet ( app, state, & client, resolved, payload. force ) . await
17301756}
17311757
1758+ fn website_import_client ( ) -> Result < reqwest:: Client , String > {
1759+ let mut builder = reqwest:: Client :: builder ( )
1760+ . user_agent ( "OpenPet/0.1 website-import" )
1761+ . connect_timeout ( Duration :: from_secs ( 10 ) )
1762+ . timeout ( Duration :: from_secs ( 25 ) ) ;
1763+
1764+ let certificates = native_tls_root_certificates ( ) ;
1765+ if !certificates. is_empty ( ) {
1766+ builder = builder. tls_certs_only ( certificates) ;
1767+ }
1768+
1769+ builder
1770+ . build ( )
1771+ . map_err ( |error| format ! ( "failed to create HTTP client: {error}" ) )
1772+ }
1773+
1774+ fn native_tls_root_certificates ( ) -> Vec < reqwest:: Certificate > {
1775+ rustls_native_certs:: load_native_certs ( )
1776+ . certs
1777+ . into_iter ( )
1778+ . filter_map ( |cert| reqwest:: Certificate :: from_der ( cert. as_ref ( ) ) . ok ( ) )
1779+ . collect ( )
1780+ }
1781+
17321782fn now_ms ( ) -> u128 {
17331783 SystemTime :: now ( )
17341784 . duration_since ( UNIX_EPOCH )
@@ -2351,6 +2401,20 @@ fn build_tray(app: &tauri::App, language: PetLanguage) -> tauri::Result<()> {
23512401 Ok ( ( ) )
23522402}
23532403
2404+ fn hide_settings_window_on_close < R : tauri:: Runtime > (
2405+ window : & tauri:: Window < R > ,
2406+ event : & WindowEvent ,
2407+ ) {
2408+ if window. label ( ) != "settings" {
2409+ return ;
2410+ }
2411+
2412+ if let WindowEvent :: CloseRequested { api, .. } = event {
2413+ api. prevent_close ( ) ;
2414+ let _ = window. hide ( ) ;
2415+ }
2416+ }
2417+
23542418#[ tauri:: command]
23552419fn get_runtime_snapshot ( app : AppHandle , state : tauri:: State < AppState > ) -> RuntimeSnapshot {
23562420 sync_pet_visibility ( & app, & state) ;
@@ -2526,6 +2590,7 @@ pub fn run() {
25262590 . plugin ( tauri_plugin_process:: init ( ) )
25272591 . plugin ( tauri_plugin_updater:: Builder :: new ( ) . build ( ) )
25282592 . manage ( state. clone ( ) )
2593+ . on_window_event ( hide_settings_window_on_close)
25292594 . setup ( move |app| {
25302595 match runtime_config_path ( app. handle ( ) ) {
25312596 Ok ( path) => {
@@ -2711,6 +2776,64 @@ mod tests {
27112776 ) ;
27122777 }
27132778
2779+ #[ test]
2780+ fn deserializes_current_petdex_manifest_shape ( ) {
2781+ let manifest = serde_json:: from_str :: < PetdexManifest > (
2782+ r#"{
2783+ "generatedAt": "2026-05-04T11:25:36.320Z",
2784+ "total": 468,
2785+ "pets": [{
2786+ "slug": "kebo",
2787+ "displayName": "Kebo",
2788+ "kind": "creature",
2789+ "submittedBy": "railly",
2790+ "spritesheetUrl": "https://cdn.example.test/curated/kebo/spritesheet.webp",
2791+ "petJsonUrl": "https://cdn.example.test/curated/kebo/pet.json",
2792+ "zipUrl": "https://cdn.example.test/curated/kebo/kebo.zip"
2793+ }]
2794+ }"# ,
2795+ )
2796+ . unwrap ( ) ;
2797+
2798+ let pet = manifest. pets . first ( ) . unwrap ( ) ;
2799+ assert_eq ! ( pet. slug, "kebo" ) ;
2800+ assert_eq ! ( pet. description. as_deref( ) , None ) ;
2801+ assert_eq ! ( pet. page_url. as_deref( ) , None ) ;
2802+ }
2803+
2804+ #[ test]
2805+ fn extracts_spriteyard_json_ld_metadata ( ) {
2806+ let base = url:: Url :: parse ( "https://spriteyard.com/pets/nib/" ) . unwrap ( ) ;
2807+ let html = r#"
2808+ <script type="application/ld+json">{
2809+ "@context": "https://schema.org",
2810+ "@type": "CreativeWork",
2811+ "name": "Nib",
2812+ "description": "A pixel pet package for Codex.",
2813+ "image": "https://assets.spriteyard.com/pets/nib/spritesheet.webp",
2814+ "encoding": {
2815+ "@type": "MediaObject",
2816+ "contentUrl": "https://assets.spriteyard.com/pets/nib/nib.zip"
2817+ }
2818+ }</script>
2819+ "# ;
2820+
2821+ assert_eq ! ( extract_page_display_name( html) . as_deref( ) , Some ( "Nib" ) ) ;
2822+ assert_eq ! (
2823+ extract_page_description( html) . as_deref( ) ,
2824+ Some ( "A pixel pet package for Codex." )
2825+ ) ;
2826+ assert_eq ! (
2827+ extract_page_spritesheet_url( html, & base) . map( |url| url. to_string( ) ) ,
2828+ Some ( "https://assets.spriteyard.com/pets/nib/spritesheet.webp" . to_string( ) )
2829+ ) ;
2830+ }
2831+
2832+ #[ test]
2833+ fn builds_website_import_client ( ) {
2834+ website_import_client ( ) . unwrap ( ) ;
2835+ }
2836+
27142837 #[ test]
27152838 fn resolves_local_package_directory ( ) {
27162839 let source = PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) ) . join ( "../public/pets/nia" ) ;
0 commit comments