A photo manager for VRChat.

Added support for multilayer photos

phaz.uk 112d77a4 d4078e02

verified
+6 -3
changelog
··· 105 106 v0.2.6: 107 - Fixed photos not being loaded if they're too low resolution 108 - - Added close to tray toggle 109 - Fixed "Open in folder" not selecting files on linux 110 - - Remove all sync stuff 111 - Fixed scroll to top button not animating out 112 - Fixed scroll to top button being ontop of filters menu 113 - Fixed photo ordering 114 - Fixed automatic updates 115 - Fixed broken legacy named photos 116 - - Fixed photos being loaded with the wrong resolution
··· 105 106 v0.2.6: 107 - Fixed photos not being loaded if they're too low resolution 108 - Fixed "Open in folder" not selecting files on linux 109 - Fixed scroll to top button not animating out 110 - Fixed scroll to top button being ontop of filters menu 111 - Fixed photo ordering 112 - Fixed automatic updates 113 - Fixed broken legacy named photos 114 + - Fixed photos being loaded with the wrong resolution 115 + 116 + - Added support for multilayer photos 117 + - Added close to tray toggle 118 + 119 + - Remove all sync stuff
+2 -2
src/Components/Managers/PhotoListRenderingManager.tsx
··· 124 let photo = (el as PhotoListPhoto).Photo; 125 126 // === DEBUG === 127 - ctx.strokeStyle = '#f00'; 128 - ctx.strokeRect((rowXPos - row.Width / 2) + canvas.width / 2, currentY - scroll, photo.scaledWidth!, row.Height); 129 130 if(!photo.loaded) 131 // If the photo is not loaded, start a new task and load it in that task
··· 124 let photo = (el as PhotoListPhoto).Photo; 125 126 // === DEBUG === 127 + // ctx.strokeStyle = '#f00'; 128 + // ctx.strokeRect((rowXPos - row.Width / 2) + canvas.width / 2, currentY - scroll, photo.scaledWidth!, row.Height); 129 130 if(!photo.loaded) 131 // If the photo is not loaded, start a new task and load it in that task
+3
src/Components/Managers/PhotoManager.tsx
··· 108 109 let photo = this.Photos.find(x => x.path === data.path); 110 if(!photo)return console.error('Cannot find photo.', data); 111 112 this._lastLoaded = photo.index; 113
··· 108 109 let photo = this.Photos.find(x => x.path === data.path); 110 if(!photo)return console.error('Cannot find photo.', data); 111 + // NOTE: this is triggered by multilayer photo layers loading their metadata 112 + // we don't need to store metadata of those photos as they inherit this 113 + // data from the main photo. 114 115 this._lastLoaded = photo.index; 116
-1
src/Components/PhotoList.tsx
··· 154 }) 155 156 window.PhotoListRenderingManager.SetCanvas(photoContainer!); 157 - 158 render(); 159 }); 160
··· 154 }) 155 156 window.PhotoListRenderingManager.SetCanvas(photoContainer!); 157 render(); 158 }); 159
+85 -3
src/Components/PhotoViewer.tsx
··· 1 import { For, Show, createEffect, onCleanup, onMount } from "solid-js"; 2 import { invoke } from '@tauri-apps/api/core'; 3 import { WorldCache } from "./Structs/WorldCache"; 4 - import { animate, JSAnimation, utils } from "animejs"; 5 6 let PhotoViewer = () => { 7 let viewer: HTMLElement; ··· 23 let allowedToOpenTray = false; 24 25 let authorProfileButton: HTMLDivElement; 26 27 let switchPhotoWithKey = ( e: KeyboardEvent ) => { 28 switch(e.key){ ··· 84 } 85 86 let copyImage = () => { 87 - invoke('copy_image', { path: window.PhotoViewerManager.CurrentPhoto()!.path }) 88 .then(() => { 89 utils.set('.copy-notif', { translateX: '-50%', translateY: '-100px' }); 90 animate('.copy-notif', { ··· 136 onMount(() => { 137 utils.set(photoControls, { translateX: '-50%' }); 138 utils.set(photoTrayCloseBtn, { translateX: '-50%', opacity: 0, scale: '0.75', bottom: '10px' }); 139 140 window.addEventListener('keyup', switchPhotoWithKey); 141 ··· 153 viewerContextMenu.style.display = 'none'; 154 } 155 }) 156 }); 157 158 viewerContextMenuButtons[0].onclick = async () => { ··· 367 ) 368 } 369 370 let toggleLayerManager = () => { 371 - 372 } 373 374 // TODO: Make layers selectable 375 376 return ( 377 <div class="photo-viewer" ref={( el ) => viewer = el}> 378 <div class="photo-context-menu" ref={( el ) => viewerContextMenu = el}> 379 <div ref={( el ) => viewerContextMenuButtons.push(el)}>Open file location</div> 380 <div ref={( el ) => viewerContextMenuButtons.push(el)}>Copy image</div>
··· 1 import { For, Show, createEffect, onCleanup, onMount } from "solid-js"; 2 import { invoke } from '@tauri-apps/api/core'; 3 import { WorldCache } from "./Structs/WorldCache"; 4 + import { animate, createSpring, JSAnimation, utils } from "animejs"; 5 6 let PhotoViewer = () => { 7 let viewer: HTMLElement; ··· 23 let allowedToOpenTray = false; 24 25 let authorProfileButton: HTMLDivElement; 26 + 27 + let photoLayerManager!: HTMLDivElement; 28 29 let switchPhotoWithKey = ( e: KeyboardEvent ) => { 30 switch(e.key){ ··· 86 } 87 88 let copyImage = () => { 89 + let path; 90 + let photo = window.PhotoViewerManager.CurrentPhoto()!; 91 + 92 + switch(layerManagerViewing){ 93 + case LayerManagerView.DEFAULT: 94 + path = photo.path; 95 + break; 96 + case LayerManagerView.ENVIRONMENT: 97 + path = photo.environmentLayer!.path; 98 + break; 99 + case LayerManagerView.PLAYER: 100 + path = photo.playerLayer!.path; 101 + break; 102 + } 103 + 104 + invoke('copy_image', { path }) 105 .then(() => { 106 utils.set('.copy-notif', { translateX: '-50%', translateY: '-100px' }); 107 animate('.copy-notif', { ··· 153 onMount(() => { 154 utils.set(photoControls, { translateX: '-50%' }); 155 utils.set(photoTrayCloseBtn, { translateX: '-50%', opacity: 0, scale: '0.75', bottom: '10px' }); 156 + utils.set(photoLayerManager, { translateY: '20px', opacity: 0, display: 'none' }); 157 158 window.addEventListener('keyup', switchPhotoWithKey); 159 ··· 171 viewerContextMenu.style.display = 'none'; 172 } 173 }) 174 + }); 175 + 176 + window.CloseAllPopups.push(() => { 177 + layerManagerOpen = false; 178 + if(layerManagerAnimation)layerManagerAnimation.cancel(); 179 + 180 + layerManagerAnimation = animate(photoLayerManager, { translateY: '20px', opacity: 0, duration: 100, onComplete: () => utils.set(photoLayerManager, { display: 'none' }) }); 181 }); 182 183 viewerContextMenuButtons[0].onclick = async () => { ··· 392 ) 393 } 394 395 + enum LayerManagerView{ 396 + DEFAULT, 397 + PLAYER, 398 + ENVIRONMENT 399 + } 400 + 401 + let layerManagerOpen = false; 402 + let layerManagerAnimation: null | JSAnimation = null; 403 + let layerManagerViewing = LayerManagerView.DEFAULT; 404 + 405 let toggleLayerManager = () => { 406 + if(layerManagerOpen){ 407 + // Close 408 + layerManagerOpen = false; 409 + if(layerManagerAnimation)layerManagerAnimation.cancel(); 410 + 411 + layerManagerAnimation = animate(photoLayerManager, { translateY: '20px', opacity: 0, duration: 100, onComplete: () => utils.set(photoLayerManager, { display: 'none' }) }); 412 + } else{ 413 + // Open 414 + layerManagerOpen = true; 415 + if(layerManagerAnimation)layerManagerAnimation.cancel(); 416 + 417 + utils.set(photoLayerManager, { display: 'block' }); 418 + layerManagerAnimation = animate(photoLayerManager, { translateY: '0px', opacity: 1, duration: 100 }); 419 + } 420 } 421 422 // TODO: Make layers selectable 423 424 return ( 425 <div class="photo-viewer" ref={( el ) => viewer = el}> 426 + <div class="photo-layer-manager" ref={photoLayerManager}> 427 + <Show when={window.PhotoViewerManager.CurrentPhoto()?.playerLayer}> 428 + <div class="photo-layer-manager-layer" onClick={() => { 429 + let photo = window.PhotoViewerManager.CurrentPhoto()?.playerLayer; 430 + if(!photo)return; 431 + 432 + layerManagerViewing = LayerManagerView.PLAYER; 433 + 434 + imageViewer.src = (window.OS === "windows" ? "http://photo.localhost/" : 'photo://localhost/') + photo.path.split('\\').join('/') + "?full"; 435 + imageViewer.crossOrigin = 'anonymous'; 436 + }}>Player Layer</div> 437 + </Show> 438 + <Show when={window.PhotoViewerManager.CurrentPhoto()?.environmentLayer}> 439 + <div class="photo-layer-manager-layer" onClick={() => { 440 + let photo = window.PhotoViewerManager.CurrentPhoto()?.environmentLayer; 441 + if(!photo)return; 442 + 443 + layerManagerViewing = LayerManagerView.ENVIRONMENT; 444 + 445 + imageViewer.src = (window.OS === "windows" ? "http://photo.localhost/" : 'photo://localhost/') + photo.path.split('\\').join('/') + "?full"; 446 + imageViewer.crossOrigin = 'anonymous'; 447 + }}>Environment Layer</div> 448 + </Show> 449 + <div class="photo-layer-manager-layer" onClick={() => { 450 + let photo = window.PhotoViewerManager.CurrentPhoto(); 451 + if(!photo)return; 452 + 453 + layerManagerViewing = LayerManagerView.DEFAULT; 454 + 455 + imageViewer.src = (window.OS === "windows" ? "http://photo.localhost/" : 'photo://localhost/') + photo.path.split('\\').join('/') + "?full"; 456 + imageViewer.crossOrigin = 'anonymous'; 457 + }}>Default Layer</div> 458 + </div> 459 + 460 <div class="photo-context-menu" ref={( el ) => viewerContextMenu = el}> 461 <div ref={( el ) => viewerContextMenuButtons.push(el)}>Open file location</div> 462 <div ref={( el ) => viewerContextMenuButtons.push(el)}>Copy image</div>
+1 -1
src/css/tray.css
··· 17 left: 50%; 18 transform: translate(-50%); 19 color: white; 20 - background: #8885; 21 backdrop-filter: blur(10px); 22 -webkit-backdrop-filter: blur(10px); 23 box-shadow: #0008 0 0 10px;
··· 17 left: 50%; 18 transform: translate(-50%); 19 color: white; 20 + background: rgba(43, 43, 43, 0.76); 21 backdrop-filter: blur(10px); 22 -webkit-backdrop-filter: blur(10px); 23 box-shadow: #0008 0 0 10px;
+26 -3
src/css/viewer.css
··· 38 left: 0; 39 padding: 10px; 40 border-radius: 5px; 41 - background: #555a; 42 color: #aaa; 43 box-shadow: #0005 0 0 10px; 44 opacity: 0; ··· 80 -webkit-user-select: none; 81 cursor: pointer; 82 z-index: 7; 83 - box-shadow: #0008 0 0 10px; 84 } 85 86 .viewer-close{ ··· 157 left: 50%; 158 color: white; 159 transform: translateX(-50%) translateY(-100px); 160 - background: #8885; 161 padding: 10px 40px; 162 backdrop-filter: blur(10px); 163 -webkit-backdrop-filter: blur(10px); ··· 166 z-index: 12; 167 opacity: 0; 168 pointer-events: none; 169 }
··· 38 left: 0; 39 padding: 10px; 40 border-radius: 5px; 41 + background: rgba(43, 43, 43, 0.76); 42 color: #aaa; 43 box-shadow: #0005 0 0 10px; 44 opacity: 0; ··· 80 -webkit-user-select: none; 81 cursor: pointer; 82 z-index: 7; 83 + background: rgba(43, 43, 43, 0.76); 84 } 85 86 .viewer-close{ ··· 157 left: 50%; 158 color: white; 159 transform: translateX(-50%) translateY(-100px); 160 + background: rgba(43, 43, 43, 0.76); 161 padding: 10px 40px; 162 backdrop-filter: blur(10px); 163 -webkit-backdrop-filter: blur(10px); ··· 166 z-index: 12; 167 opacity: 0; 168 pointer-events: none; 169 + } 170 + 171 + .photo-layer-manager{ 172 + background: rgba(43, 43, 43, 0.76); 173 + color: #fff; 174 + padding: 10px; 175 + backdrop-filter: blur(10px); 176 + position: fixed; 177 + bottom: 10px; 178 + left: 10px; 179 + border-radius: 10px; 180 + } 181 + 182 + .photo-layer-manager-layer{ 183 + cursor: pointer; 184 + -webkit-user-select: none; 185 + user-select: none; 186 + padding: 5px 20px; 187 + transition: 0.1s; 188 + } 189 + 190 + .photo-layer-manager-layer:hover{ 191 + color: #bbb; 192 }