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
21// TODO: Linux support
22
23fn main() {
24 #[cfg(target_os = "linux")]
25 std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // Fix webkitgtk being shit
26
27 let cache = Cache::new();
28
29 // Double check the app has an install directory
30 let container_folder = dirs::config_dir()
31 .unwrap()
32 .join("PhazeDev/VRChatPhotoManager");
33
34 match fs::metadata(&container_folder) {
35 Ok(meta) => {
36 if meta.is_file() {
37 panic!("Cannot launch app as the container path is a file not a directory");
38 }
39 }
40 Err(_) => {
41 let folder = dirs::config_dir().unwrap();
42 match fs::metadata(&folder) {
43 Ok(meta) => {
44 if meta.is_file() {
45 panic!("Cannot launch app as the container path is a file not a directory");
46 }
47 }
48 Err(_) => {
49 fs::create_dir(&folder).unwrap();
50 }
51 }
52
53 let phaz_folder = dirs::config_dir().unwrap().join("PhazeDev");
54 match fs::metadata(&phaz_folder) {
55 Ok(meta) => {
56 if meta.is_file() {
57 panic!("Cannot launch app as the container path is a file not a directory");
58 }
59 }
60 Err(_) => {
61 fs::create_dir(&phaz_folder).unwrap();
62 }
63 }
64
65 fs::create_dir(&container_folder).unwrap();
66 }
67 }
68
69 let sync_lock_path = dirs::config_dir()
70 .unwrap()
71 .join("PhazeDev/VRChatPhotoManager/.sync_lock");
72
73 match fs::metadata(&sync_lock_path) {
74 Ok(_) => {
75 fs::remove_file(&sync_lock_path).unwrap();
76 }
77 Err(_) => {}
78 }
79
80 println!("Loading App...");
81 let photos_path = util::get_photo_path::get_photo_path();
82
83 cache.insert("photo-path".into(), photos_path.to_str().unwrap().to_owned());
84
85 match fs::metadata(&photos_path) {
86 Ok(_) => {}
87 Err(_) => {
88 fs::create_dir(&photos_path).unwrap();
89 }
90 };
91
92 // Listen for file updates, store each update in an mpsc channel and send to the frontend
93 let (sender, receiver) = std::sync::mpsc::channel();
94 let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | {
95 match res {
96 Ok(event) => {
97 match event.kind{
98 EventKind::Remove(_) => {
99 let path = event.paths.first().unwrap();
100 let name = path.file_name().unwrap().to_str().unwrap().to_owned();
101
102 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();
103 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();
104
105 if
106 re1.is_match(&name) ||
107 re2.is_match(&name)
108 {
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 = 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();
117 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();
118
119 if
120 re1.is_match(&name) ||
121 re2.is_match(&name)
122 {
123 thread::sleep(time::Duration::from_millis(1000));
124 sender.send((1, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap();
125 }
126 },
127 _ => {}
128 }
129 },
130 Err(e) => println!("watch error: {:?}", e),
131 }
132 }).unwrap();
133
134 println!("Watching dir: {:?}", util::get_photo_path::get_photo_path());
135 watcher
136 .watch(
137 &util::get_photo_path::get_photo_path(),
138 RecursiveMode::Recursive,
139 )
140 .unwrap();
141
142 let clipboard = Clipboard::new().unwrap();
143
144 tauri::Builder::default()
145 .plugin(tauri_plugin_single_instance::init(| app, _argv, _cwd | {
146 app.get_webview_window("main").unwrap().show().unwrap();
147 }))
148 .plugin(tauri_plugin_deep_link::init())
149 .plugin(tauri_plugin_process::init())
150 .plugin(tauri_plugin_http::init())
151 .plugin(tauri_plugin_shell::init())
152 .register_asynchronous_uri_scheme_protocol("photo", |ctx, req, res| {
153 let cache: State<Cache> = ctx.app_handle().state();
154 util::handle_uri_proto::handle_uri_proto(req, res, cache);
155 })
156 .on_window_event(|window, event| match event {
157 WindowEvent::CloseRequested { api, .. } => {
158 window.hide().unwrap();
159 api.prevent_close();
160 }
161 _ => {}
162 })
163 .manage(cache)
164 .manage(Mutex::new(clipboard))
165 .setup(|app| {
166 let handle = app.handle();
167
168 app.deep_link().register("vrcpm").unwrap();
169 util::setup_traymenu::setup_traymenu(handle);
170
171 // reads the file update mpsc channel and sends the events to the frontend
172 let window = app.get_webview_window("main").unwrap();
173 thread::spawn(move || {
174 thread::sleep(time::Duration::from_millis(100));
175
176 for event in receiver {
177 match event.0 {
178 1 => {
179 window.emit("photo_create", event.1).unwrap();
180 }
181 2 => {
182 window.emit("photo_remove", event.1).unwrap();
183 }
184 _ => {}
185 }
186 }
187 });
188
189 Ok(())
190 })
191 .invoke_handler(tauri::generate_handler![
192 load_photos::load_photos,
193 close_splashscreen::close_splashscreen,
194 load_photo_meta::load_photo_meta,
195 delete_photo::delete_photo,
196 open_url::open_url,
197 open_folder::open_folder,
198 find_world_by_id::find_world_by_id,
199 #[cfg(windows)]
200 start_with_win::start_with_win,
201 get_user_photos_path::get_user_photos_path,
202 change_final_path::change_final_path,
203 sync_photos::sync_photos,
204 util::get_version::get_version,
205 config::set_config_value_string,
206 config::get_config_value_string,
207 config::set_config_value_int,
208 config::get_config_value_int,
209 get_os::get_os,
210 copy_image::copy_image
211 ])
212 .run(tauri::generate_context!())
213 .expect("error while running tauri application");
214}