A photo manager for VRChat.

Added support for multilayer photos

phaz.uk 112d77a4 d4078e02

verified
+6 -3
changelog
··· 105 105 106 106 v0.2.6: 107 107 - Fixed photos not being loaded if they're too low resolution 108 - - Added close to tray toggle 109 108 - Fixed "Open in folder" not selecting files on linux 110 - - Remove all sync stuff 111 109 - Fixed scroll to top button not animating out 112 110 - Fixed scroll to top button being ontop of filters menu 113 111 - Fixed photo ordering 114 112 - Fixed automatic updates 115 113 - Fixed broken legacy named photos 116 - - Fixed photos being loaded with the wrong resolution 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 124 let photo = (el as PhotoListPhoto).Photo; 125 125 126 126 // === DEBUG === 127 - ctx.strokeStyle = '#f00'; 128 - ctx.strokeRect((rowXPos - row.Width / 2) + canvas.width / 2, currentY - scroll, photo.scaledWidth!, row.Height); 127 + // ctx.strokeStyle = '#f00'; 128 + // ctx.strokeRect((rowXPos - row.Width / 2) + canvas.width / 2, currentY - scroll, photo.scaledWidth!, row.Height); 129 129 130 130 if(!photo.loaded) 131 131 // If the photo is not loaded, start a new task and load it in that task
+3
src/Components/Managers/PhotoManager.tsx
··· 108 108 109 109 let photo = this.Photos.find(x => x.path === data.path); 110 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. 111 114 112 115 this._lastLoaded = photo.index; 113 116
-1
src/Components/PhotoList.tsx
··· 154 154 }) 155 155 156 156 window.PhotoListRenderingManager.SetCanvas(photoContainer!); 157 - 158 157 render(); 159 158 }); 160 159
+85 -3
src/Components/PhotoViewer.tsx
··· 1 1 import { For, Show, createEffect, onCleanup, onMount } from "solid-js"; 2 2 import { invoke } from '@tauri-apps/api/core'; 3 3 import { WorldCache } from "./Structs/WorldCache"; 4 - import { animate, JSAnimation, utils } from "animejs"; 4 + import { animate, createSpring, JSAnimation, utils } from "animejs"; 5 5 6 6 let PhotoViewer = () => { 7 7 let viewer: HTMLElement; ··· 23 23 let allowedToOpenTray = false; 24 24 25 25 let authorProfileButton: HTMLDivElement; 26 + 27 + let photoLayerManager!: HTMLDivElement; 26 28 27 29 let switchPhotoWithKey = ( e: KeyboardEvent ) => { 28 30 switch(e.key){ ··· 84 86 } 85 87 86 88 let copyImage = () => { 87 - invoke('copy_image', { path: window.PhotoViewerManager.CurrentPhoto()!.path }) 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 }) 88 105 .then(() => { 89 106 utils.set('.copy-notif', { translateX: '-50%', translateY: '-100px' }); 90 107 animate('.copy-notif', { ··· 136 153 onMount(() => { 137 154 utils.set(photoControls, { translateX: '-50%' }); 138 155 utils.set(photoTrayCloseBtn, { translateX: '-50%', opacity: 0, scale: '0.75', bottom: '10px' }); 156 + utils.set(photoLayerManager, { translateY: '20px', opacity: 0, display: 'none' }); 139 157 140 158 window.addEventListener('keyup', switchPhotoWithKey); 141 159 ··· 153 171 viewerContextMenu.style.display = 'none'; 154 172 } 155 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' }) }); 156 181 }); 157 182 158 183 viewerContextMenuButtons[0].onclick = async () => { ··· 367 392 ) 368 393 } 369 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 + 370 405 let toggleLayerManager = () => { 371 - 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 + } 372 420 } 373 421 374 422 // TODO: Make layers selectable 375 423 376 424 return ( 377 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 + 378 460 <div class="photo-context-menu" ref={( el ) => viewerContextMenu = el}> 379 461 <div ref={( el ) => viewerContextMenuButtons.push(el)}>Open file location</div> 380 462 <div ref={( el ) => viewerContextMenuButtons.push(el)}>Copy image</div>
+1 -1
src/css/tray.css
··· 17 17 left: 50%; 18 18 transform: translate(-50%); 19 19 color: white; 20 - background: #8885; 20 + background: rgba(43, 43, 43, 0.76); 21 21 backdrop-filter: blur(10px); 22 22 -webkit-backdrop-filter: blur(10px); 23 23 box-shadow: #0008 0 0 10px;
+26 -3
src/css/viewer.css
··· 38 38 left: 0; 39 39 padding: 10px; 40 40 border-radius: 5px; 41 - background: #555a; 41 + background: rgba(43, 43, 43, 0.76); 42 42 color: #aaa; 43 43 box-shadow: #0005 0 0 10px; 44 44 opacity: 0; ··· 80 80 -webkit-user-select: none; 81 81 cursor: pointer; 82 82 z-index: 7; 83 - box-shadow: #0008 0 0 10px; 83 + background: rgba(43, 43, 43, 0.76); 84 84 } 85 85 86 86 .viewer-close{ ··· 157 157 left: 50%; 158 158 color: white; 159 159 transform: translateX(-50%) translateY(-100px); 160 - background: #8885; 160 + background: rgba(43, 43, 43, 0.76); 161 161 padding: 10px 40px; 162 162 backdrop-filter: blur(10px); 163 163 -webkit-backdrop-filter: blur(10px); ··· 166 166 z-index: 12; 167 167 opacity: 0; 168 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; 169 192 }