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
21use crate::frontend_calls::config::get_config_value_string;
22
23// TODO: Linux support
24
25fn main() {
26 #[cfg(target_os = "linux")]
27 std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // Fix webkitgtk being shit
28
29 let cache = Cache::new();
30
31 // Double check the app has an install directory
32 let container_folder = dirs::config_dir()
33 .unwrap()
34 .join("PhazeDev/VRChatPhotoManager");
35
36 match fs::metadata(&container_folder) {
37 Ok(meta) => {
38 if meta.is_file() {
39 panic!("Cannot launch app as the container path is a file not a directory");
40 }
41 }
42 Err(_) => {
43 let folder = dirs::config_dir().unwrap();
44 match fs::metadata(&folder) {
45 Ok(meta) => {
46 if meta.is_file() {
47 panic!("Cannot launch app as the container path is a file not a directory");
48 }
49 }
50 Err(_) => {
51 fs::create_dir(&folder).unwrap();
52 }
53 }
54
55 let phaz_folder = dirs::config_dir().unwrap().join("PhazeDev");
56 match fs::metadata(&phaz_folder) {
57 Ok(meta) => {
58 if meta.is_file() {
59 panic!("Cannot launch app as the container path is a file not a directory");
60 }
61 }
62 Err(_) => {
63 fs::create_dir(&phaz_folder).unwrap();
64 }
65 }
66
67 fs::create_dir(&container_folder).unwrap();
68 }
69 }
70
71 let sync_lock_path = dirs::config_dir()
72 .unwrap()
73 .join("PhazeDev/VRChatPhotoManager/.sync_lock");
74
75 match fs::metadata(&sync_lock_path) {
76 Ok(_) => {
77 fs::remove_file(&sync_lock_path).unwrap();
78 }
79 Err(_) => {}
80 }
81
82 println!("Loading App...");
83 let photos_path = util::get_photo_path::get_photo_path();
84
85 cache.insert("photo-path".into(), photos_path.to_str().unwrap().to_owned());
86
87 match fs::metadata(&photos_path) {
88 Ok(_) => {}
89 Err(_) => {
90 fs::create_dir(&photos_path).unwrap();
91 }
92 };
93
94 // Listen for file updates, store each update in an mpsc channel and send to the frontend
95 let (sender, receiver) = std::sync::mpsc::channel();
96 let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | {
97 match res {
98 Ok(event) => {
99 match event.kind{
100 EventKind::Remove(_) => {
101 let path = event.paths.first().unwrap();
102 let name = path.file_name().unwrap().to_str().unwrap().to_owned();
103
104 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();
105 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();
106
107 if
108 re1.is_match(&name) ||
109 re2.is_match(&name)
110 {
111 sender.send((2, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap();
112 }
113 },
114 EventKind::Create(_) => {
115 let path = event.paths.first().unwrap();
116 let name = path.file_name().unwrap().to_str().unwrap().to_owned();
117
118 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();
119 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();
120
121 if
122 re1.is_match(&name) ||
123 re2.is_match(&name)
124 {
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_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 let val = get_config_value_string("minimise-on-close".into());
161 if val.is_some() && val.unwrap() == "false"{ return; }
162
163 window.hide().unwrap();
164 api.prevent_close();
165 }
166 _ => {}
167 })
168 .manage(cache)
169 .manage(Mutex::new(clipboard))
170 .setup(|app| {
171 let handle = app.handle();
172
173 app.deep_link().register("vrcpm").unwrap();
174 util::setup_traymenu::setup_traymenu(handle);
175
176 // reads the file update mpsc channel and sends the events to the frontend
177 let window = app.get_webview_window("main").unwrap();
178 thread::spawn(move || {
179 thread::sleep(time::Duration::from_millis(100));
180
181 for event in receiver {
182 match event.0 {
183 1 => {
184 window.emit("photo_create", event.1).unwrap();
185 }
186 2 => {
187 window.emit("photo_remove", event.1).unwrap();
188 }
189 _ => {}
190 }
191 }
192 });
193
194 Ok(())
195 })
196 .invoke_handler(tauri::generate_handler![
197 load_photos::load_photos,
198 close_splashscreen::close_splashscreen,
199 load_photo_meta::load_photo_meta,
200 delete_photo::delete_photo,
201 open_url::open_url,
202 open_folder::open_folder,
203 find_world_by_id::find_world_by_id,
204 #[cfg(windows)]
205 start_with_win::start_with_win,
206 get_user_photos_path::get_user_photos_path,
207 change_final_path::change_final_path,
208 sync_photos::sync_photos,
209 util::get_version::get_version,
210 config::set_config_value_string,
211 config::get_config_value_string,
212 config::set_config_value_int,
213 config::get_config_value_int,
214 get_os::get_os,
215 copy_image::copy_image
216 ])
217 .run(tauri::generate_context!())
218 .expect("error while running tauri application");
219}