A photo manager for VRChat.
at main 9.6 kB view raw
1import { createSignal, onCleanup, onMount, Show } 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 let [ updateAvailable, setUpdateAvailable ] = createSignal(false); 33 34 Window.getCurrent().isVisible().then(visible => { 35 quitRender = !visible; 36 }) 37 38 39 window.ViewManager.OnStateTransition(ViewState.PHOTO_LIST, ViewState.SETTINGS, () => { 40 animate(photoContainer, { opacity: 0.5, filter: 'blur(10px)', easing: 'easeInOutQuad', duration: 100 }); 41 animate('.filter-options', { opacity: 0, easing: 'easeInOutQuad', duration: 100 }); 42 animate('.scroll-to-top', { opacity: 0, easing: 'easeInOutQuad', duration: 100 }); 43 }); 44 45 window.ViewManager.OnStateTransition(ViewState.SETTINGS, ViewState.PHOTO_LIST, () => { 46 animate(photoContainer, { opacity: 1, filter: 'blur(0px)', easing: 'easeInOutQuad', duration: 100, onComplete: () => photoContainer.style.filter = '' }); 47 animate('.filter-options', { opacity: 1, easing: 'easeInOutQuad', duration: 100 }); 48 animate('.scroll-to-top', { opacity: 1, easing: 'easeInOutQuad', duration: 100 }); 49 }); 50 51 52 window.ViewManager.OnStateTransition(ViewState.PHOTO_LIST, ViewState.PHOTO_VIEWER, () => { 53 animate(photoContainer, { opacity: 0.5, filter: 'blur(10px)', easing: 'easeInOutQuad', duration: 100 }); 54 animate('.filter-options', { opacity: 0, easing: 'easeInOutQuad', duration: 100 }); 55 animate('.scroll-to-top', { opacity: 0, easing: 'easeInOutQuad', duration: 100 }); 56 }); 57 58 window.ViewManager.OnStateTransition(ViewState.PHOTO_VIEWER, ViewState.PHOTO_LIST, () => { 59 animate(photoContainer, { opacity: 1, filter: 'blur(0px)', easing: 'easeInOutQuad', duration: 100, onComplete: () => photoContainer.style.filter = '' }); 60 animate('.filter-options', { opacity: 1, easing: 'easeInOutQuad', duration: 100 }); 61 animate('.scroll-to-top', { opacity: 1, easing: 'easeInOutQuad', duration: 100 }); 62 }); 63 64 65 let closeWithKey = ( e: KeyboardEvent ) => { 66 if(e.key === 'Escape'){ 67 closeCurrentPopup(); 68 } 69 } 70 71 let onResize = () => { 72 photoContainer.width = window.innerWidth; 73 photoContainer.height = window.innerHeight; 74 75 window.PhotoListRenderingManager.ComputeLayout(); 76 } 77 78 let closeCurrentPopup = () => { 79 switch(currentPopup){ 80 case ListPopup.FILTERS: 81 animate(filterContainer!, { 82 opacity: 0, 83 translateY: '10px', 84 easing: 'easeInOutQuad', 85 duration: 100, 86 onComplete: () => { 87 filterContainer!.style.display = 'none'; 88 currentPopup = ListPopup.NONE; 89 } 90 }); 91 92 break; 93 } 94 } 95 96 let render = () => { 97 if(!quitRender) 98 requestAnimationFrame(render); 99 else 100 return quitRender = false; 101 102 if(!scrollToTopActive && scroll > photoContainer.height){ 103 scrollToTop.style.display = 'flex'; 104 animate(scrollToTop, { opacity: 1, translateY: '0px', easing: 'easeInOutQuad', duration: 100 }); 105 106 scrollToTopActive = true; 107 } else if(scrollToTopActive && scroll < photoContainer.height){ 108 animate(scrollToTop, { opacity: 0, translateY: '-10px', onComplete: () => scrollToTop.style.display = 'none', easing: 'easeInOutQuad', duration: 100 }); 109 110 scrollToTopActive = false; 111 } 112 113 if(!ctx)return; 114 ctx.clearRect(0, 0, photoContainer.width, photoContainer.height); 115 116 scroll = scroll + (targetScroll - scroll) * 0.1; 117 118 window.PhotoListRenderingManager.Render(ctx, photoContainer!, scroll); 119 120 if(window.PhotoManager.FilteredPhotos.length == 0){ 121 ctx.textAlign = 'center'; 122 ctx.textBaseline = 'middle'; 123 ctx.globalAlpha = 1; 124 ctx.fillStyle = '#fff'; 125 ctx.font = '40px Rubik'; 126 127 ctx.fillText("It's looking empty in here! You have no photos :O", photoContainer.width / 2, photoContainer.height / 2); 128 } 129 } 130 131 listen('hide-window', () => { 132 quitRender = true; 133 console.log('Hide Window'); 134 }) 135 136 listen('show-window', () => { 137 if(quitRender)quitRender = false; 138 console.log('Shown Window'); 139 140 photoContainer.width = window.innerWidth; 141 photoContainer.height = window.innerHeight; 142 143 if(window.PhotoManager.HasFirstLoaded){ 144 requestAnimationFrame(render); 145 window.PhotoManager.HasFirstLoaded = false; 146 } 147 }) 148 149 window.PhotoManager.OnLoadingFinished(() => { 150 invoke('close_splashscreen'); 151 152 animate('.reload-photos', { 153 opacity: 1, 154 duration: 150, 155 easing: 'easeInOutQuad' 156 }) 157 158 window.PhotoListRenderingManager.SetCanvas(photoContainer!); 159 render(); 160 }); 161 162 onMount(() => { 163 // Update Stuff 164 fetch('https://api.github.com/repos/phaze-the-dumb/VRChat-Photo-Manager/releases/latest') 165 .then(data => { 166 if(data.status !== 200)return; 167 168 data.json().then(async data => { 169 let currentVersion = await invoke('get_version'); 170 setUpdateAvailable(data.tag_name !== currentVersion); 171 }) 172 }) 173 .catch(e => { 174 console.error(e); 175 setUpdateAvailable(false); 176 }) 177 178 // Other Stuff 179 ctx = photoContainer.getContext('2d')!; 180 181 window.PhotoManager.Load(); 182 183 utils.set(scrollToTop, { opacity: 0, translateY: '-10px', display: 'none' }); 184 185 photoContainer.onwheel = ( e: WheelEvent ) => { 186 targetScroll += e.deltaY * 2; 187 188 if(targetScroll < 0) 189 targetScroll = 0; 190 }; 191 192 window.addEventListener('keyup', closeWithKey); 193 window.addEventListener('resize', onResize); 194 195 photoContainer.width = window.innerWidth; 196 photoContainer.height = window.innerHeight; 197 198 photoContainer.onclick = ( e: MouseEvent ) => { 199 let photo = window.PhotoManager.FilteredPhotos.find(x => 200 e.clientX > x.x && 201 e.clientY > x.y && 202 e.clientX < x.x + x.scaledWidth! && 203 e.clientY < x.y + x.scaledHeight! && 204 x.shown 205 ); 206 207 if(photo) 208 window.PhotoViewerManager.OpenPhoto(photo); 209 // else 210 // currentPhotoIndex = -1; 211 } 212 }) 213 214 onCleanup(() => { 215 photoContainer.onwheel = () => {}; 216 photoContainer.onclick = () => {}; 217 218 window.removeEventListener('keyup', closeWithKey); 219 window.removeEventListener('resize', onResize); 220 }) 221 222 return ( 223 <div class="photo-list"> 224 <div ref={filterContainer!} class="filter-container"> 225 <FilterMenu /> 226 </div> 227 228 <div class="scroll-to-top" ref={( el ) => scrollToTop = el} onClick={() => targetScroll = 0}> 229 <div class="icon"> 230 <img draggable="false" src="/icon/angle-up-solid.svg"></img> 231 </div> 232 </div> 233 234 <div class="filter-options"> 235 <div> 236 <div onClick={() => { 237 if(currentPopup != ListPopup.NONE)return closeCurrentPopup(); 238 currentPopup = ListPopup.FILTERS; 239 240 filterContainer!.style.display = 'block'; 241 242 animate(filterContainer!, { 243 opacity: 1, 244 translateY: 0, 245 easing: 'easeInOutQuad', 246 duration: 100 247 }); 248 }} class="icon"> 249 <img draggable="false" style={{ width: "20px", height: "20px" }} src="/icon/sliders-solid.svg"></img> 250 </div> 251 <div class="icon-label">Filters</div> 252 </div> 253 254 <div> 255 <div onClick={() => { 256 window.location.reload(); 257 }} class="icon"> 258 <img draggable="false" style={{ width: "20px", height: "20px" }} src="/icon/arrows-rotate-solid.svg"></img> 259 </div> 260 <div class="icon-label">Reload Photos</div> 261 </div> 262 263 <div> 264 <div onClick={() => { 265 utils.set('.settings', { display: 'block' }); 266 animate('.settings', { 267 opacity: 1, 268 translateX: '0px', 269 easing: 'easeInOutQuad', 270 duration: 250 271 }) 272 273 window.ViewManager.ChangeState(ViewState.SETTINGS); 274 }} class="icon"> 275 <img draggable="false" style={{ width: "20px", height: "20px" }} src="/icon/gear-solid-full.svg"></img> 276 </div> 277 <div class="icon-label">Settings</div> 278 </div> 279 280 <Show when={updateAvailable()}> 281 <div> 282 <div onClick={() => { 283 invoke('open_url', { url: 'https://github.com/phaze-the-dumb/VRChat-Photo-Manager/releases/latest' }); 284 }} class="icon"> 285 <img draggable="false" style={{ width: "20px", height: "20px" }} src="/icon/download-solid-full.svg"></img> 286 </div> 287 <div class="icon-label">Update Available</div> 288 </div> 289 </Show> 290 </div> 291 292 <canvas class="photo-container" ref={( el ) => photoContainer = el}></canvas> 293 </div> 294 ) 295} 296 297export default PhotoList;