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 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}