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