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 81 cache.insert("photo-path".into(), photos_path.to_str().unwrap().to_owned()); 82 83 match fs::metadata(&photos_path) { 84 Ok(_) => {} 85 Err(_) => { 86 fs::create_dir(&photos_path).unwrap(); 87 } 88 }; 89 90 // Listen for file updates, store each update in an mpsc channel and send to the frontend 91 let (sender, receiver) = std::sync::mpsc::channel(); 92 let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | { 93 match res { 94 Ok(event) => { 95 match event.kind{ 96 EventKind::Remove(_) => { 97 let path = event.paths.first().unwrap(); 98 let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 99 100 let re1_match = // This is the current format used by VRChat 101 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) || 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}_Player.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}_Environment.png").unwrap().is_match(&name); 104 105 let re2_match = // This is the format VRCX uses if you enable renaming photos 106 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); 107 108 if re1_match || re2_match{ 109 sender.send((2, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 110 } 111 }, 112 EventKind::Create(_) => { 113 let path = event.paths.first().unwrap(); 114 let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 115 116 let re1_match = // This is the current format used by VRChat 117 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) || 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}_Player.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}_Environment.png").unwrap().is_match(&name); 120 121 let re2_match = // This is the format VRCX uses if you enable renaming photos 122 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); 123 124 if re1_match || re2_match{ 125 thread::sleep(time::Duration::from_millis(1000)); 126 sender.send((1, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 127 } 128 }, 129 _ => {} 130 } 131 }, 132 Err(e) => println!("watch error: {:?}", e), 133 } 134 }).unwrap(); 135 136 println!("Watching dir: {:?}", util::get_photo_path::get_photo_path()); 137 watcher 138 .watch( 139 &util::get_photo_path::get_photo_path(), 140 RecursiveMode::Recursive, 141 ) 142 .unwrap(); 143 144 let clipboard = Clipboard::new().unwrap(); 145 146 tauri::Builder::default() 147 .plugin(tauri_plugin_single_instance::init(| app, _argv, _cwd | { 148 app.get_webview_window("main").unwrap().show().unwrap(); 149 })) 150 .plugin(tauri_plugin_process::init()) 151 .plugin(tauri_plugin_http::init()) 152 .plugin(tauri_plugin_shell::init()) 153 .register_asynchronous_uri_scheme_protocol("photo", |ctx, req, res| { 154 let cache: State<Cache> = ctx.app_handle().state(); 155 util::handle_uri_proto::handle_uri_proto(req, res, cache); 156 }) 157 .on_window_event(|window, event| match event { 158 WindowEvent::CloseRequested { api, .. } => { 159 let config: State<Config> = window.state(); 160 161 let val = get_config_value_string("close-to-tray".into(), config.clone()); 162 if val.is_none() || val.unwrap() != "true"{ 163 config.save(); 164 return; 165 } 166 167 window.hide().unwrap(); 168 api.prevent_close(); 169 } 170 _ => {} 171 }) 172 .manage(Config::new()) 173 .manage(cache) 174 .manage(Mutex::new(clipboard)) 175 .setup(|app| { 176 let handle = app.handle(); 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}