add caching for icons and json, spacing for ui
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
/target
|
||||
*.json
|
||||
/icons
|
||||
Cargo.lock
|
||||
*.sh
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
97
src/main.rs
97
src/main.rs
@@ -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;
|
||||
}
|
||||
|
||||
15
src/steam.rs
15
src/steam.rs
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
26
src/ui.rs
26
src/ui.rs
@@ -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.);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
40
src/watch.rs
40
src/watch.rs
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user