add caching for icons and json, spacing for ui

This commit is contained in:
2025-01-25 21:09:29 +01:00
parent 1fe2a21d0d
commit cabdd7d5be
7 changed files with 149 additions and 46 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,3 @@
/target
*.json
/icons
Cargo.lock
*.sh

View File

@@ -3,6 +3,13 @@ name = "achievement-watcher"
version = "0.1.0"
edition = "2021"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
[dependencies]
dirs = "6.0.0"
eframe = "0.30.0"

View File

@@ -29,7 +29,8 @@ pub struct Achievement {
pub struct Context {
pub api_key: String,
pub game_name_cache_path: String,
pub unlocked_icon_path: String,
pub json_cache_path: String,
pub icon_cache_path: String,
pub game_name_list: Vec<App>,
pub dirs: Vec<String>,
}
@@ -47,7 +48,7 @@ pub async fn get_achievement_data(client: &Client, app_id: u32, context: &Contex
app_id,
achievements: Vec::new(),
};
let game_data = steam::get_steam_data(client, &context.api_key, app_id, "en").await;
let game_data = steam::get_steam_data(client, &context.api_key, app_id, "en", &context.json_cache_path).await;
let ini_data = steam::get_achievement_data_all_ini(&context.dirs);
let ini_game = ini_data.iter().find(|m| m.id == app_id)?;
for json_achievement in &game_data.game.available_game_stats.achievements {
@@ -97,7 +98,7 @@ pub async fn get_achievement_data_all(client: &Client, context: &Context) -> Vec
.await
.unwrap(),
};
let game_data = steam::get_steam_data(client, &context.api_key, ini_game.id, "en").await;
let game_data = steam::get_steam_data(client, &context.api_key, ini_game.id, "en", &context.json_cache_path).await;
for json_achievement in &game_data.game.available_game_stats.achievements {
let mut found_in_ini = false;
for ini_achievement in &ini_game.achievements {

View File

@@ -1,4 +1,4 @@
use achievement_watcher::Context;
use achievement_watcher::{Context, Game};
use reqwest::Client;
use std::{env, fs};
@@ -8,16 +8,26 @@ mod watch;
#[tokio::main]
async fn main() {
let client = Client::new();
let context = setup();
let context = match setup() {
Some(v) => v,
None => {
eprintln!("ERROR: was not able to create context");
std::process::exit(1);
}
};
let mut games = achievement_watcher::get_achievement_data_all(&client, &context).await;
cache_icons(&context, &games).await;
let args: Vec<String> = std::env::args().skip(1).collect();
if args.is_empty() {
ui::ui(games).unwrap();
if let Err(e) = ui::ui(context, games) {
eprintln!("ERROR: {e}");
std::process::exit(1);
}
} else if args.len() == 1 {
match args[0].as_str() {
"-w" => watch::watch_file(&context, &mut games)
.await
.unwrap(),
"-w" => watch::watch_files(&context, &mut games).await.unwrap(),
_ => panic!("wrong arg"),
}
} else {
@@ -25,23 +35,22 @@ async fn main() {
}
}
fn setup() -> Context {
let config_dir =
dirs::config_dir().unwrap().to_str().unwrap().to_owned() + "/achievement-watcher";
let cache_dir =
dirs::cache_dir().unwrap().to_str().unwrap().to_owned() + "/achievement-watcher";
fn setup() -> Option<Context> {
let config_dir = dirs::config_dir()?.to_str()?.to_owned() + "/achievement-watcher";
let cache_dir = dirs::cache_dir()?.to_str()?.to_owned() + "/achievement-watcher";
match std::fs::exists(&config_dir) {
Ok(false) => std::fs::create_dir(&config_dir).unwrap(),
Ok(false) => std::fs::create_dir(&config_dir).ok()?,
Err(e) => eprintln!("error in setup: config dir: {e}"),
Ok(true) => (),
}
match std::fs::exists(&cache_dir) {
Ok(false) => std::fs::create_dir(&cache_dir).unwrap(),
Ok(false) => std::fs::create_dir(&cache_dir).ok()?,
Err(e) => eprintln!("error in setup: cache dir: {e}"),
Ok(true) => (),
}
let game_name_cache_path = format!("{cache_dir}/game_names.txt");
let unlocked_icon_path = format!("{cache_dir}/unlocked.png");
let json_cache_path = format!("{cache_dir}/json");
let icon_cache_path = format!("{cache_dir}/icon");
let game_name_list = achievement_watcher::util::get_game_name_file_cache(&game_name_cache_path);
let api_key_path = format!("{config_dir}/api-key.txt");
let api_key = match fs::read_to_string(&api_key_path) {
@@ -52,16 +61,68 @@ fn setup() -> Context {
}
};
let api_key = api_key.trim();
let wine_dir = env::var("WINEPREFIX").unwrap_or(env::var("HOME").unwrap() + ".wine");
let wine_dir = env::var("WINEPREFIX").unwrap_or(env::var("HOME").ok()? + ".wine");
let dirs = vec![
wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/CODEX",
wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/RUNE",
];
Context {
Some(Context {
api_key: api_key.to_owned(),
dirs,
game_name_list,
game_name_cache_path,
unlocked_icon_path,
}
json_cache_path,
icon_cache_path,
})
}
async fn cache_icons(context: &Context, games: &Vec<Game>) {
let client = Client::new();
let mut futures = Vec::new();
for game in games {
for achievement in &game.achievements {
let client = &client;
let future = async move {
let icon_dir = &format!("{}/{}", &context.icon_cache_path, game.name);
if !std::path::Path::new(icon_dir).exists() {
std::fs::create_dir(icon_dir).unwrap();
}
let icon_path = format!(
"{}/{}/{}.png",
&context.icon_cache_path, game.name, achievement.name,
);
if !std::path::Path::new(&icon_path).exists() {
let data = client
.get(&achievement.icon)
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
std::fs::write(&icon_path, &data).unwrap();
println!("written icon path: {icon_path}");
}
let icon_path = format!(
"{}/{}/{}gray.png",
context.icon_cache_path, game.name, achievement.name,
);
if !std::path::Path::new(&icon_path).exists() {
let data = client
.get(&achievement.icongray)
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
std::fs::write(&icon_path, &data).unwrap();
println!("written icon: {icon_path}");
}
};
futures.push(future);
}
}
futures::future::join_all(futures).await;
}

View File

@@ -2,6 +2,7 @@ use ini::Ini;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
@@ -90,9 +91,21 @@ impl IniAchievement {
}
}
pub async fn get_steam_data(client: &Client, key: &str, appid: u32, lang: &str) -> JsonGame {
pub async fn get_steam_data(
client: &Client,
key: &str,
appid: u32,
lang: &str,
json_cache_path: &str,
) -> JsonGame {
let json_file_path = format!("{json_cache_path}/{appid}.json");
if Path::new(&json_file_path).exists() {
let json = fs::read_to_string(json_file_path).unwrap();
return serde_json::from_str(&json).unwrap();
}
let url = format!("https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v0002/?key={key}&appid={appid}&l={lang}&format=json");
let json = client.get(url).send().await.unwrap().text().await.unwrap();
fs::write(json_file_path, &json).unwrap();
serde_json::from_str(&json).unwrap()
}

View File

@@ -1,7 +1,8 @@
use achievement_watcher::Game;
use achievement_watcher::{Context, Game};
use eframe::egui;
use std::path::Path;
pub fn ui(games: Vec<Game>) -> eframe::Result {
pub fn ui(context: Context, games: Vec<Game>) -> eframe::Result {
env_logger::init();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default(),
@@ -41,18 +42,29 @@ pub fn ui(games: Vec<Game>) -> eframe::Result {
ui.add(egui::ProgressBar::new(
achieved_count as f32 / game.achievements.len() as f32,
));
ui.add_space(32.);
for achievement in &game.achievements {
ui.heading(&achievement.name);
if let Some(desc) = &achievement.description {
ui.label(desc);
}
let icon = if achievement.achieved {
&achievement.icon
let icon_path = format!(
"{}/{}/{}{}.png",
context.icon_cache_path,
game.name,
achievement.name,
if achievement.achieved { "" } else { "gray" }
);
if Path::new(&icon_path).exists() {
ui.add(
egui::Image::new(format!("file://{}", icon_path))
.fit_to_original_size(1.),
);
} else {
&achievement.icongray
};
ui.add(egui::Image::new(icon).fit_to_original_size(1.));
panic!("image not cached");
}
ui.add_space(32.);
}
});
});

View File

@@ -4,7 +4,7 @@ use reqwest::Client;
use std::path::Path;
use std::sync::mpsc;
pub async fn watch_file(context: &Context, games: &mut Vec<Game>) -> Result<()> {
pub async fn watch_files(context: &Context, games: &mut Vec<Game>) -> Result<()> {
let (tx, rx) = mpsc::channel::<Result<Event>>();
let mut watcher = notify::recommended_watcher(tx)?;
let client = Client::new();
@@ -15,7 +15,6 @@ pub async fn watch_file(context: &Context, games: &mut Vec<Game>) -> Result<()>
for res in &rx {
match res {
Ok(event) => {
// println!("event: {:#?}", event);
if let EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write)) =
event.kind
{
@@ -65,23 +64,34 @@ async fn check_for_new_achievement(
for i in indexes {
let new_achievement = &new.achievements[i];
println!("new achievement: {:#?}", new_achievement);
let jpg_icon: &[u8] = &client
.get(&new_achievement.icon)
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
let image: image::DynamicImage = image::load_from_memory(jpg_icon).unwrap();
image
.save_with_format(&context.unlocked_icon_path, image::ImageFormat::Png)
.unwrap();
let icon_path = format!(
"{}/{}/{}.png",
context.icon_cache_path, old.name, new_achievement.name
);
let icon_path = Path::new(&icon_path);
println!("icon path: {:?}", &icon_path);
if !icon_path.exists() {
let jpg_icon: &[u8] = &client
.get(&new_achievement.icon)
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
let image: image::DynamicImage = image::load_from_memory(jpg_icon).unwrap();
image
.save_with_format(icon_path, image::ImageFormat::Png)
.unwrap();
} else {
println!("using cached icons");
}
util::send_notification(
&new.name,
&new_achievement.name,
new_achievement.description.as_deref(),
Some(&context.unlocked_icon_path),
Some(icon_path.to_str().unwrap()),
);
}
} else {