A photo manager for VRChat.
1import { listen } from "@tauri-apps/api/event"; 2import { Accessor, createSignal } from "solid-js"; 3import { Photo } from "../Structs/Photo"; 4import { invoke } from "@tauri-apps/api/core"; 5import { PhotoMetadata } from "../Structs/PhotoMetadata"; 6import { Vars } from "../Structs/Vars"; 7import { FilterType } from "../FilterMenu"; 8import { MergeSort } from "../Utils/Sort"; 9 10export class PhotoManager{ 11 public PhotoCount: Accessor<number>; 12 public PhotoSize: Accessor<number>; 13 14 public Photos: Photo[] = []; 15 public FilteredPhotos: Photo[] = []; 16 17 public HasFirstLoaded = false; 18 19 private _amountLoaded = 0; 20 private _finishedLoadingCallbacks: (() => void)[] = []; 21 22 private _filterType: FilterType = FilterType.USER; 23 private _filter: string = ""; 24 25 private _lastLoaded: number = 0; 26 private _onLoadedMeta: any = {}; 27 private _hasBeenIndexed: Accessor<boolean>; 28 29 constructor(){ 30 let [ photoCount, setPhotoCount ] = createSignal(-1); 31 let [ photoSize, setPhotoSize ] = createSignal(-1); 32 33 this.PhotoCount = photoCount; 34 this.PhotoSize = photoSize; 35 36 let setHasBeenIndexed; 37 [ this._hasBeenIndexed, setHasBeenIndexed ] = createSignal(false); 38 39 listen('photos_loaded', ( event: any ) => { 40 let photoPaths = event.payload.photos.reverse(); 41 console.log(photoPaths); 42 43 setPhotoCount(photoPaths.length); 44 setPhotoSize(event.payload.size); 45 46 if(photoPaths.length <= Vars.MAX_PHOTOS_BULK_LOAD) 47 setHasBeenIndexed(true); 48 49 let photoLayers: Photo[] = []; 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 else 57 photo = new Photo(path, false, i); 58 59 if(!photo.legacy && photo.splitPath[4]){ 60 photoLayers.push(photo); 61 } else 62 this.Photos.push(photo); 63 64 if(photoPaths.length <= Vars.MAX_PHOTOS_BULK_LOAD) 65 photo.loadMeta(); 66 }) 67 68 photoLayers.forEach(photo => { 69 let type = photo.splitPath[4]; 70 photo.splitPath.pop(); 71 72 let mainPhotoPath = photo.splitPath.join('_') + '.png'; 73 let mainPhoto = this.Photos.find(x => x.path === mainPhotoPath); 74 75 if(!mainPhoto) 76 this.Photos.push(photo); 77 else{ 78 mainPhoto.isMultiLayer = true; 79 80 switch(type){ 81 case 'Player.png': 82 mainPhoto.playerLayer = photo; 83 break; 84 case 'Environment.png': 85 mainPhoto.environmentLayer = photo; 86 break; 87 } 88 } 89 }); 90 91 this.Photos = MergeSort(this.Photos); 92 console.log(this.Photos[0]); 93 94 console.log(this.Photos.length + ' Photos found.'); 95 96 if(this.Photos.length === 0 || photoPaths.length > Vars.MAX_PHOTOS_BULK_LOAD){ 97 console.log('No photos found or over bulk load limit, Skipping loading stage.'); 98 99 this.FilteredPhotos = this.Photos; 100 this.HasFirstLoaded = true; 101 102 this._finishedLoadingCallbacks.forEach(cb => cb()); 103 } 104 }); 105 106 listen('photo_meta_loaded', ( event: any ) => { 107 let data: PhotoMetadata = event.payload; 108 109 let photo = this.Photos.find(x => x.path === data.path); 110 if(!photo)return console.error('Cannot find photo.', data); 111 // NOTE: this is triggered by multilayer photo layers loading their metadata 112 // we don't need to store metadata of those photos as they inherit this 113 // data from the main photo. 114 115 this._lastLoaded = photo.index; 116 117 if(this._onLoadedMeta[photo.index]){ 118 this._onLoadedMeta[photo.index](); 119 delete this._onLoadedMeta[photo.index]; 120 } 121 122 photo.width = data.width; 123 photo.height = data.height; 124 125 let scale = Vars.PHOTO_HEIGHT / photo.height; 126 127 photo.scaledWidth = photo.width * scale; 128 photo.scaledHeight = Vars.PHOTO_HEIGHT; 129 130 photo.metadata = data.metadata.split('\u0000').filter(x => x !== '')[1]; 131 this._amountLoaded++; 132 133 photo.metaLoaded = true; 134 photo.onMetaLoaded(); 135 136 window.PhotoListRenderingManager.ComputeLayout(); 137 138 if(this._amountLoaded === this.Photos.length - 1 && !this.HasFirstLoaded){ 139 this.FilteredPhotos = this.Photos; 140 this.HasFirstLoaded = true; 141 142 this._finishedLoadingCallbacks.forEach(cb => cb()); 143 } 144 }) 145 146 listen('photo_create', async ( event: any ) => { 147 let photo = new Photo(event.payload, false, 0); 148 149 if(photo.splitPath[4]){ 150 let type = photo.splitPath[4]; 151 photo.splitPath.pop(); 152 153 let mainPhotoPath = photo.splitPath.join('_') + '.png'; 154 let mainPhoto = this.Photos.find(x => x.path === mainPhotoPath); 155 156 if(!mainPhoto){ 157 this.Photos.forEach(p => p.index++); // Probably a really dumb way of doing this 158 this.Photos.splice(0, 0, photo); 159 } else{ 160 mainPhoto.isMultiLayer = true; 161 162 switch(type){ 163 case 'Player.png': 164 mainPhoto.playerLayer = photo; 165 break; 166 case 'Environment.png': 167 mainPhoto.environmentLayer = photo; 168 break; 169 } 170 } 171 } else{ 172 this.Photos.forEach(p => p.index++); // Probably a really dumb way of doing this 173 this.Photos.splice(0, 0, photo); 174 } 175 176 photo.onMetaLoaded = () => this.ReloadFilters(); 177 photo.loadMeta(); 178 }) 179 180 listen('photo_remove', ( event: any ) => { 181 this.Photos = this.Photos.filter(x => x.path !== event.payload); 182 183 if(event.payload === window.PhotoViewerManager.CurrentPhoto()?.path) 184 window.PhotoViewerManager.Close() 185 186 this.ReloadFilters(); 187 }) 188 } 189 190 public SetFilterType( type: FilterType ){ 191 this._filterType = type; 192 this.ReloadFilters(); 193 } 194 195 public SetFilter( filter: string ){ 196 this._filter = filter; 197 this.ReloadFilters(); 198 } 199 200 public HasBeenIndexed(){ 201 return this._hasBeenIndexed(); 202 } 203 204 public LoadPhotoMetaAndWait( photo: Photo ){ 205 return new Promise(res => { 206 photo.loadMeta(); 207 this._onLoadedMeta[photo.index] = res; 208 }) 209 } 210 211 public async LoadSomeAndReloadFilters(){ 212 if(this.Photos.length < this._lastLoaded + 1)return; 213 214 for (let i = 1; i < 10; i++) { 215 if(!this.Photos[this._lastLoaded + 1])break; 216 await this.LoadPhotoMetaAndWait(this.Photos[this._lastLoaded + 1]); 217 } 218 219 this.ReloadFilters(); 220 } 221 222 public ReloadFilters(){ 223 this.FilteredPhotos = []; 224 225 if(this._filter === ''){ 226 this.FilteredPhotos = this.Photos; 227 window.PhotoListRenderingManager.ComputeLayout(); 228 229 return; 230 } 231 232 switch(this._filterType){ 233 case FilterType.USER: 234 this.Photos.map(p => { 235 if(p.metadata){ 236 try{ 237 let meta = JSON.parse(p.metadata); 238 let photo = meta.players.find(( y: any ) => 239 y.displayName.toLowerCase().includes(this._filter) || 240 y.id === this._filter 241 ); 242 243 if(photo)this.FilteredPhotos.push(p); 244 } catch(e){} 245 } 246 }) 247 break; 248 case FilterType.WORLD: 249 this.Photos.map(p => { 250 if(p.metadata){ 251 try{ 252 let meta = JSON.parse(p.metadata); 253 let photo = 254 meta.world.name.toLowerCase().includes(this._filter) || 255 meta.world.id === this._filter; 256 257 if(photo)this.FilteredPhotos.push(p); 258 } catch(e){} 259 } 260 }) 261 break; 262 } 263 264 window.PhotoListRenderingManager.ComputeLayout(); 265 } 266 267 public Load(){ 268 this.Photos = []; 269 this.FilteredPhotos = []; 270 271 this._amountLoaded = 0; 272 273 invoke('load_photos'); 274 } 275 276 public OnLoadingFinished( cb: () => void ){ 277 this._finishedLoadingCallbacks.push(cb); 278 } 279}