A photo manager for VRChat.
1import { onCleanup, onMount } from "solid-js";
2import { listen } from '@tauri-apps/api/event';
3import { Window } from "@tauri-apps/api/window";
4
5import anime from "animejs";
6import FilterMenu from "./FilterMenu";
7import { ViewState } from "./Managers/ViewManager";
8import { invoke } from "@tauri-apps/api/core";
9
10enum ListPopup{
11 FILTERS,
12 NONE
13}
14
15let PhotoList = () => {
16 let photoTreeLoadingContainer: HTMLElement;
17
18 let scrollToTop: HTMLElement;
19 let scrollToTopActive = false;
20
21 let photoContainer: HTMLCanvasElement;
22 let photoContainerBG: HTMLCanvasElement;
23
24 let filterContainer: HTMLDivElement;
25
26 let ctx: CanvasRenderingContext2D;
27 let ctxBG: CanvasRenderingContext2D;
28
29 let scroll: number = 0;
30 let targetScroll: number = 0;
31
32 let quitRender: boolean = true;
33
34 let currentPopup = ListPopup.NONE;
35
36 Window.getCurrent().isVisible().then(visible => {
37 quitRender = !visible;
38 })
39
40
41 window.ViewManager.OnStateTransition(ViewState.PHOTO_LIST, ViewState.SETTINGS, () => {
42 anime({ targets: photoContainer, opacity: 0, easing: 'easeInOutQuad', duration: 100 });
43 });
44
45 window.ViewManager.OnStateTransition(ViewState.SETTINGS, ViewState.PHOTO_LIST, () => {
46 anime({ targets: photoContainer, opacity: 1, easing: 'easeInOutQuad', duration: 100 });
47 });
48
49
50 window.ViewManager.OnStateTransition(ViewState.PHOTO_LIST, ViewState.PHOTO_VIEWER, () => {
51 anime({ targets: photoContainer, opacity: 0, easing: 'easeInOutQuad', duration: 100 });
52 anime({ targets: '.filter-options', opacity: 0, easing: 'easeInOutQuad', duration: 100 });
53 anime({ targets: '.reload-photos', opacity: 0, easing: 'easeInOutQuad', duration: 100 });
54 });
55
56 window.ViewManager.OnStateTransition(ViewState.PHOTO_VIEWER, ViewState.PHOTO_LIST, () => {
57 anime({ targets: photoContainer, opacity: 1, easing: 'easeInOutQuad', duration: 100 });
58 anime({ targets: '.filter-options', opacity: 1, easing: 'easeInOutQuad', duration: 100 });
59 anime({ targets: '.reload-photos', opacity: 1, easing: 'easeInOutQuad', duration: 100 });
60 });
61
62
63 let closeWithKey = ( e: KeyboardEvent ) => {
64 if(e.key === 'Escape'){
65 closeCurrentPopup();
66 }
67 }
68
69 let onResize = () => {
70 photoContainer.width = window.innerWidth;
71 photoContainer.height = window.innerHeight;
72
73 photoContainerBG.width = window.innerWidth;
74 photoContainerBG.height = window.innerHeight;
75
76 window.PhotoListRenderingManager.ComputeLayout();
77 }
78
79 let closeCurrentPopup = () => {
80 switch(currentPopup){
81 case ListPopup.FILTERS:
82 anime({
83 targets: filterContainer!,
84 opacity: 0,
85 easing: 'easeInOutQuad',
86 duration: 100,
87 complete: () => {
88 filterContainer!.style.display = 'none';
89 currentPopup = ListPopup.NONE;
90 }
91 });
92
93 break;
94 }
95 }
96
97 let render = () => {
98 if(!quitRender)
99 requestAnimationFrame(render);
100 else
101 return quitRender = false;
102
103 if(!scrollToTopActive && scroll > photoContainer.height){
104 scrollToTop.style.display = 'flex';
105 anime({ targets: scrollToTop, opacity: 1, translateY: '0px', easing: 'easeInOutQuad', duration: 100 });
106
107 scrollToTopActive = true;
108 } else if(scrollToTopActive && scroll < photoContainer.height){
109 anime({ targets: scrollToTop, opacity: 0, translateY: '-10px', complete: () => scrollToTop.style.display = 'none', easing: 'easeInOutQuad', duration: 100 });
110 scrollToTopActive = false;
111 }
112
113 if(!ctx || !ctxBG)return;
114 ctx.clearRect(0, 0, photoContainer.width, photoContainer.height);
115 ctxBG.clearRect(0, 0, photoContainerBG.width, photoContainerBG.height);
116
117 scroll = scroll + (targetScroll - scroll) * 0.2;
118
119 window.PhotoListRenderingManager.Render(ctx, photoContainer!, scroll);
120
121 if(window.PhotoManager.FilteredPhotos.length == 0){
122 ctx.textAlign = 'center';
123 ctx.textBaseline = 'middle';
124 ctx.globalAlpha = 1;
125 ctx.fillStyle = '#fff';
126 ctx.font = '40px Rubik';
127
128 ctx.fillText("It's looking empty in here! You have no photos :O", photoContainer.width / 2, photoContainer.height / 2);
129 }
130
131 ctxBG.drawImage(photoContainer, 0, 0);
132 }
133
134 listen('hide-window', () => {
135 quitRender = true;
136 console.log('Hide Window');
137 })
138
139 listen('show-window', () => {
140 if(quitRender)quitRender = false;
141 console.log('Shown Window');
142
143 photoContainer.width = window.innerWidth;
144 photoContainer.height = window.innerHeight;
145
146 photoContainerBG.width = window.innerWidth;
147 photoContainerBG.height = window.innerHeight;
148
149 if(window.PhotoManager.HasFirstLoaded){
150 requestAnimationFrame(render);
151 window.PhotoManager.HasFirstLoaded = false;
152 }
153 })
154
155 window.PhotoManager.OnLoadingFinished(() => {
156 invoke('close_splashscreen');
157
158 anime({
159 targets: photoTreeLoadingContainer,
160 height: 0,
161 easing: 'easeInOutQuad',
162 duration: 500,
163 opacity: 0,
164 complete: () => {
165 photoTreeLoadingContainer.style.display = 'none';
166 }
167 })
168
169 anime({
170 targets: '.reload-photos',
171 opacity: 1,
172 duration: 150,
173 easing: 'easeInOutQuad'
174 })
175
176 window.PhotoListRenderingManager.SetCanvas(photoContainer!);
177 window.PhotoListRenderingManager.ComputeLayout();
178
179 render();
180 });
181
182 onMount(() => {
183 ctx = photoContainer.getContext('2d')!;
184 ctxBG = photoContainerBG.getContext('2d')!;
185
186 window.PhotoManager.Load();
187
188 anime.set(scrollToTop, { opacity: 0, translateY: '-10px', display: 'none' });
189
190 photoContainer.onwheel = ( e: WheelEvent ) => {
191 targetScroll += e.deltaY;
192
193 if(targetScroll < 0)
194 targetScroll = 0;
195 };
196
197 window.addEventListener('keyup', closeWithKey);
198 window.addEventListener('resize', onResize);
199
200 photoContainer.width = window.innerWidth;
201 photoContainer.height = window.innerHeight;
202
203 photoContainerBG.width = window.innerWidth;
204 photoContainerBG.height = window.innerHeight;
205
206 photoContainer.onclick = ( e: MouseEvent ) => {
207 let photo = window.PhotoManager.FilteredPhotos.find(x =>
208 e.clientX > x.x &&
209 e.clientY > x.y &&
210 e.clientX < x.x + x.scaledWidth! &&
211 e.clientY < x.y + x.scaledHeight! &&
212 x.shown
213 );
214
215 if(photo)
216 window.PhotoViewerManager.OpenPhoto(photo);
217 // else
218 // currentPhotoIndex = -1;
219 }
220 })
221
222 onCleanup(() => {
223 photoContainer.onwheel = () => {};
224 photoContainer.onclick = () => {};
225
226 window.removeEventListener('keyup', closeWithKey);
227 window.removeEventListener('resize', onResize);
228 })
229
230 return (
231 <div class="photo-list">
232 <div ref={filterContainer!} class="filter-container" style={{
233 height: window.PhotoManager.HasBeenIndexed() ? '83px' : '110px',
234 width: window.PhotoManager.HasBeenIndexed() ? '600px' : '650px'
235 }}>
236 <FilterMenu />
237 </div>
238
239 <div class="photo-tree-loading" ref={( el ) => photoTreeLoadingContainer = el}>Scanning Photo Tree...</div>
240
241 <div class="scroll-to-top" ref={( el ) => scrollToTop = el} onClick={() => targetScroll = 0}>
242 <div class="icon">
243 <img draggable="false" src="/icon/angle-up-solid.svg"></img>
244 </div>
245 </div>
246 <div class="reload-photos" onClick={() => window.ConfirmationBoxManager.SetConfirmationBox("Are you sure you want to reload all photos? This can cause the application to slow down while it is loading...", () => window.location.reload())}>
247 <div class="icon" style={{ width: '17px' }}>
248 <img draggable="false" width="17" height="17" src="/icon/arrows-rotate-solid.svg"></img>
249 </div>
250 </div>
251
252 <div class="filter-options">
253 <div>
254 <div onClick={() => {
255 if(currentPopup != ListPopup.NONE)return closeCurrentPopup();
256 currentPopup = ListPopup.FILTERS;
257
258 filterContainer!.style.display = 'block';
259
260 anime({
261 targets: filterContainer!,
262 opacity: 1,
263 easing: 'easeInOutQuad',
264 duration: 100
265 });
266 }} class="icon" style={{ width: '20px', height: '20px', padding: '20px' }}>
267 <img draggable="false" style={{ width: "20px", height: "20px" }} src="/icon/sliders-solid.svg"></img>
268 </div>
269 <div class="icon-label">Filters</div>
270 </div>
271 </div>
272
273 <canvas class="photo-container" ref={( el ) => photoContainer = el}></canvas>
274 <canvas class="photo-container-bg" ref={( el ) => photoContainerBG = el}></canvas>
275 </div>
276 )
277}
278
279export default PhotoList;