A photo manager for VRChat.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

perhaps some bug fixes...

+95 -25
+24 -2
src-tauri/src/main.rs
··· 148 fs::remove_file(p).unwrap(); 149 } 150 151 fn main() { 152 std::env::set_var("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--ignore-gpu-blacklist"); 153 154 - tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 155 156 // Setup the tray icon and menu buttons 157 let quit = CustomMenuItem::new("quit".to_string(), "Quit"); ··· 193 re1.is_match(path.to_str().unwrap()) || 194 re2.is_match(path.to_str().unwrap()) 195 { 196 sender.send((1, path.clone().strip_prefix(dirs::picture_dir().unwrap().join("VRChat")).unwrap().to_path_buf())).unwrap(); 197 } 198 }, ··· 331 .invoke_handler(tauri::generate_handler![ 332 start_user_auth, load_photos, close_splashscreen, 333 load_photo_meta, delete_photo, open_url, 334 - find_world_by_id, start_with_win, get_user_photos_path 335 ]) 336 .run(tauri::generate_context!()) 337 .expect("error while running tauri application");
··· 148 fs::remove_file(p).unwrap(); 149 } 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 + 157 fn main() { 158 std::env::set_var("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--ignore-gpu-blacklist"); 159 + tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 160 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) 175 176 // Setup the tray icon and menu buttons 177 let quit = CustomMenuItem::new("quit".to_string(), "Quit"); ··· 213 re1.is_match(path.to_str().unwrap()) || 214 re2.is_match(path.to_str().unwrap()) 215 { 216 + thread::sleep(time::Duration::from_millis(1000)); 217 sender.send((1, path.clone().strip_prefix(dirs::picture_dir().unwrap().join("VRChat")).unwrap().to_path_buf())).unwrap(); 218 } 219 }, ··· 352 .invoke_handler(tauri::generate_handler![ 353 start_user_auth, load_photos, close_splashscreen, 354 load_photo_meta, delete_photo, open_url, 355 + find_world_by_id, start_with_win, get_user_photos_path, 356 + change_final_path 357 ]) 358 .run(tauri::generate_context!()) 359 .expect("error while running tauri application");
+12 -4
src-tauri/src/worldscraper.rs
··· 14 favourites: u64, 15 tags: String, 16 from: String, 17 - from_site: String 18 } 19 20 impl World{ ··· 22 println!("Fetching world data for {}", &world_id); 23 24 let mut world = World { 25 - id: "".into(), 26 name: "".into(), 27 author: "".into(), 28 author_id: "".into(), ··· 33 favourites: 0, 34 tags: "".into(), 35 from: "https://vrclist.com/worlds/".into(), 36 - from_site: "vrclist.com".into() 37 }; 38 39 let client = reqwest::blocking::Client::new(); ··· 48 .text() 49 .unwrap(); 50 51 let fixed_id: serde_json::Value = serde_json::from_str(&fixed_id_req).unwrap(); 52 world.from = format!("https://vrclist.com/worlds/{}", fixed_id["id"].to_string()); 53 ··· 60 .json() 61 .unwrap(); 62 63 - world.id = world_id.clone(); 64 world.name = world_data["name"].to_string(); 65 world.author = world_data["authorName"].to_string(); 66 world.author_id = world_data["authorId"].to_string(); ··· 101 s.serialize_field("tags", &self.tags)?; 102 s.serialize_field("from", &self.from)?; 103 s.serialize_field("fromSite", &self.from_site)?; 104 105 s.end() 106 }
··· 14 favourites: u64, 15 tags: String, 16 from: String, 17 + from_site: String, 18 + found: bool 19 } 20 21 impl World{ ··· 23 println!("Fetching world data for {}", &world_id); 24 25 let mut world = World { 26 + id: world_id.clone(), 27 name: "".into(), 28 author: "".into(), 29 author_id: "".into(), ··· 34 favourites: 0, 35 tags: "".into(), 36 from: "https://vrclist.com/worlds/".into(), 37 + from_site: "vrclist.com".into(), 38 + found: false 39 }; 40 41 let client = reqwest::blocking::Client::new(); ··· 50 .text() 51 .unwrap(); 52 53 + if &fixed_id_req == "" { 54 + return world; 55 + } 56 + 57 + world.found = true; 58 + 59 let fixed_id: serde_json::Value = serde_json::from_str(&fixed_id_req).unwrap(); 60 world.from = format!("https://vrclist.com/worlds/{}", fixed_id["id"].to_string()); 61 ··· 68 .json() 69 .unwrap(); 70 71 world.name = world_data["name"].to_string(); 72 world.author = world_data["authorName"].to_string(); 73 world.author_id = world_data["authorId"].to_string(); ··· 108 s.serialize_field("tags", &self.tags)?; 109 s.serialize_field("from", &self.from)?; 110 s.serialize_field("fromSite", &self.from_site)?; 111 + s.serialize_field("found", &self.found)?; 112 113 s.end() 114 }
+25 -17
src/Components/PhotoViewer.tsx
··· 24 favourites: number, 25 tags: any, 26 from: string, 27 - fromSite: string 28 } 29 } 30 ··· 204 anime({ targets: '.prev-button', top: '75%', easing: 'easeInOutQuad', duration: 100 }); 205 anime({ targets: '.next-button', top: '75%', easing: 'easeInOutQuad', duration: 100 }); 206 } 207 - 208 isOpen = photo != null; 209 }) 210 }) 211 212 let loadWorldData = ( data: WorldCache ) => { 213 - let tags: string[] = JSON.parse(data.worldData.tags.split('\\\\').join("").split('\\').join("").slice(1, -1)); 214 - 215 worldInfoContainer.innerHTML = ''; 216 worldInfoContainer.appendChild( 217 <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> 220 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> 229 </div> as Node 230 ) 231 } ··· 245 favourites: event.payload.favourites, 246 tags: event.payload.tags, 247 from: event.payload.from, 248 - fromSite: event.payload.fromSite 249 } 250 } 251 252 - loadWorldData(worldData); 253 - 254 worldCache.push(worldData); 255 localStorage.setItem("worldCache", JSON.stringify(worldCache)); 256 }) 257 258 return (
··· 24 favourites: number, 25 tags: any, 26 from: string, 27 + fromSite: string, 28 + found: boolean 29 } 30 } 31 ··· 205 anime({ targets: '.prev-button', top: '75%', easing: 'easeInOutQuad', duration: 100 }); 206 anime({ targets: '.next-button', top: '75%', easing: 'easeInOutQuad', duration: 100 }); 207 } 208 + 209 isOpen = photo != null; 210 }) 211 }) 212 213 let loadWorldData = ( data: WorldCache ) => { 214 worldInfoContainer.innerHTML = ''; 215 worldInfoContainer.appendChild( 216 <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> 226 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> 236 </div> as Node 237 ) 238 } ··· 252 favourites: event.payload.favourites, 253 tags: event.payload.tags, 254 from: event.payload.from, 255 + fromSite: event.payload.fromSite, 256 + found: event.payload.found 257 } 258 } 259 260 worldCache.push(worldData); 261 localStorage.setItem("worldCache", JSON.stringify(worldCache)); 262 + 263 + loadWorldData(worldData); 264 }) 265 266 return (
+33 -2
src/Components/SettingsMenu.tsx
··· 13 let settingsContainer: HTMLElement; 14 let currentButton = 0; 15 let lastClickedButton = -1; 16 17 onMount(() => { 18 let sliderMouseDown = false; ··· 192 </div> 193 194 <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> 197 </div> 198 <div class="settings-block"> 199 <h1>Account Settings</h1>
··· 13 let settingsContainer: HTMLElement; 14 let currentButton = 0; 15 let lastClickedButton = -1; 16 + let finalPathConfirm: HTMLElement; 17 + let finalPathInput: HTMLElement; 18 + let finalPathData: string; 19 + let finalPathPreviousData: string; 20 21 onMount(() => { 22 let sliderMouseDown = false; ··· 196 </div> 197 198 <br /> 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> 228 </div> 229 <div class="settings-block"> 230 <h1>Account Settings</h1>
+1
src/styles.css
··· 564 background: #000a; 565 border-radius: 5px; 566 margin-left: 5px; 567 }
··· 564 background: #000a; 565 border-radius: 5px; 566 margin-left: 5px; 567 + cursor: pointer; 568 }