A photo manager for VRChat.

don't load all photos on startup

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