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