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 }; 18 19use crate::frontend_calls::config::{get_config_value_string, Config}; 20 21fn main() { 22 #[cfg(target_os = "linux")] 23 std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // Fix webkitgtk being shit 24 25 let cache = Cache::new(); 26 27 // Double check the app has an install directory 28 let container_folder = dirs::config_dir() 29 .unwrap() 30 .join("PhazeDev/VRChatPhotoManager"); 31 32 match fs::metadata(&container_folder) { 33 Ok(meta) => { 34 if meta.is_file() { 35 panic!("Cannot launch app as the container path is a file not a directory"); 36 } 37 } 38 Err(_) => { 39 let folder = dirs::config_dir().unwrap(); 40 match fs::metadata(&folder) { 41 Ok(meta) => { 42 if meta.is_file() { 43 panic!("Cannot launch app as the container path is a file not a directory"); 44 } 45 } 46 Err(_) => { 47 fs::create_dir(&folder).unwrap(); 48 } 49 } 50 51 let phaz_folder = dirs::config_dir().unwrap().join("PhazeDev"); 52 match fs::metadata(&phaz_folder) { 53 Ok(meta) => { 54 if meta.is_file() { 55 panic!("Cannot launch app as the container path is a file not a directory"); 56 } 57 } 58 Err(_) => { 59 fs::create_dir(&phaz_folder).unwrap(); 60 } 61 } 62 63 fs::create_dir(&container_folder).unwrap(); 64 } 65 } 66 67 let sync_lock_path = dirs::config_dir() 68 .unwrap() 69 .join("PhazeDev/VRChatPhotoManager/.sync_lock"); 70 71 match fs::metadata(&sync_lock_path) { 72 Ok(_) => { 73 fs::remove_file(&sync_lock_path).unwrap(); 74 } 75 Err(_) => {} 76 } 77 78 println!("Loading App..."); 79 let photos_path = util::get_photo_path::get_photo_path(); 80 println!("Loading photos from: {:#?}", &photos_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_match = // This is the current format used by VRChat 102 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]{3,4}x[0-9]{3,4}.png").unwrap().is_match(&name) || 103 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]{3,4}x[0-9]{3,4}_Player.png").unwrap().is_match(&name) || 104 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]{3,4}x[0-9]{3,4}_Environment.png").unwrap().is_match(&name); 105 106 let re2_match = // This is the format VRCX uses if you enable renaming photos 107 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]{3,4}x[0-9]{3,4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png").unwrap().is_match(&name); 108 109 if re1_match || re2_match{ 110 sender.send((2, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 111 } 112 }, 113 EventKind::Create(_) => { 114 let path = event.paths.first().unwrap(); 115 let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 116 117 let re1_match = // This is the current format used by VRChat 118 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]{3,4}x[0-9]{3,4}.png").unwrap().is_match(&name) || 119 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]{3,4}x[0-9]{3,4}_Player.png").unwrap().is_match(&name) || 120 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]{3,4}x[0-9]{3,4}_Environment.png").unwrap().is_match(&name); 121 122 let re2_match = // This is the format VRCX uses if you enable renaming photos 123 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]{3,4}x[0-9]{3,4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png").unwrap().is_match(&name); 124 125 if re1_match || re2_match{ 126 thread::sleep(time::Duration::from_millis(1000)); 127 sender.send((1, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 128 } 129 }, 130 _ => {} 131 } 132 }, 133 Err(e) => println!("watch error: {:?}", e), 134 } 135 }).unwrap(); 136 137 println!("Watching dir: {:?}", util::get_photo_path::get_photo_path()); 138 watcher 139 .watch( 140 &util::get_photo_path::get_photo_path(), 141 RecursiveMode::Recursive, 142 ) 143 .unwrap(); 144 145 let clipboard = Clipboard::new().unwrap(); 146 147 tauri::Builder::default() 148 .plugin(tauri_plugin_single_instance::init(| app, _argv, _cwd | { 149 app.get_webview_window("main").unwrap().show().unwrap(); 150 })) 151 .plugin(tauri_plugin_process::init()) 152 .plugin(tauri_plugin_http::init()) 153 .plugin(tauri_plugin_shell::init()) 154 .register_asynchronous_uri_scheme_protocol("photo", |ctx, req, res| { 155 let cache: State<Cache> = ctx.app_handle().state(); 156 util::handle_uri_proto::handle_uri_proto(req, res, cache); 157 }) 158 .on_window_event(|window, event| match event { 159 WindowEvent::CloseRequested { api, .. } => { 160 let config: State<Config> = window.state(); 161 162 let val = get_config_value_string("close-to-tray".into(), config.clone()); 163 if val.is_none() || val.unwrap() != "true"{ 164 config.save(); 165 return; 166 } 167 168 window.hide().unwrap(); 169 api.prevent_close(); 170 } 171 _ => {} 172 }) 173 .manage(Config::new()) 174 .manage(cache) 175 .manage(Mutex::new(clipboard)) 176 .setup(|app| { 177 let handle = app.handle(); 178 util::setup_traymenu::setup_traymenu(handle); 179 180 // reads the file update mpsc channel and sends the events to the frontend 181 let window = app.get_webview_window("main").unwrap(); 182 thread::spawn(move || { 183 thread::sleep(time::Duration::from_millis(100)); 184 185 for event in receiver { 186 match event.0 { 187 1 => { 188 window.emit("photo_create", event.1).unwrap(); 189 } 190 2 => { 191 window.emit("photo_remove", event.1).unwrap(); 192 } 193 _ => {} 194 } 195 } 196 }); 197 198 Ok(()) 199 }) 200 .invoke_handler(tauri::generate_handler![ 201 load_photos::load_photos, 202 close_splashscreen::close_splashscreen, 203 load_photo_meta::load_photo_meta, 204 delete_photo::delete_photo, 205 open_url::open_url, 206 open_folder::open_folder, 207 find_world_by_id::find_world_by_id, 208 #[cfg(windows)] 209 start_with_win::start_with_win, 210 get_user_photos_path::get_user_photos_path, 211 change_final_path::change_final_path, 212 util::get_version::get_version, 213 config::set_config_value_string, 214 config::get_config_value_string, 215 config::set_config_value_int, 216 config::get_config_value_int, 217 get_os::get_os, 218 copy_image::copy_image 219 ]) 220 .run(tauri::generate_context!()) 221 .expect("error while running tauri application"); 222}