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}