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