A photo manager for VRChat.
0
fork

Configure Feed

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

jlkdfgjhslk;jg sedlk;f utgkjerdfgh k;lredhfgvlkjdh gflkjhj vkj hykiludfh fkjedhbnukj

+170 -38
+67 -27
src-tauri/src/main.rs
··· 3 3 mod pngmeta; 4 4 mod worldscraper; 5 5 6 - use tauri::{ CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowEvent, http::ResponseBuilder }; 6 + use tauri::{ http::ResponseBuilder, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowEvent }; 7 7 use core::time; 8 8 use std::{ fs, io::Read, path, thread }; 9 9 use regex::Regex; ··· 18 18 path: String, 19 19 } 20 20 21 + // Scans all files under the "Pictures/VRChat" path 22 + // then sends the list of photos to the frontend 23 + #[derive(Clone, serde::Serialize)] 24 + struct PhotosLoadedResponse{ 25 + photos: Vec<path::PathBuf>, 26 + size: usize 27 + } 28 + 29 + pub fn get_photo_path() -> path::PathBuf{ 30 + let config_path = dirs::home_dir().unwrap().join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 31 + 32 + match fs::read_to_string(config_path){ 33 + Ok(path) => { 34 + if path != dirs::picture_dir().unwrap().join("VRChat").to_str().unwrap().to_owned(){ 35 + let dir = path::PathBuf::from(path); 36 + match fs::metadata(&dir){ 37 + Ok(_) => {} 38 + Err(_) => { 39 + fs::create_dir(&dir).unwrap(); 40 + } 41 + }; 42 + 43 + dir 44 + } else{ 45 + let dir = dirs::picture_dir().unwrap().join("VRChat"); 46 + match fs::metadata(&dir){ 47 + Ok(_) => {} 48 + Err(_) => { 49 + fs::create_dir(&dir).unwrap(); 50 + } 51 + }; 52 + 53 + dir 54 + } 55 + } 56 + Err(_) => { 57 + let dir = dirs::picture_dir().unwrap().join("VRChat"); 58 + match fs::metadata(&dir){ 59 + Ok(_) => {} 60 + Err(_) => { 61 + fs::create_dir(&dir).unwrap(); 62 + } 63 + }; 64 + 65 + dir 66 + } 67 + } 68 + } 69 + 21 70 #[tauri::command] 22 71 async fn close_splashscreen(window: tauri::Window) { 23 72 window.get_window("main").unwrap().show().unwrap(); ··· 33 82 open::that(url).unwrap(); 34 83 } 35 84 85 + // Check if the photo config file exists 86 + // if not just return the default vrchat path 36 87 #[tauri::command] 37 88 fn get_user_photos_path() -> path::PathBuf { 38 - dirs::picture_dir().unwrap().join("VRChat") 89 + get_photo_path() 39 90 } 40 91 41 92 // When the user changes the start with windows toggle ··· 70 121 }); 71 122 } 72 123 73 - // Scans all files under the "Pictures/VRChat" path 74 - // then sends the list of photos to the frontend 75 - #[derive(Clone, serde::Serialize)] 76 - struct PhotosLoadedResponse{ 77 - photos: Vec<path::PathBuf>, 78 - size: usize 79 - } 80 - 81 124 #[tauri::command] 82 125 fn load_photos(window: tauri::Window) { 83 126 thread::spawn(move || { 84 - let base_dir = dirs::picture_dir().unwrap().join("VRChat"); 127 + let base_dir = get_photo_path(); 85 128 86 129 let mut photos: Vec<path::PathBuf> = Vec::new(); 87 130 let mut size: usize = 0; 88 131 89 - for folder in fs::read_dir(base_dir).unwrap() { 132 + for folder in fs::read_dir(&base_dir).unwrap() { 90 133 let f = folder.unwrap(); 91 134 92 135 if f.metadata().unwrap().is_dir() { ··· 110 153 if metadata.is_file() { 111 154 size += metadata.len() as usize; 112 155 113 - let path = path.strip_prefix(dirs::picture_dir().unwrap().join("VRChat")).unwrap().to_path_buf(); 156 + let path = path.strip_prefix(&base_dir).unwrap().to_path_buf(); 114 157 photos.push(path); 115 158 } 116 159 } ··· 130 173 let photo = photo.to_string(); 131 174 132 175 thread::spawn(move || { 133 - let mut base_dir = dirs::picture_dir().unwrap().join("VRChat"); 134 - base_dir.push(&photo); 135 - 176 + let base_dir = get_photo_path().join(&photo); 177 + 136 178 let mut file = fs::File::open(base_dir.clone()).expect("Cannot read image file."); 137 179 let mut buffer = Vec::new(); 138 - 180 + 139 181 let _out = file.read_to_end(&mut buffer); 140 182 window.emit("photo_meta_loaded", PNGImage::new(buffer, photo)).unwrap(); 141 183 }); ··· 144 186 // Delete a photo when the users confirms the prompt in the ui 145 187 #[tauri::command] 146 188 fn delete_photo( path: &str ){ 147 - let p = dirs::picture_dir().unwrap().join("VRChat").join(path); 189 + let p = get_photo_path().join(path); 148 190 fs::remove_file(p).unwrap(); 149 191 } 150 192 151 193 #[tauri::command] 152 194 fn change_final_path( new_path: &str ){ 153 - let config_path = dirs::picture_dir().unwrap().join(".vrchat_photos"); 195 + let config_path = dirs::home_dir().unwrap().join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 154 196 fs::write(&config_path, new_path.as_bytes()).unwrap(); 155 197 } 156 198 157 199 fn main() { 158 200 std::env::set_var("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--ignore-gpu-blacklist"); 159 201 tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 202 + 203 + println!("Loading App..."); 160 204 161 205 // Double check the app has an install directory 162 206 let container_folder = dirs::home_dir().unwrap().join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); ··· 200 244 re1.is_match(path.to_str().unwrap()) || 201 245 re2.is_match(path.to_str().unwrap()) 202 246 { 203 - sender.send((2, path.clone().strip_prefix(dirs::picture_dir().unwrap().join("VRChat")).unwrap().to_path_buf())).unwrap(); 247 + sender.send((2, path.clone().strip_prefix(get_photo_path()).unwrap().to_path_buf())).unwrap(); 204 248 } 205 249 }, 206 250 EventKind::Create(_) => { ··· 214 258 re2.is_match(path.to_str().unwrap()) 215 259 { 216 260 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(); 261 + sender.send((1, path.clone().strip_prefix(get_photo_path()).unwrap().to_path_buf())).unwrap(); 218 262 } 219 263 }, 220 264 _ => {} ··· 224 268 } 225 269 }).unwrap(); 226 270 227 - watcher.watch(&dirs::picture_dir().unwrap().join("VRChat"), RecursiveMode::Recursive).unwrap(); 271 + watcher.watch(&get_photo_path(), RecursiveMode::Recursive).unwrap(); 228 272 229 273 tauri::Builder::default() 230 274 .system_tray(tray) ··· 275 319 } 276 320 277 321 let path = uri.replace("photo://localhost/", ""); 278 - 279 - let mut base_dir = dirs::picture_dir().unwrap().join("VRChat"); 280 - base_dir.push(path); 281 - 282 - let file = fs::File::open(base_dir); 322 + let file = fs::File::open(path); 283 323 284 324 match file{ 285 325 Ok(mut file) => {
+10 -2
src/Components/App.tsx
··· 25 25 let [ photoCount, setPhotoCount ] = createSignal(0); 26 26 let [ photoSize, setPhotoSize ] = createSignal(0); 27 27 28 + let [ requestPhotoReload, setRequestPhotoReload ] = createSignal(false); 29 + 28 30 let setConfirmationBox = ( text: string, cb: () => void ) => { 29 31 setConfirmationBoxText(text); 30 32 confirmationBoxCallback = cb; 31 33 } 32 34 35 + console.log(localStorage.getItem('token')); 33 36 if(localStorage.getItem('token')){ 34 37 fetch<any>('https://photos.phazed.xyz/api/v1/account', { 35 38 method: 'GET', ··· 41 44 return console.error(data); 42 45 } 43 46 47 + console.log(data.data); 44 48 setLoggedIn({ loggedIn: true, username: data.data.user.username, avatar: data.data.user.avatar, id: data.data.user._id }); 45 49 }) 46 50 .catch(e => { ··· 115 119 return setLoadingType('none'); 116 120 } 117 121 122 + console.log(data.data); 118 123 localStorage.setItem('token', token); 119 124 120 125 setLoadingType('none'); ··· 150 155 setPhotoNavChoice={setPhotoNavChoice} 151 156 setConfirmationBox={setConfirmationBox} 152 157 setPhotoCount={setPhotoCount} 153 - setPhotoSize={setPhotoSize} /> 158 + setPhotoSize={setPhotoSize} 159 + requestPhotoReload={requestPhotoReload} 160 + setRequestPhotoReload={setRequestPhotoReload} /> 154 161 155 162 <PhotoViewer 156 163 setPhotoNavChoice={setPhotoNavChoice} ··· 160 167 161 168 <SettingsMenu 162 169 photoCount={photoCount} 163 - photoSize={photoSize} /> 170 + photoSize={photoSize} 171 + setRequestPhotoReload={setRequestPhotoReload} /> 164 172 165 173 <div class="copy-notif">Image Copied!</div> 166 174
+55 -6
src/Components/PhotoList.tsx
··· 17 17 photoNavChoice!: () => string; 18 18 setPhotoNavChoice!: ( view: any ) => any; 19 19 setConfirmationBox!: ( text: string, cb: () => void ) => void; 20 + requestPhotoReload!: () => boolean; 21 + setRequestPhotoReload!: ( val: boolean ) => boolean; 20 22 } 21 23 22 24 let PhotoList = ( props: PhotoListProps ) => { ··· 27 29 let photoMetaDataLoadingContainer: HTMLElement; 28 30 let photoMetaDataLoadingBar: HTMLElement; 29 31 32 + let scrollToTop: HTMLElement; 33 + let scrollToTopActive = false; 34 + 30 35 let photoContainer: HTMLCanvasElement; 31 36 let ctx: CanvasRenderingContext2D; 32 37 ··· 37 42 let targetScroll: number = 0; 38 43 39 44 let quitRender: boolean = false; 45 + let photoPath: string; 46 + 47 + createEffect(() => { 48 + if(props.requestPhotoReload()){ 49 + props.setRequestPhotoReload(false); 50 + reloadPhotos(); 51 + } 52 + }) 40 53 41 54 class PhotoMetadata{ 42 55 width!: number; ··· 84 97 85 98 this.imageEl = document.createElement('img'); 86 99 this.imageEl.crossOrigin = 'anonymous'; 87 - this.imageEl.src = 'https://photo.localhost/' + this.path; 100 + this.imageEl.src = 'https://photo.localhost/' + photoPath + this.path; 88 101 89 102 this.imageEl.onload = () => { 90 103 this.image!.width = this.scaledWidth!; ··· 127 140 else 128 141 return quitRender = false; 129 142 143 + if(!scrollToTopActive && scroll > photoContainer.height){ 144 + scrollToTop.style.display = 'flex'; 145 + anime({ targets: scrollToTop, opacity: 1, translateY: '0px', easing: 'easeInOutQuad', duration: 100 }); 146 + 147 + scrollToTopActive = true; 148 + } else if(scrollToTopActive && scroll < photoContainer.height){ 149 + anime({ targets: scrollToTop, opacity: 0, translateY: '-10px', complete: () => scrollToTop.style.display = 'none', easing: 'easeInOutQuad', duration: 100 }); 150 + scrollToTopActive = false; 151 + } 152 + 130 153 if(!ctx)return; 131 154 ctx.clearRect(0, 0, photoContainer.width, photoContainer.height); 132 155 ··· 142 165 143 166 if(currentRowIndex * 210 - scroll > photoContainer.height){ 144 167 p.shown = false; 145 - break; 168 + continue; 146 169 } 147 170 148 171 if(!lastPhoto || (lastPhoto.dateString !== p.dateString)){ ··· 228 251 229 252 lastPhoto = p; 230 253 } 254 + 255 + if(photos.length == 0){ 256 + ctx.textAlign = 'center'; 257 + ctx.textBaseline = 'middle'; 258 + ctx.globalAlpha = 1; 259 + ctx.fillStyle = '#fff'; 260 + ctx.font = '50px Rubik'; 261 + 262 + ctx.fillText("You have no bitches", photoContainer.width / 2, photoContainer.height / 2); 263 + } 231 264 } 232 265 233 266 listen('photo_meta_loaded', ( event: any ) => { 234 267 let data: PhotoMetadata = event.payload; 235 - let photo = photos.find(x => x.path === data.path); 236 268 269 + let photo = photos.find(x => x.path === data.path); 237 270 if(!photo)return; 238 271 239 272 photo.width = data.width; ··· 274 307 }) 275 308 276 309 listen('photo_create', ( event: any ) => { 277 - console.log(event); 278 - 279 310 let photo = new Photo(event.payload); 280 311 photos.splice(0, 0, photo); 281 312 }) ··· 289 320 } 290 321 }) 291 322 292 - let reloadPhotos = () => { 323 + let reloadPhotos = async () => { 324 + photoPath = await invoke('get_user_photos_path') + '/'; 325 + 293 326 photoTreeLoadingContainer.style.opacity = '1'; 294 327 photoTreeLoadingContainer.style.height = '100%'; 295 328 photoTreeLoadingContainer.style.display = 'flex'; ··· 316 349 } 317 350 318 351 let loadPhotos = async () => { 352 + photoPath = await invoke('get_user_photos_path') + '/'; 319 353 invoke('load_photos') 320 354 321 355 listen('photos_loaded', ( event: any ) => { ··· 329 363 photos.push(photo); 330 364 }) 331 365 366 + if(photoPaths.length == 0){ 367 + anime.set(photoMetaDataLoadingContainer, { height: 0, opacity: 0, display: 'none' }); 368 + render(); 369 + 370 + anime({ 371 + targets: '.reload-photos', 372 + opacity: 1, 373 + duration: 150, 374 + easing: 'easeInOutQuad' 375 + }) 376 + } 377 + 332 378 anime({ 333 379 targets: photoTreeLoadingContainer, 334 380 height: 0, ··· 345 391 onMount(() => { 346 392 ctx = photoContainer.getContext('2d')!; 347 393 loadPhotos(); 394 + 395 + anime.set(scrollToTop, { opacity: 0, translateY: '-10px', display: 'none' }); 348 396 349 397 photoContainer.addEventListener('wheel', ( e: WheelEvent ) => { 350 398 targetScroll += e.deltaY; ··· 388 436 </div> 389 437 </div> 390 438 439 + <div class="scroll-to-top" ref={( el ) => scrollToTop = el} onClick={() => targetScroll = 0}><i class="fa-solid fa-angle-up"></i></div> 391 440 <div class="reload-photos" onClick={() => props.setConfirmationBox("Are you sure you want to reload all photos? This can cause the application to slow down while it is loading...", reloadPhotos)}><i class="fa-solid fa-arrows-rotate"></i></div> 392 441 393 442 <canvas class="photo-container" ref={( el ) => photoContainer = el}></canvas>
+7 -1
src/Components/PhotoViewer.tsx
··· 44 44 let photoTrayCloseBtn: HTMLElement; 45 45 46 46 let worldInfoContainer: HTMLElement; 47 + let photoPath: string; 47 48 48 49 let openTray = () => { 49 50 if(trayOpen)return; ··· 109 110 imageViewer.style.opacity = '0'; 110 111 111 112 if(photo){ 112 - imageViewer.style.background = 'url(\'https://photo.localhost/' + props.currentPhotoView().path.replace('\\', '/') +'\')'; 113 + (async () => { 114 + if(!photoPath) 115 + photoPath = await invoke('get_user_photos_path') + '/'; 116 + 117 + imageViewer.style.background = 'url(\'https://photo.localhost/' + (photoPath + props.currentPhotoView().path).split('\\').join('/') +'\')'; 118 + })(); 113 119 114 120 anime({ 115 121 targets: imageViewer,
+16 -2
src/Components/SettingsMenu.tsx
··· 6 6 class SettingsMenuProps{ 7 7 photoCount!: () => number; 8 8 photoSize!: () => number; 9 + setRequestPhotoReload!: ( val: boolean ) => boolean; 9 10 } 10 11 11 12 let SettingsMenu = ( props: SettingsMenuProps ) => { ··· 196 197 </div> 197 198 198 199 <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 200 <p> 201 - Final Photo Path: 201 + VRChat Photo Path: 202 202 <span class="path" ref={( el ) => 203 203 invoke('get_user_photos_path').then(( path: any ) => { 204 204 el.innerHTML = ''; ··· 216 216 <span class="path" style={{ color: 'green' }} onClick={() => { 217 217 finalPathPreviousData = finalPathData; 218 218 finalPathConfirm.style.display = 'none'; 219 + invoke('change_final_path', { newPath: finalPathData }); 220 + 221 + anime({ 222 + targets: '.settings', 223 + opacity: 0, 224 + translateX: '500px', 225 + easing: 'easeInOutQuad', 226 + duration: 250, 227 + complete: () => { 228 + anime.set('.settings', { display: 'none' }); 229 + } 230 + }) 231 + 232 + props.setRequestPhotoReload(true); 219 233 }}><i class="fa-solid fa-check"></i></span> 220 234 221 235 <span class="path" style={{ color: 'red' }} onClick={() => {
+15
src/styles.css
··· 565 565 border-radius: 5px; 566 566 margin-left: 5px; 567 567 cursor: pointer; 568 + } 569 + 570 + .scroll-to-top{ 571 + position: fixed; 572 + bottom: 10px; 573 + right: 10px; 574 + color: white; 575 + width: 40px; 576 + height: 40px; 577 + cursor: pointer; 578 + border-radius: 50%; 579 + border: 2px solid white; 580 + display: flex; 581 + justify-content: center; 582 + align-items: center; 568 583 }