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 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, thread };
17use tauri::{ Emitter, Manager, State, WindowEvent };
18use tauri_plugin_deep_link::DeepLinkExt;
19
20// TODO: Linux support
21
22fn main() {
23 #[cfg(target_os = "linux")]
24 std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // Fix webkit 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 // Updater only supports windows, so don't update if on linux
92 #[cfg(windows)]
93 util::check_updates::check_updates(container_folder);
94
95 // Listen for file updates, store each update in an mpsc channel and send to the frontend
96 let (sender, receiver) = std::sync::mpsc::channel();
97 let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | {
98 match res {
99 Ok(event) => {
100 match event.kind{
101 EventKind::Remove(_) => {
102 let path = event.paths.first().unwrap();
103 let name = path.file_name().unwrap().to_str().unwrap().to_owned();
104
105 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();
106 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();
107
108 if
109 re1.is_match(&name) ||
110 re2.is_match(&name)
111 {
112 sender.send((2, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap();
113 }
114 },
115 EventKind::Create(_) => {
116 let path = event.paths.first().unwrap();
117 let name = path.file_name().unwrap().to_str().unwrap().to_owned();
118
119 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();
120 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();
121
122 if
123 re1.is_match(&name) ||
124 re2.is_match(&name)
125 {
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 tauri::Builder::default()
146 .plugin(tauri_plugin_single_instance::init(| app, argv, _cwd | {
147 app.get_webview_window("main").unwrap().show().unwrap();
148 util::handle_deeplink::handle_deeplink(argv[1].clone(), app);
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 window.hide().unwrap();
161 api.prevent_close();
162 }
163 _ => {}
164 })
165 .manage(cache)
166 .setup(|app| {
167 let handle = app.handle();
168
169 app.deep_link().register("vrcpm").unwrap();
170 util::setup_traymenu::setup_traymenu(handle);
171
172 // reads the file update mpsc channel and sends the events to the frontend
173 let window = app.get_webview_window("main").unwrap();
174 thread::spawn(move || {
175 thread::sleep(time::Duration::from_millis(100));
176
177 for event in receiver {
178 match event.0 {
179 1 => {
180 window.emit("photo_create", event.1).unwrap();
181 }
182 2 => {
183 window.emit("photo_remove", event.1).unwrap();
184 }
185 _ => {}
186 }
187 }
188 });
189
190 Ok(())
191 })
192 .invoke_handler(tauri::generate_handler![
193 start_user_auth::start_user_auth,
194 load_photos::load_photos,
195 close_splashscreen::close_splashscreen,
196 load_photo_meta::load_photo_meta,
197 delete_photo::delete_photo,
198 open_url::open_url,
199 open_folder::open_folder,
200 find_world_by_id::find_world_by_id,
201 #[cfg(windows)]
202 start_with_win::start_with_win,
203 get_user_photos_path::get_user_photos_path,
204 change_final_path::change_final_path,
205 sync_photos::sync_photos,
206 util::get_version::get_version,
207 relaunch::relaunch,
208 config::set_config_value_string,
209 config::get_config_value_string,
210 config::set_config_value_int,
211 config::get_config_value_int,
212 get_os::get_os
213 ])
214 .run(tauri::generate_context!())
215 .expect("error while running tauri application");
216}