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;