Skip to content

Commit e63cf98

Browse files
committed
Release OpenPet v0.1.2
1 parent d756930 commit e63cf98

5 files changed

Lines changed: 158 additions & 33 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openpet",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"private": true,
55
"type": "module",
66
"description": "OpenPet desktop runtime for Codex-compatible pet companions.",

src-tauri/Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "openpet"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
description = "OpenPet desktop runtime for Codex-compatible pet companions"
55
authors = ["OpenPet contributors"]
66
edition = "2021"
@@ -15,6 +15,7 @@ tauri-build = { version = "2.5.1", features = [] }
1515

1616
[dependencies]
1717
reqwest = { version = "0.13.3", default-features = false, features = ["json", "rustls"] }
18+
rustls-native-certs = "0.8.3"
1819
serde = { version = "1.0", features = ["derive"] }
1920
serde_json = "1.0"
2021
tauri = { version = "2.11.0", features = ["tray-icon", "image-png"] }

src-tauri/src/lib.rs

Lines changed: 152 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1919
const DEFAULT_PORT: u16 = 17321;
@@ -983,8 +983,10 @@ struct PetdexManifest {
983983
struct 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+
12351252
async 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+
12821312
fn 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+
17321782
fn 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]
23552419
fn 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");

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "OpenPet",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"identifier": "dev.xter.openpet",
66
"build": {
77
"beforeDevCommand": "pnpm dev",

0 commit comments

Comments
 (0)