A photo manager for VRChat.

don't load all photos on startup

+8 -1
changelog
··· 69 - Fixed filters not updating photo list 70 - Fixed adding / removing photos not updating the photo list 71 72 Dev Stuff: 73 - - Split frontend up into many smaller files for easier editing
··· 69 - Fixed filters not updating photo list 70 - Fixed adding / removing photos not updating the photo list 71 72 + Hotfix 1: 73 + - Fixed new installations immediately crashing 74 + 75 Dev Stuff: 76 + - Split frontend up into many smaller files for easier editing 77 + 78 + v0.2.4: 79 + - Refactor loading system to not load all photos at the start 80 + (should help with large numbers of photos)
+1 -1
src-tauri/Cargo.lock
··· 5260 5261 [[package]] 5262 name = "vrcpm-rs" 5263 - version = "0.2.3" 5264 dependencies = [ 5265 "dirs", 5266 "fast_image_resize",
··· 5260 5261 [[package]] 5262 name = "vrcpm-rs" 5263 + version = "0.2.3-hot1" 5264 dependencies = [ 5265 "dirs", 5266 "fast_image_resize",
+1 -1
src-tauri/Cargo.toml
··· 1 [package] 2 name = "vrcpm-rs" 3 - version = "0.2.3" 4 description = "VRChat Photo Manager" 5 authors = ["_phaz"] 6 edition = "2021"
··· 1 [package] 2 name = "vrcpm-rs" 3 + version = "0.2.3-hot1" 4 description = "VRChat Photo Manager" 5 authors = ["_phaz"] 6 edition = "2021"
+1 -1
src-tauri/src/frontend_calls/close_splashscreen.rs
··· 14 } 15 } 16 17 - let value = get_config_value_string("start-in-bg".to_owned()).unwrap(); 18 if value == "true"{ 19 show = false; 20 }
··· 14 } 15 } 16 17 + let value: String = match get_config_value_string("start-in-bg".to_owned()) { Some(val) => val, None => "false".to_owned() }; 18 if value == "true"{ 19 show = false; 20 }
-2
src-tauri/src/util/get_photo_path.rs
··· 5 .unwrap() 6 .join("PhazeDev/VRChatPhotoManager/.photos_path"); 7 8 - dbg!(&config_path); 9 - 10 match fs::read_to_string(config_path) { 11 Ok(path) => { 12 path::PathBuf::from(path)
··· 5 .unwrap() 6 .join("PhazeDev/VRChatPhotoManager/.photos_path"); 7 8 match fs::read_to_string(config_path) { 9 Ok(path) => { 10 path::PathBuf::from(path)
+5
src/Components/FilterMenu.tsx
··· 1 import { FilterType } from "./Structs/FilterType"; 2 3 let FilterMenu = () => { ··· 10 11 return ( 12 <> 13 <div class="filter-type-select"> 14 <div class="selected-filter" ref={( el ) => selectionButtons.push(el)} onClick={() => { 15 select(0);
··· 1 + import { Show } from "solid-js"; 2 import { FilterType } from "./Structs/FilterType"; 3 4 let FilterMenu = () => { ··· 11 12 return ( 13 <> 14 + <Show when={!window.PhotoManager.HasBeenIndexed()}> 15 + <div>Your photos aren't indexed due to the large number, filters may take a while to load.</div> 16 + <div style={{ height: '8px' }}></div> 17 + </Show> 18 <div class="filter-type-select"> 19 <div class="selected-filter" ref={( el ) => selectionButtons.push(el)} onClick={() => { 20 select(0);
+12
src/Components/Managers/PhotoListRenderingManager.tsx
··· 9 private _layout: PhotoListRow[] = []; 10 private _canvas!: HTMLCanvasElement; 11 12 constructor(){} 13 14 public SetCanvas( canvas: HTMLCanvasElement ){ ··· 145 } 146 147 currentY += row.Height + 10; 148 } 149 } 150 }
··· 9 private _layout: PhotoListRow[] = []; 10 private _canvas!: HTMLCanvasElement; 11 12 + private _isLoading = false; 13 + 14 constructor(){} 15 16 public SetCanvas( canvas: HTMLCanvasElement ){ ··· 147 } 148 149 currentY += row.Height + 10; 150 + } 151 + 152 + if(!this._isLoading){ 153 + console.log('Loading more photos...'); 154 + this._isLoading = true; 155 + 156 + window.PhotoManager.LoadSomeAndReloadFilters() 157 + .then(() => { 158 + this._isLoading = false; 159 + }); 160 } 161 } 162 }
+52 -7
src/Components/Managers/PhotoManager.tsx
··· 20 21 private _filterType: FilterType = FilterType.USER; 22 private _filter: string = ""; 23 24 constructor(){ 25 let [ photoCount, setPhotoCount ] = createSignal(-1); ··· 27 28 this.PhotoCount = photoCount; 29 this.PhotoSize = photoSize; 30 31 listen('photos_loaded', ( event: any ) => { 32 let photoPaths = event.payload.photos.reverse(); ··· 36 setPhotoSize(event.payload.size); 37 38 let doesHaveLegacy = false; 39 40 - photoPaths.forEach(( path: string ) => { 41 let photo 42 43 if(path.slice(0, 9) === "legacy://"){ 44 - photo = new Photo(path.slice(9), true); 45 doesHaveLegacy = true; 46 } else 47 - photo = new Photo(path, false); 48 49 this.Photos.push(photo); 50 - photo.loadMeta(); 51 }) 52 53 if(doesHaveLegacy){ ··· 55 } 56 57 console.log(this.Photos.length + ' Photos found.'); 58 - if(this.Photos.length === 0){ 59 console.log('No photos found, Skipping loading stage.'); 60 61 this.FilteredPhotos = this.Photos; ··· 63 64 this._finishedLoadingCallbacks.forEach(cb => cb()); 65 } 66 }); 67 68 listen('photo_meta_loaded', ( event: any ) => { ··· 70 71 let photo = this.Photos.find(x => x.path === data.path); 72 if(!photo)return console.error('Cannot find photo.', data); 73 74 photo.width = data.width; 75 photo.height = data.height; ··· 94 }) 95 96 listen('photo_create', async ( event: any ) => { 97 - let photo = new Photo(event.payload); 98 - 99 this.Photos.splice(0, 0, photo); 100 101 photo.onMetaLoaded = () => this.ReloadFilters(); ··· 124 125 public SetFilter( filter: string ){ 126 this._filter = filter; 127 this.ReloadFilters(); 128 } 129
··· 20 21 private _filterType: FilterType = FilterType.USER; 22 private _filter: string = ""; 23 + 24 + private _lastLoaded: number = 0; 25 + private _onLoadedMeta: any = {}; 26 + private _hasBeenIndexed: Accessor<boolean>; 27 28 constructor(){ 29 let [ photoCount, setPhotoCount ] = createSignal(-1); ··· 31 32 this.PhotoCount = photoCount; 33 this.PhotoSize = photoSize; 34 + 35 + let setHasBeenIndexed; 36 + [ this._hasBeenIndexed, setHasBeenIndexed ] = createSignal(false); 37 + console.log(this._hasBeenIndexed()) 38 39 listen('photos_loaded', ( event: any ) => { 40 let photoPaths = event.payload.photos.reverse(); ··· 44 setPhotoSize(event.payload.size); 45 46 let doesHaveLegacy = false; 47 + 48 + if(photoPaths.length <= Vars.MAX_PHOTOS_BULK_LOAD) 49 + setHasBeenIndexed(true); 50 51 + photoPaths.forEach(( path: string, i: number ) => { 52 let photo 53 54 if(path.slice(0, 9) === "legacy://"){ 55 + photo = new Photo(path.slice(9), true, i); 56 doesHaveLegacy = true; 57 } else 58 + photo = new Photo(path, false, i); 59 60 this.Photos.push(photo); 61 + 62 + if(photoPaths.length <= Vars.MAX_PHOTOS_BULK_LOAD) 63 + photo.loadMeta(); 64 }) 65 66 if(doesHaveLegacy){ ··· 68 } 69 70 console.log(this.Photos.length + ' Photos found.'); 71 + if(this.Photos.length === 0 || photoPaths.length > Vars.MAX_PHOTOS_BULK_LOAD){ 72 console.log('No photos found, Skipping loading stage.'); 73 74 this.FilteredPhotos = this.Photos; ··· 76 77 this._finishedLoadingCallbacks.forEach(cb => cb()); 78 } 79 + 80 + console.log(this._hasBeenIndexed()) 81 }); 82 83 listen('photo_meta_loaded', ( event: any ) => { ··· 85 86 let photo = this.Photos.find(x => x.path === data.path); 87 if(!photo)return console.error('Cannot find photo.', data); 88 + 89 + this._lastLoaded = photo.index; 90 + 91 + if(this._onLoadedMeta[photo.index]){ 92 + this._onLoadedMeta[photo.index](); 93 + delete this._onLoadedMeta[photo.index]; 94 + } 95 96 photo.width = data.width; 97 photo.height = data.height; ··· 116 }) 117 118 listen('photo_create', async ( event: any ) => { 119 + let photo = new Photo(event.payload, false, 0); 120 + 121 + this.Photos.forEach(p => p.index++); // Probably a really dumb way of doing this 122 this.Photos.splice(0, 0, photo); 123 124 photo.onMetaLoaded = () => this.ReloadFilters(); ··· 147 148 public SetFilter( filter: string ){ 149 this._filter = filter; 150 + this.ReloadFilters(); 151 + } 152 + 153 + public HasBeenIndexed(){ 154 + return this._hasBeenIndexed(); 155 + } 156 + 157 + public LoadPhotoMetaAndWait( photo: Photo ){ 158 + return new Promise(res => { 159 + photo.loadMeta(); 160 + this._onLoadedMeta[photo.index] = res; 161 + }) 162 + } 163 + 164 + public async LoadSomeAndReloadFilters(){ 165 + if(this.Photos.length < this._lastLoaded + 1)return; 166 + 167 + for (let i = 1; i < 10; i++) { 168 + if(!this.Photos[this._lastLoaded + 1])break; 169 + await this.LoadPhotoMetaAndWait(this.Photos[this._lastLoaded + 1]); 170 + } 171 + 172 this.ReloadFilters(); 173 } 174
+4 -1
src/Components/PhotoList.tsx
··· 190 191 return ( 192 <div class="photo-list"> 193 - <div ref={filterContainer!} class="filter-container"> 194 <FilterMenu /> 195 </div> 196
··· 190 191 return ( 192 <div class="photo-list"> 193 + <div ref={filterContainer!} class="filter-container" style={{ 194 + height: window.PhotoManager.HasBeenIndexed() ? '83px' : '110px', 195 + width: window.PhotoManager.HasBeenIndexed() ? '600px' : '650px' 196 + }}> 197 <FilterMenu /> 198 </div> 199
+23 -4
src/Components/Structs/Photo.ts
··· 27 date: Date; 28 29 legacy: boolean = false; 30 31 public onMetaLoaded: () => void = () => {}; 32 33 - constructor( path: string, isLegacy: boolean = false ){ 34 this.path = path; 35 this.legacy = isLegacy; 36 37 if(this.legacy) 38 - this.dateString = this.path.split('_')[2]; 39 else 40 - this.dateString = this.path.split('_')[1]; 41 42 let splitDateString = this.dateString.split('-'); 43 ··· 46 this.date.setFullYear(parseInt(splitDateString[0])); 47 this.date.setMonth(parseInt(splitDateString[1])); 48 this.date.setDate(parseInt(splitDateString[2])); 49 } 50 51 loadMeta(){ ··· 56 if(this.loading || this.loaded || imagesLoading >= Vars.MAX_IMAGE_LOAD)return; 57 58 // this.loadMeta(); 59 - if(!this.metaLoaded)return; 60 61 this.loading = true; 62
··· 27 date: Date; 28 29 legacy: boolean = false; 30 + index: number = 0; 31 32 public onMetaLoaded: () => void = () => {}; 33 34 + constructor( path: string, isLegacy: boolean = false, i: number ){ 35 this.path = path; 36 this.legacy = isLegacy; 37 + this.index = i; 38 + 39 + let split = this.path.split('_'); 40 41 if(this.legacy) 42 + this.dateString = split[2]; 43 else 44 + this.dateString = split[1]; 45 46 let splitDateString = this.dateString.split('-'); 47 ··· 50 this.date.setFullYear(parseInt(splitDateString[0])); 51 this.date.setMonth(parseInt(splitDateString[1])); 52 this.date.setDate(parseInt(splitDateString[2])); 53 + 54 + let resSplit = split[3].split('x'); 55 + 56 + let width = parseInt(resSplit[0]); 57 + let height = parseInt(resSplit[1]); 58 + 59 + if(!isNaN(width) || !isNaN(height)){ 60 + this.width = width; 61 + this.height = height; 62 + 63 + let scale = Vars.PHOTO_HEIGHT / this.height; 64 + 65 + this.scaledWidth = this.width * scale; 66 + this.scaledHeight = Vars.PHOTO_HEIGHT; 67 + } 68 } 69 70 loadMeta(){ ··· 75 if(this.loading || this.loaded || imagesLoading >= Vars.MAX_IMAGE_LOAD)return; 76 77 // this.loadMeta(); 78 + if(!this.metaLoaded)return this.loadMeta(); 79 80 this.loading = true; 81
+2
src/Components/Structs/Vars.ts
··· 1 export class Vars{ 2 static MAX_IMAGE_LOAD = 10; 3 static PHOTO_HEIGHT = 200; 4 }
··· 1 export class Vars{ 2 static MAX_IMAGE_LOAD = 10; 3 static PHOTO_HEIGHT = 200; 4 + 5 + static MAX_PHOTOS_BULK_LOAD = 100; 6 }