A photo manager for VRChat.
1#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 3mod frontend_calls; 4mod pngmeta; 5mod util; 6mod worldscraper; 7 8use core::time; 9use arboard::Clipboard; 10use frontend_calls::*; 11 12use notify::{ EventKind, RecursiveMode, Watcher }; 13use pngmeta::PNGImage; 14use regex::Regex; 15use util::{ cache::Cache, get_photo_path::get_photo_path }; 16use std::{ env, fs, sync::Mutex, thread }; 17use tauri::{ Emitter, Manager, State, WindowEvent }; 18use tauri_plugin_deep_link::DeepLinkExt; 19 20use crate::frontend_calls::config::{get_config_value_string, Config}; 21 22fn main() { 23 #[cfg(target_os = "linux")] 24 std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // Fix webkitgtk being shit 25 26 let cache = Cache::new(); 27 28 // Double check the app has an install directory 29 let container_folder = dirs::config_dir() 30 .unwrap() 31 .join("PhazeDev/VRChatPhotoManager"); 32 33 match fs::metadata(&container_folder) { 34 Ok(meta) => { 35 if meta.is_file() { 36 panic!("Cannot launch app as the container path is a file not a directory"); 37 } 38 } 39 Err(_) => { 40 let folder = dirs::config_dir().unwrap(); 41 match fs::metadata(&folder) { 42 Ok(meta) => { 43 if meta.is_file() { 44 panic!("Cannot launch app as the container path is a file not a directory"); 45 } 46 } 47 Err(_) => { 48 fs::create_dir(&folder).unwrap(); 49 } 50 } 51 52 let phaz_folder = dirs::config_dir().unwrap().join("PhazeDev"); 53 match fs::metadata(&phaz_folder) { 54 Ok(meta) => { 55 if meta.is_file() { 56 panic!("Cannot launch app as the container path is a file not a directory"); 57 } 58 } 59 Err(_) => { 60 fs::create_dir(&phaz_folder).unwrap(); 61 } 62 } 63 64 fs::create_dir(&container_folder).unwrap(); 65 } 66 } 67 68 let sync_lock_path = dirs::config_dir() 69 .unwrap() 70 .join("PhazeDev/VRChatPhotoManager/.sync_lock"); 71 72 match fs::metadata(&sync_lock_path) { 73 Ok(_) => { 74 fs::remove_file(&sync_lock_path).unwrap(); 75 } 76 Err(_) => {} 77 } 78 79 println!("Loading App..."); 80 let photos_path = util::get_photo_path::get_photo_path(); 81 82 cache.insert("photo-path".into(), photos_path.to_str().unwrap().to_owned()); 83 84 match fs::metadata(&photos_path) { 85 Ok(_) => {} 86 Err(_) => { 87 fs::create_dir(&photos_path).unwrap(); 88 } 89 }; 90 91 // Listen for file updates, store each update in an mpsc channel and send to the frontend 92 let (sender, receiver) = std::sync::mpsc::channel(); 93 let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | { 94 match res { 95 Ok(event) => { 96 match event.kind{ 97 EventKind::Remove(_) => { 98 let path = event.paths.first().unwrap(); 99 let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 100 101 let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 102 let re2 = Regex::new(r"(?m)/VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png/gm").unwrap(); 103 104 if 105 re1.is_match(&name) || 106 re2.is_match(&name) 107 { 108 sender.send((2, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 109 } 110 }, 111 EventKind::Create(_) => { 112 let path = event.paths.first().unwrap(); 113 let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 114 115 let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 116 let re2 = Regex::new(r"(?m)/VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png/gm").unwrap(); 117 118 if 119 re1.is_match(&name) || 120 re2.is_match(&name) 121 { 122 thread::sleep(time::Duration::from_millis(1000)); 123 sender.send((1, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 124 } 125 }, 126 _ => {} 127 } 128 }, 129 Err(e) => println!("watch error: {:?}", e), 130 } 131 }).unwrap(); 132 133 println!("Watching dir: {:?}", util::get_photo_path::get_photo_path()); 134 watcher 135 .watch( 136 &util::get_photo_path::get_photo_path(), 137 RecursiveMode::Recursive, 138 ) 139 .unwrap(); 140 141 let clipboard = Clipboard::new().unwrap(); 142 143 tauri::Builder::default() 144 .plugin(tauri_plugin_single_instance::init(| app, _argv, _cwd | { 145 app.get_webview_window("main").unwrap().show().unwrap(); 146 })) 147 .plugin(tauri_plugin_deep_link::init()) 148 .plugin(tauri_plugin_process::init()) 149 .plugin(tauri_plugin_http::init()) 150 .plugin(tauri_plugin_shell::init()) 151 .register_asynchronous_uri_scheme_protocol("photo", |ctx, req, res| { 152 let cache: State<Cache> = ctx.app_handle().state(); 153 util::handle_uri_proto::handle_uri_proto(req, res, cache); 154 }) 155 .on_window_event(|window, event| match event { 156 WindowEvent::CloseRequested { api, .. } => { 157 let config: State<Config> = window.state(); 158 159 let val = get_config_value_string("minimise-on-close".into(), config.clone()); 160 if val.is_some() && val.unwrap() == "false"{ 161 config.save(); 162 return; 163 } 164 165 window.hide().unwrap(); 166 api.prevent_close(); 167 } 168 _ => {} 169 }) 170 .manage(Config::new()) 171 .manage(cache) 172 .manage(Mutex::new(clipboard)) 173 .setup(|app| { 174 let handle = app.handle(); 175 176 app.deep_link().register("vrcpm").unwrap(); 177 util::setup_traymenu::setup_traymenu(handle); 178 179 // reads the file update mpsc channel and sends the events to the frontend 180 let window = app.get_webview_window("main").unwrap(); 181 thread::spawn(move || { 182 thread::sleep(time::Duration::from_millis(100)); 183 184 for event in receiver { 185 match event.0 { 186 1 => { 187 window.emit("photo_create", event.1).unwrap(); 188 } 189 2 => { 190 window.emit("photo_remove", event.1).unwrap(); 191 } 192 _ => {} 193 } 194 } 195 }); 196 197 Ok(()) 198 }) 199 .invoke_handler(tauri::generate_handler![ 200 load_photos::load_photos, 201 close_splashscreen::close_splashscreen, 202 load_photo_meta::load_photo_meta, 203 delete_photo::delete_photo, 204 open_url::open_url, 205 open_folder::open_folder, 206 find_world_by_id::find_world_by_id, 207 #[cfg(windows)] 208 start_with_win::start_with_win, 209 get_user_photos_path::get_user_photos_path, 210 change_final_path::change_final_path, 211 util::get_version::get_version, 212 config::set_config_value_string, 213 config::get_config_value_string, 214 config::set_config_value_int, 215 config::get_config_value_int, 216 get_os::get_os, 217 copy_image::copy_image 218 ]) 219 .run(tauri::generate_context!()) 220 .expect("error while running tauri application"); 221}