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}