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 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, thread }; 17use tauri::{ Emitter, Manager, State, WindowEvent }; 18use tauri_plugin_deep_link::DeepLinkExt; 19 20// TODO: Linux support 21 22fn main() { 23 #[cfg(target_os = "linux")] 24 std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // Fix webkit 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 // Updater only supports windows, so don't update if on linux 92 #[cfg(windows)] 93 util::check_updates::check_updates(container_folder); 94 95 // Listen for file updates, store each update in an mpsc channel and send to the frontend 96 let (sender, receiver) = std::sync::mpsc::channel(); 97 let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | { 98 match res { 99 Ok(event) => { 100 match event.kind{ 101 EventKind::Remove(_) => { 102 let path = event.paths.first().unwrap(); 103 let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 104 105 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(); 106 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(); 107 108 if 109 re1.is_match(&name) || 110 re2.is_match(&name) 111 { 112 sender.send((2, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 113 } 114 }, 115 EventKind::Create(_) => { 116 let path = event.paths.first().unwrap(); 117 let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 118 119 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(); 120 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(); 121 122 if 123 re1.is_match(&name) || 124 re2.is_match(&name) 125 { 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 tauri::Builder::default() 146 .plugin(tauri_plugin_single_instance::init(| app, argv, _cwd | { 147 app.get_webview_window("main").unwrap().show().unwrap(); 148 util::handle_deeplink::handle_deeplink(argv[1].clone(), app); 149 })) 150 .plugin(tauri_plugin_deep_link::init()) 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 window.hide().unwrap(); 161 api.prevent_close(); 162 } 163 _ => {} 164 }) 165 .manage(cache) 166 .setup(|app| { 167 let handle = app.handle(); 168 169 app.deep_link().register("vrcpm").unwrap(); 170 util::setup_traymenu::setup_traymenu(handle); 171 172 // reads the file update mpsc channel and sends the events to the frontend 173 let window = app.get_webview_window("main").unwrap(); 174 thread::spawn(move || { 175 thread::sleep(time::Duration::from_millis(100)); 176 177 for event in receiver { 178 match event.0 { 179 1 => { 180 window.emit("photo_create", event.1).unwrap(); 181 } 182 2 => { 183 window.emit("photo_remove", event.1).unwrap(); 184 } 185 _ => {} 186 } 187 } 188 }); 189 190 Ok(()) 191 }) 192 .invoke_handler(tauri::generate_handler![ 193 start_user_auth::start_user_auth, 194 load_photos::load_photos, 195 close_splashscreen::close_splashscreen, 196 load_photo_meta::load_photo_meta, 197 delete_photo::delete_photo, 198 open_url::open_url, 199 open_folder::open_folder, 200 find_world_by_id::find_world_by_id, 201 #[cfg(windows)] 202 start_with_win::start_with_win, 203 get_user_photos_path::get_user_photos_path, 204 change_final_path::change_final_path, 205 sync_photos::sync_photos, 206 util::get_version::get_version, 207 relaunch::relaunch, 208 config::set_config_value_string, 209 config::get_config_value_string, 210 config::set_config_value_int, 211 config::get_config_value_int, 212 get_os::get_os 213 ]) 214 .run(tauri::generate_context!()) 215 .expect("error while running tauri application"); 216}