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}