A photo manager for VRChat.

perhaps some bug fixes...

Changed files
+95 -25
src
src-tauri
+24 -2
src-tauri/src/main.rs
··· 148 148 fs::remove_file(p).unwrap(); 149 149 } 150 150 151 + #[tauri::command] 152 + fn change_final_path( new_path: &str ){ 153 + let config_path = dirs::picture_dir().unwrap().join(".vrchat_photos"); 154 + fs::write(&config_path, new_path.as_bytes()).unwrap(); 155 + } 156 + 151 157 fn main() { 152 158 std::env::set_var("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--ignore-gpu-blacklist"); 159 + tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 153 160 154 - tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 161 + // Double check the app has an install directory 162 + let container_folder = dirs::home_dir().unwrap().join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 163 + match fs::metadata(&container_folder){ 164 + Ok(meta) => { 165 + if meta.is_file(){ 166 + panic!("Cannot launch app as the container path is a file not a directory"); 167 + } 168 + }, 169 + Err(_) => { 170 + fs::create_dir(&container_folder).unwrap(); 171 + } 172 + } 173 + 174 + // Do auto update stuff here (once im publishing builds) 155 175 156 176 // Setup the tray icon and menu buttons 157 177 let quit = CustomMenuItem::new("quit".to_string(), "Quit"); ··· 193 213 re1.is_match(path.to_str().unwrap()) || 194 214 re2.is_match(path.to_str().unwrap()) 195 215 { 216 + thread::sleep(time::Duration::from_millis(1000)); 196 217 sender.send((1, path.clone().strip_prefix(dirs::picture_dir().unwrap().join("VRChat")).unwrap().to_path_buf())).unwrap(); 197 218 } 198 219 }, ··· 331 352 .invoke_handler(tauri::generate_handler![ 332 353 start_user_auth, load_photos, close_splashscreen, 333 354 load_photo_meta, delete_photo, open_url, 334 - find_world_by_id, start_with_win, get_user_photos_path 355 + find_world_by_id, start_with_win, get_user_photos_path, 356 + change_final_path 335 357 ]) 336 358 .run(tauri::generate_context!()) 337 359 .expect("error while running tauri application");
+12 -4
src-tauri/src/worldscraper.rs
··· 14 14 favourites: u64, 15 15 tags: String, 16 16 from: String, 17 - from_site: String 17 + from_site: String, 18 + found: bool 18 19 } 19 20 20 21 impl World{ ··· 22 23 println!("Fetching world data for {}", &world_id); 23 24 24 25 let mut world = World { 25 - id: "".into(), 26 + id: world_id.clone(), 26 27 name: "".into(), 27 28 author: "".into(), 28 29 author_id: "".into(), ··· 33 34 favourites: 0, 34 35 tags: "".into(), 35 36 from: "https://vrclist.com/worlds/".into(), 36 - from_site: "vrclist.com".into() 37 + from_site: "vrclist.com".into(), 38 + found: false 37 39 }; 38 40 39 41 let client = reqwest::blocking::Client::new(); ··· 48 50 .text() 49 51 .unwrap(); 50 52 53 + if &fixed_id_req == "" { 54 + return world; 55 + } 56 + 57 + world.found = true; 58 + 51 59 let fixed_id: serde_json::Value = serde_json::from_str(&fixed_id_req).unwrap(); 52 60 world.from = format!("https://vrclist.com/worlds/{}", fixed_id["id"].to_string()); 53 61 ··· 60 68 .json() 61 69 .unwrap(); 62 70 63 - world.id = world_id.clone(); 64 71 world.name = world_data["name"].to_string(); 65 72 world.author = world_data["authorName"].to_string(); 66 73 world.author_id = world_data["authorId"].to_string(); ··· 101 108 s.serialize_field("tags", &self.tags)?; 102 109 s.serialize_field("from", &self.from)?; 103 110 s.serialize_field("fromSite", &self.from_site)?; 111 + s.serialize_field("found", &self.found)?; 104 112 105 113 s.end() 106 114 }
+25 -17
src/Components/PhotoViewer.tsx
··· 24 24 favourites: number, 25 25 tags: any, 26 26 from: string, 27 - fromSite: string 27 + fromSite: string, 28 + found: boolean 28 29 } 29 30 } 30 31 ··· 204 205 anime({ targets: '.prev-button', top: '75%', easing: 'easeInOutQuad', duration: 100 }); 205 206 anime({ targets: '.next-button', top: '75%', easing: 'easeInOutQuad', duration: 100 }); 206 207 } 207 - 208 + 208 209 isOpen = photo != null; 209 210 }) 210 211 }) 211 212 212 213 let loadWorldData = ( data: WorldCache ) => { 213 - let tags: string[] = JSON.parse(data.worldData.tags.split('\\\\').join("").split('\\').join("").slice(1, -1)); 214 - 215 214 worldInfoContainer.innerHTML = ''; 216 215 worldInfoContainer.appendChild( 217 216 <div> 218 - <div class="world-name">{ data.worldData.name } <i onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/world/' + data.worldData.id })} style={{ "margin-left": '0px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} class="fa-solid fa-arrow-up-right-from-square"></i></div> 219 - <div style={{ width: '75%', margin: 'auto' }}>{ data.worldData.desc }</div> 217 + <Show when={ data.worldData.found == false && props.currentPhotoView().metadata }> 218 + <div> 219 + <div class="world-name">{ JSON.parse(props.currentPhotoView().metadata).world.name } <i onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/world/' + data.worldData.id })} style={{ "margin-left": '0px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} class="fa-solid fa-arrow-up-right-from-square"></i></div> 220 + <div style={{ width: '75%', margin: 'auto' }}>Could not fetch world information... Is the world private?</div> 221 + </div> 222 + </Show> 223 + <Show when={ data.worldData.found == true }> 224 + <div class="world-name">{ data.worldData.name } <i onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/world/' + data.worldData.id })} style={{ "margin-left": '0px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} class="fa-solid fa-arrow-up-right-from-square"></i></div> 225 + <div style={{ width: '75%', margin: 'auto' }}>{ data.worldData.desc }</div> 220 226 221 - <br /> 222 - <div class="world-tags"> 223 - <For each={tags}> 224 - {( tag ) => 225 - <div>{ tag.replace("author_tag_", "").replace("system_", "") }</div> 226 - } 227 - </For> 228 - </div> 227 + <br /> 228 + <div class="world-tags"> 229 + <For each={JSON.parse(data.worldData.tags.split('\\\\').join("").split('\\').join("").slice(1, -1))}> 230 + {( tag ) => 231 + <div>{ tag.replace("author_tag_", "").replace("system_", "") }</div> 232 + } 233 + </For> 234 + </div> 235 + </Show> 229 236 </div> as Node 230 237 ) 231 238 } ··· 245 252 favourites: event.payload.favourites, 246 253 tags: event.payload.tags, 247 254 from: event.payload.from, 248 - fromSite: event.payload.fromSite 255 + fromSite: event.payload.fromSite, 256 + found: event.payload.found 249 257 } 250 258 } 251 259 252 - loadWorldData(worldData); 253 - 254 260 worldCache.push(worldData); 255 261 localStorage.setItem("worldCache", JSON.stringify(worldCache)); 262 + 263 + loadWorldData(worldData); 256 264 }) 257 265 258 266 return (
+33 -2
src/Components/SettingsMenu.tsx
··· 13 13 let settingsContainer: HTMLElement; 14 14 let currentButton = 0; 15 15 let lastClickedButton = -1; 16 + let finalPathConfirm: HTMLElement; 17 + let finalPathInput: HTMLElement; 18 + let finalPathData: string; 19 + let finalPathPreviousData: string; 16 20 17 21 onMount(() => { 18 22 let sliderMouseDown = false; ··· 192 196 </div> 193 197 194 198 <br /> 195 - <p>VRChat Photo Path: <span class="path" ref={( el ) => invoke('get_user_photos_path').then(( path: any ) => { el.innerHTML = path; console.log(path) })}>Loading...</span></p> 196 - <p>Final Photo Path: <span class="path" ref={( el ) => invoke('get_user_photos_path').then(( path: any ) => { el.innerHTML = path; console.log(path) })}>Loading...</span></p> 199 + <p>VRChat Photo Path: <span class="path" ref={( el ) => invoke('get_user_photos_path').then(( path: any ) => el.innerHTML = path)}>Loading...</span></p> 200 + <p> 201 + Final Photo Path: 202 + <span class="path" ref={( el ) => 203 + invoke('get_user_photos_path').then(( path: any ) => { 204 + el.innerHTML = ''; 205 + el.appendChild(<span style={{ outline: 'none' }} ref={( el ) => finalPathInput = el} onInput={( el ) => { 206 + finalPathConfirm.style.display = 'inline-block'; 207 + finalPathData = el.target.innerHTML; 208 + }} contenteditable>{path}</span> as Node); 209 + 210 + finalPathPreviousData = path; 211 + }) 212 + }> 213 + Loading... 214 + </span> 215 + <span style={{ display: 'none' }} ref={( el ) => finalPathConfirm = el}> 216 + <span class="path" style={{ color: 'green' }} onClick={() => { 217 + finalPathPreviousData = finalPathData; 218 + finalPathConfirm.style.display = 'none'; 219 + }}><i class="fa-solid fa-check"></i></span> 220 + 221 + <span class="path" style={{ color: 'red' }} onClick={() => { 222 + finalPathData = finalPathPreviousData; 223 + finalPathInput.innerHTML = finalPathPreviousData; 224 + finalPathConfirm.style.display = 'none'; 225 + }}><i class="fa-solid fa-xmark"></i></span> 226 + </span> 227 + </p> 197 228 </div> 198 229 <div class="settings-block"> 199 230 <h1>Account Settings</h1>
+1
src/styles.css
··· 564 564 background: #000a; 565 565 border-radius: 5px; 566 566 margin-left: 5px; 567 + cursor: pointer; 567 568 }