A photo manager for VRChat.
at main 7.9 kB view raw
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 photo.error = data.error; 116 this._lastLoaded = photo.index; 117 118 if(this._onLoadedMeta[photo.index]){ 119 this._onLoadedMeta[photo.index](); 120 delete this._onLoadedMeta[photo.index]; 121 } 122 123 photo.width = data.width; 124 photo.height = data.height; 125 126 let scale = Vars.PHOTO_HEIGHT / photo.height; 127 128 photo.scaledWidth = photo.width * scale; 129 photo.scaledHeight = Vars.PHOTO_HEIGHT; 130 131 photo.metadata = data.metadata.split('\u0000').filter(x => x !== '')[1]; 132 this._amountLoaded++; 133 134 photo.loadingMeta = false; 135 photo.metaLoaded = true; 136 photo.onMetaLoaded(); 137 138 window.PhotoListRenderingManager.ComputeLayout(); 139 140 if(this._amountLoaded === this.Photos.length - 1 && !this.HasFirstLoaded){ 141 this.FilteredPhotos = this.Photos; 142 this.HasFirstLoaded = true; 143 144 this._finishedLoadingCallbacks.forEach(cb => cb()); 145 } 146 }) 147 148 listen('photo_create', async ( event: any ) => { 149 let photo = new Photo(event.payload, false, 0); 150 151 if(photo.splitPath[4]){ 152 let type = photo.splitPath[4]; 153 photo.splitPath.pop(); 154 155 let mainPhotoPath = photo.splitPath.join('_') + '.png'; 156 let mainPhoto = this.Photos.find(x => x.path === mainPhotoPath); 157 158 if(!mainPhoto){ 159 this.Photos.forEach(p => p.index++); // Probably a really dumb way of doing this 160 this.Photos.splice(0, 0, photo); 161 } else{ 162 mainPhoto.isMultiLayer = true; 163 164 switch(type){ 165 case 'Player.png': 166 mainPhoto.playerLayer = photo; 167 break; 168 case 'Environment.png': 169 mainPhoto.environmentLayer = photo; 170 break; 171 } 172 } 173 } else{ 174 this.Photos.forEach(p => p.index++); // Probably a really dumb way of doing this 175 this.Photos.splice(0, 0, photo); 176 } 177 178 photo.onMetaLoaded = () => this.ReloadFilters(); 179 photo.loadMeta(); 180 }) 181 182 listen('photo_remove', ( event: any ) => { 183 this.Photos = this.Photos.filter(x => x.path !== event.payload); 184 185 if(event.payload === window.PhotoViewerManager.CurrentPhoto()?.path) 186 window.PhotoViewerManager.Close() 187 188 this.ReloadFilters(); 189 }) 190 } 191 192 public SetFilterType( type: FilterType ){ 193 this._filterType = type; 194 this.ReloadFilters(); 195 } 196 197 public SetFilter( filter: string ){ 198 this._filter = filter; 199 this.ReloadFilters(); 200 } 201 202 public HasBeenIndexed(){ 203 return this._hasBeenIndexed(); 204 } 205 206 public LoadPhotoMetaAndWait( photo: Photo ){ 207 return new Promise(res => { 208 photo.loadMeta(); 209 this._onLoadedMeta[photo.index] = res; 210 }) 211 } 212 213 public async LoadSomeAndReloadFilters(){ 214 if(this.Photos.length < this._lastLoaded + 1)return; 215 216 for (let i = 1; i < 10; i++) { 217 if(!this.Photos[this._lastLoaded + 1])break; 218 await this.LoadPhotoMetaAndWait(this.Photos[this._lastLoaded + 1]); 219 } 220 221 this.ReloadFilters(); 222 } 223 224 public ReloadFilters(){ 225 this.FilteredPhotos = []; 226 227 if(this._filter === ''){ 228 this.FilteredPhotos = this.Photos; 229 window.PhotoListRenderingManager.ComputeLayout(); 230 231 return; 232 } 233 234 switch(this._filterType){ 235 case FilterType.USER: 236 this.Photos.map(p => { 237 if(p.metadata){ 238 try{ 239 let meta = JSON.parse(p.metadata); 240 let photo = meta.players.find(( y: any ) => 241 y.displayName.toLowerCase().includes(this._filter) || 242 y.id === this._filter 243 ); 244 245 if(photo)this.FilteredPhotos.push(p); 246 } catch(e){} 247 } 248 }) 249 break; 250 case FilterType.WORLD: 251 this.Photos.map(p => { 252 if(p.metadata){ 253 try{ 254 let meta = JSON.parse(p.metadata); 255 let photo = 256 meta.world.name.toLowerCase().includes(this._filter) || 257 meta.world.id === this._filter; 258 259 if(photo)this.FilteredPhotos.push(p); 260 } catch(e){} 261 } 262 }) 263 break; 264 } 265 266 window.PhotoListRenderingManager.ComputeLayout(); 267 } 268 269 public Load(){ 270 this.Photos = []; 271 this.FilteredPhotos = []; 272 273 this._amountLoaded = 0; 274 275 invoke('load_photos'); 276 } 277 278 public OnLoadingFinished( cb: () => void ){ 279 this._finishedLoadingCallbacks.push(cb); 280 } 281}