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;