A photo manager for VRChat.

create fancy as fuck settings menu

Changed files
+283 -3
src
+13 -1
src/Components/App.tsx
··· 1 - import { createSignal, createEffect, Switch, Match } from "solid-js"; 1 + import { createSignal, createEffect, Switch, Match, onMount } from "solid-js"; 2 2 import { listen } from '@tauri-apps/api/event'; 3 3 import { fetch, ResponseType } from "@tauri-apps/api/http" 4 4 import anime from "animejs"; ··· 7 7 import NavBar from "./NavBar"; 8 8 import PhotoList from "./PhotoList"; 9 9 import PhotoViewer from "./PhotoViewer"; 10 + import SettingsMenu from "./SettingsMenu"; 10 11 11 12 function App() { 12 13 invoke('close_splashscreen') ··· 125 126 console.warn('Authetication Denied'); 126 127 }) 127 128 129 + onMount(() => { 130 + anime.set('.settings', 131 + { 132 + display: 'none', 133 + opacity: 0, 134 + translateX: '500px' 135 + }) 136 + }) 137 + 128 138 return ( 129 139 <div class="container"> 130 140 <NavBar setLoadingType={setLoadingType} loggedIn={loggedIn} /> 131 141 <PhotoList setCurrentPhotoView={setCurrentPhotoView} currentPhotoView={currentPhotoView} photoNavChoice={photoNavChoice} setPhotoNavChoice={setPhotoNavChoice} setConfirmationBox={setConfirmationBox} /> 132 142 <PhotoViewer setPhotoNavChoice={setPhotoNavChoice} currentPhotoView={currentPhotoView} setCurrentPhotoView={setCurrentPhotoView} setConfirmationBox={setConfirmationBox} /> 143 + 144 + <SettingsMenu /> 133 145 134 146 <div class="copy-notif">Image Copied!</div> 135 147
+26 -2
src/Components/NavBar.tsx
··· 59 59 <> 60 60 <div class="navbar"> 61 61 <div class="tabs"> 62 - <div class="nav-tab">Photos</div> 62 + <div class="nav-tab" onClick={() => { 63 + anime( 64 + { 65 + targets: '.settings', 66 + opacity: 0, 67 + translateX: '500px', 68 + easing: 'easeInOutQuad', 69 + duration: 250, 70 + complete: () => { 71 + anime.set('.settings', { display: 'none' }); 72 + } 73 + }) 74 + }}>Photos</div> 63 75 </div> 64 76 <div class="account" onClick={() => setDropdownVisibility(!dropdownVisible)}> 65 77 <Show when={props.loggedIn().loggedIn}> ··· 70 82 </div> 71 83 72 84 <div class="dropdown" ref={( el ) => dropdown = el}> 73 - <div class="dropdown-button">Settings</div> 85 + <div class="dropdown-button" onClick={() => { 86 + anime.set('.settings', { display: 'block' }); 87 + anime( 88 + { 89 + targets: '.settings', 90 + opacity: 1, 91 + translateX: '0px', 92 + easing: 'easeInOutQuad', 93 + duration: 250 94 + }) 95 + 96 + setDropdownVisibility(false); 97 + }}>Settings</div> 74 98 75 99 <Show when={props.loggedIn().loggedIn == false} fallback={ 76 100 <div class="dropdown-button" onClick={() => {
+163
src/Components/SettingsMenu.tsx
··· 1 + import { onMount } from "solid-js"; 2 + import anime from "animejs"; 3 + 4 + let SettingsMenu = () => { 5 + let sliderBar: HTMLElement; 6 + let settingsContainer: HTMLElement; 7 + let currentButton = 0; 8 + 9 + onMount(() => { 10 + let sliderMouseDown = false; 11 + let mouseStartX = 0; 12 + 13 + let width = window.innerWidth; 14 + let buttons = [ 370, 680 ]; 15 + 16 + let sliderPos = width / 2 - buttons[currentButton]; 17 + let sliderScale = width / (buttons[1] - buttons[0]); 18 + 19 + let render = () => { 20 + requestAnimationFrame(render); 21 + 22 + if(!sliderMouseDown){ 23 + sliderPos = sliderPos + (width / 2 - buttons[currentButton] - sliderPos) * 0.2; 24 + anime.set(sliderBar, { translateX: sliderPos }); 25 + 26 + settingsContainer.style.left = (sliderPos - (width / 2 - buttons[0])) * sliderScale + 'px'; 27 + } 28 + } 29 + 30 + render(); 31 + anime.set(sliderBar, { translateX: sliderPos }); 32 + 33 + sliderBar.addEventListener('touchstart', ( e: TouchEvent ) => { 34 + sliderMouseDown = true; 35 + mouseStartX = e.touches[0].clientX; 36 + }) 37 + 38 + window.addEventListener('touchmove', ( e: TouchEvent ) => { 39 + if(sliderMouseDown){ 40 + anime.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.touches[0].clientX) }); 41 + settingsContainer.style.left = (sliderPos - (mouseStartX - e.touches[0].clientX) - (width / 2 - buttons[0])) * sliderScale + 'px'; 42 + } 43 + }) 44 + 45 + window.addEventListener('touchend', ( e: TouchEvent ) => { 46 + if(sliderMouseDown){ 47 + sliderPos = sliderPos - (mouseStartX - e.touches[0].clientX); 48 + 49 + anime.set(sliderBar, { translateX: sliderPos }); 50 + sliderMouseDown = false; 51 + 52 + let shortestDistance = 0; 53 + let selectedButton = -1; 54 + 55 + buttons.forEach(( pos, indx ) => { 56 + let dis = Math.abs(sliderPos - (width / 2 - pos)); 57 + 58 + if(selectedButton === -1){ 59 + shortestDistance = dis; 60 + selectedButton = indx; 61 + } else if(shortestDistance > dis){ 62 + shortestDistance = dis; 63 + selectedButton = indx; 64 + } 65 + }) 66 + 67 + currentButton = selectedButton; 68 + } 69 + }) 70 + 71 + sliderBar.addEventListener('mousedown', ( e: MouseEvent ) => { 72 + sliderMouseDown = true; 73 + mouseStartX = e.clientX; 74 + }); 75 + 76 + window.addEventListener('mousemove', ( e: MouseEvent ) => { 77 + if(sliderMouseDown){ 78 + anime.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.clientX) }); 79 + settingsContainer.style.left = sliderPos - (mouseStartX - e.clientX) + 'px'; 80 + settingsContainer.style.left = (sliderPos - (mouseStartX - e.clientX) - (width / 2 - buttons[0])) * sliderScale + 'px'; 81 + } 82 + }) 83 + 84 + window.addEventListener('mouseup', ( e: MouseEvent ) => { 85 + if(sliderMouseDown){ 86 + sliderPos = sliderPos - (mouseStartX - e.clientX); 87 + 88 + anime.set(sliderBar, { translateX: sliderPos }); 89 + sliderMouseDown = false; 90 + 91 + let shortestDistance = 0; 92 + let selectedButton = -1; 93 + 94 + buttons.forEach(( pos, indx ) => { 95 + let dis = Math.abs(sliderPos - (width / 2 - pos)); 96 + 97 + if(selectedButton === -1){ 98 + shortestDistance = dis; 99 + selectedButton = indx; 100 + } else if(shortestDistance > dis){ 101 + shortestDistance = dis; 102 + selectedButton = indx; 103 + } 104 + }) 105 + 106 + currentButton = selectedButton; 107 + } 108 + }) 109 + 110 + window.addEventListener('resize', () => { 111 + width = window.innerWidth; 112 + sliderPos = width / 2 - buttons[currentButton]; 113 + sliderScale = (buttons[1] - buttons[0]) / width; 114 + 115 + anime.set(sliderBar, { translateX: sliderPos }); 116 + }) 117 + 118 + sliderBar.addEventListener('wheel', ( e: WheelEvent ) => { 119 + if(e.deltaY > 0){ 120 + if(buttons[currentButton + 1]) 121 + currentButton++; 122 + } else{ 123 + if(buttons[currentButton - 1]) 124 + currentButton--; 125 + } 126 + }) 127 + }) 128 + 129 + return ( 130 + <div class="settings"> 131 + <div class="settings-container" ref={( el ) => settingsContainer = el}> 132 + <div class="settings-block"> 133 + <h1>Program Settings</h1> 134 + </div> 135 + <div class="settings-block"> 136 + <h1>Account Settings</h1> 137 + </div> 138 + </div> 139 + 140 + <div class="slide-bar-tri"></div> 141 + <div class="slide-bar"> 142 + <div class="inner-slide-bar" ref={( el ) => sliderBar = el}> 143 + <div class="slider-dot"></div> 144 + <div class="slider-dot"></div> 145 + <div class="slider-dot"></div> 146 + <div class="slider-dot"></div> 147 + <div class="slider-dot"></div> 148 + <div class="slider-text" onClick={() => currentButton = 0}>Program Settings</div> 149 + <div class="slider-dot"></div> 150 + <div class="slider-dot"></div> 151 + <div class="slider-text" onClick={() => currentButton = 1}>Account Settings</div> 152 + <div class="slider-dot"></div> 153 + <div class="slider-dot"></div> 154 + <div class="slider-dot"></div> 155 + <div class="slider-dot"></div> 156 + <div class="slider-dot"></div> 157 + </div> 158 + </div> 159 + </div> 160 + ) 161 + } 162 + 163 + export default SettingsMenu;
+81
src/styles.css
··· 437 437 438 438 .world-name{ 439 439 font-size: 17px; 440 + } 441 + 442 + .settings{ 443 + position: fixed; 444 + top: 0; 445 + left: 0; 446 + width: 100%; 447 + height: 100%; 448 + background: rgba(0, 0, 0, 0.4); 449 + backdrop-filter: blur(100px); 450 + } 451 + 452 + .slide-bar{ 453 + position: fixed; 454 + bottom: 0; 455 + left: 0; 456 + width: 100%; 457 + height: 50px; 458 + border-top: #aaa 1px solid; 459 + overflow-x: hidden; 460 + mask-image: linear-gradient(to left, #0000 0%, #000 20%, #000 80%, #0000 100%); 461 + background: #aaa2; 462 + box-shadow: #000 0 0 10px; 463 + } 464 + 465 + .inner-slide-bar{ 466 + display: flex; 467 + height: 50px; 468 + width: 200%; 469 + color: white; 470 + align-items: center; 471 + cursor: pointer; 472 + user-select: none; 473 + } 474 + 475 + .slider-dot{ 476 + width: 5px; 477 + height: 5px; 478 + border-radius: 5px; 479 + background: #aaa; 480 + margin: auto 25px; 481 + } 482 + 483 + .slider-text{ 484 + width: 200px; 485 + text-align: center; 486 + height: 50px; 487 + display: flex; 488 + justify-content: center; 489 + align-items: center; 490 + color: #aaa; 491 + transition: 0.25s; 492 + } 493 + 494 + .slider-text:hover{ 495 + color: #fff; 496 + } 497 + 498 + .slide-bar-tri{ 499 + position: fixed; 500 + bottom: 40px; 501 + left: 50%; 502 + transform: translateX(-50%); 503 + border: transparent solid 5px; 504 + border-top: #fff solid 5px; 505 + } 506 + 507 + .settings-container{ 508 + position: fixed; 509 + top: 50px; 510 + left: 0px; 511 + width: 200%; 512 + height: calc(100% - 100px); 513 + display: flex; 514 + } 515 + 516 + .settings-block{ 517 + width: 50%; 518 + height: 100%; 519 + color: white; 520 + text-align: center; 440 521 }