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 };
18use tauri_plugin_deep_link::DeepLinkExt;
19
20use crate::frontend_calls::config::{get_config_value_string, Config};
21
22fn main() {
23 #[cfg(target_os = "linux")]
24 std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // Fix webkitgtk 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 // 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 = 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();
102 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();
103
104 if
105 re1.is_match(&name) ||
106 re2.is_match(&name)
107 {
108 sender.send((2, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap();
109 }
110 },
111 EventKind::Create(_) => {
112 let path = event.paths.first().unwrap();
113 let name = path.file_name().unwrap().to_str().unwrap().to_owned();
114
115 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();
116 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();
117
118 if
119 re1.is_match(&name) ||
120 re2.is_match(&name)
121 {
122 thread::sleep(time::Duration::from_millis(1000));
123 sender.send((1, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap();
124 }
125 },
126 _ => {}
127 }
128 },
129 Err(e) => println!("watch error: {:?}", e),
130 }
131 }).unwrap();
132
133 println!("Watching dir: {:?}", util::get_photo_path::get_photo_path());
134 watcher
135 .watch(
136 &util::get_photo_path::get_photo_path(),
137 RecursiveMode::Recursive,
138 )
139 .unwrap();
140
141 let clipboard = Clipboard::new().unwrap();
142
143 tauri::Builder::default()
144 .plugin(tauri_plugin_single_instance::init(| app, _argv, _cwd | {
145 app.get_webview_window("main").unwrap().show().unwrap();
146 }))
147 .plugin(tauri_plugin_deep_link::init())
148 .plugin(tauri_plugin_process::init())
149 .plugin(tauri_plugin_http::init())
150 .plugin(tauri_plugin_shell::init())
151 .register_asynchronous_uri_scheme_protocol("photo", |ctx, req, res| {
152 let cache: State<Cache> = ctx.app_handle().state();
153 util::handle_uri_proto::handle_uri_proto(req, res, cache);
154 })
155 .on_window_event(|window, event| match event {
156 WindowEvent::CloseRequested { api, .. } => {
157 let config: State<Config> = window.state();
158
159 let val = get_config_value_string("minimise-on-close".into(), config.clone());
160 if val.is_some() && val.unwrap() == "false"{
161 config.save();
162 return;
163 }
164
165 window.hide().unwrap();
166 api.prevent_close();
167 }
168 _ => {}
169 })
170 .manage(Config::new())
171 .manage(cache)
172 .manage(Mutex::new(clipboard))
173 .setup(|app| {
174 let handle = app.handle();
175
176 app.deep_link().register("vrcpm").unwrap();
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}