A photo manager for VRChat.
1import { onCleanup, onMount, Show } from "solid-js"; 2import { bytesToFormatted } from "../utils"; 3import { invoke } from '@tauri-apps/api/core'; 4import { ViewState } from "./Managers/ViewManager"; 5import { animate, utils } from "animejs"; 6 7let SettingsMenu = () => { 8 // let sliderBar: HTMLElement; 9 let settingsContainer: HTMLElement; 10 // let currentButton = 0; 11 // let lastClickedButton = -1; 12 let finalPathConfirm: HTMLElement; 13 let finalPathInput: HTMLElement; 14 let finalPathData: string; 15 let finalPathPreviousData: string; 16 17 let closeWithKey = ( e: KeyboardEvent ) => { 18 if(e.key === 'Escape'){ 19 window.ViewManager.ChangeState(ViewState.PHOTO_LIST); 20 animate('.settings', { 21 opacity: 0, 22 translateX: '500px', 23 easing: 'easeInOutQuad', 24 duration: 250, 25 onComplete: () => { 26 utils.set('.settings', { display: 'none' }); 27 } 28 }) 29 } 30 } 31 32 onMount(async () => { 33 if(await invoke('get_config_value_string', { key: 'transparent' }) === "true"){ 34 invoke('set_config_value_string', { key: 'transparent', value: 'true' }); 35 36 animate(document.body, { background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); 37 animate('.settings', { background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); 38 } else{ 39 invoke('set_config_value_string', { key: 'transparent', value: 'false' }); 40 41 animate(document.body, { background: 'rgba(0, 0, 0, 1)', easing: 'linear', duration: 100 }); 42 animate('.settings', { background: 'rgba(0, 0, 0, 0)', easing: 'linear', duration: 100 }); 43 } 44 45 // let sliderMouseDown = false; 46 // let mouseStartX = 0; 47 48 // let width = window.innerWidth; 49 // let buttons = [ 370, 680 ]; 50 51 // let sliderPos = width / 2 - buttons[currentButton]; 52 // let sliderScale = width / (buttons[1] - buttons[0]); 53 54 // let render = () => { 55 // requestAnimationFrame(render); 56 57 // if(!sliderMouseDown){ 58 // sliderPos = sliderPos + (width / 2 - buttons[currentButton] - sliderPos) * 0.25; 59 // utils.set(sliderBar, { translateX: sliderPos }); 60 61 // settingsContainer.style.left = (sliderPos - (width / 2 - buttons[0])) * sliderScale + 'px'; 62 // } 63 // } 64 65 // render(); 66 // utils.set(sliderBar, { translateX: sliderPos }); 67 68 // sliderBar.addEventListener('touchstart', ( e: TouchEvent ) => { 69 // sliderMouseDown = true; 70 // mouseStartX = e.touches[0].clientX; 71 // }) 72 73 // window.addEventListener('touchmove', ( e: TouchEvent ) => { 74 // if(sliderMouseDown){ 75 // utils.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.touches[0].clientX) }); 76 // settingsContainer.style.left = (sliderPos - (mouseStartX - e.touches[0].clientX) - (width / 2 - buttons[0])) * sliderScale + 'px'; 77 // } 78 // }) 79 80 // window.addEventListener('keyup', closeWithKey); 81 82 // window.addEventListener('touchend', ( e: TouchEvent ) => { 83 // if(sliderMouseDown){ 84 // sliderPos = sliderPos - (mouseStartX - e.touches[0].clientX); 85 86 // utils.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.touches[0].clientX) }); 87 // sliderMouseDown = false; 88 89 // if(Math.abs(mouseStartX - e.touches[0].clientX) > 50){ 90 // let shortestDistance = 0; 91 // let selectedButton = -1; 92 93 // buttons.forEach(( pos, indx ) => { 94 // let dis = Math.abs(sliderPos - (width / 2 - pos)); 95 96 // if(selectedButton === -1){ 97 // shortestDistance = dis; 98 // selectedButton = indx; 99 // } else if(shortestDistance > dis){ 100 // shortestDistance = dis; 101 // selectedButton = indx; 102 // } 103 // }) 104 105 // currentButton = selectedButton; 106 // } else if(lastClickedButton != -1){ 107 // currentButton = lastClickedButton; 108 // lastClickedButton = -1 109 // } 110 // } 111 // }) 112 113 // sliderBar.addEventListener('mousedown', ( e: MouseEvent ) => { 114 // sliderMouseDown = true; 115 // mouseStartX = e.clientX; 116 // }); 117 118 // window.addEventListener('mousemove', ( e: MouseEvent ) => { 119 // if(sliderMouseDown){ 120 // utils.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.clientX) }); 121 // settingsContainer.style.left = sliderPos - (mouseStartX - e.clientX) + 'px'; 122 // settingsContainer.style.left = (sliderPos - (mouseStartX - e.clientX) - (width / 2 - buttons[0])) * sliderScale + 'px'; 123 // } 124 // }) 125 126 // window.addEventListener('mouseup', ( e: MouseEvent ) => { 127 // if(sliderMouseDown){ 128 // sliderPos = sliderPos - (mouseStartX - e.clientX); 129 130 // utils.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.clientX) }); 131 // sliderMouseDown = false; 132 133 // if(Math.abs(mouseStartX - e.clientX) > 50){ 134 // let shortestDistance = 0; 135 // let selectedButton = -1; 136 137 // buttons.forEach(( pos, indx ) => { 138 // let dis = Math.abs(sliderPos - (width / 2 - pos)); 139 140 // if(selectedButton === -1){ 141 // shortestDistance = dis; 142 // selectedButton = indx; 143 // } else if(shortestDistance > dis){ 144 // shortestDistance = dis; 145 // selectedButton = indx; 146 // } 147 // }) 148 149 // currentButton = selectedButton; 150 // } else if(lastClickedButton != -1){ 151 // currentButton = lastClickedButton; 152 // lastClickedButton = -1 153 // } 154 // } 155 // }) 156 157 // window.addEventListener('resize', () => { 158 // width = window.innerWidth; 159 // sliderPos = width / 2 - buttons[currentButton]; 160 // sliderScale = width / (buttons[1] - buttons[0]); 161 162 // utils.set(sliderBar, { translateX: sliderPos }); 163 // }) 164 165 // sliderBar.addEventListener('wheel', ( e: WheelEvent ) => { 166 // if(e.deltaY > 0){ 167 // if(buttons[currentButton + 1]) 168 // currentButton++; 169 // } else{ 170 // if(buttons[currentButton - 1]) 171 // currentButton--; 172 // } 173 // }) 174 }) 175 176 onCleanup(() => { 177 window.removeEventListener('keyup', closeWithKey); 178 }) 179 180 return ( 181 <div class="settings"> 182 <div class="settings-close" onClick={() => { 183 window.ViewManager.ChangeState(ViewState.PHOTO_LIST); 184 animate('.settings', 185 { 186 opacity: 0, 187 translateX: '500px', 188 easing: 'easeInOutQuad', 189 duration: 250, 190 onComplete: () => { 191 utils.set('.settings', { display: 'none' }); 192 } 193 }) 194 }}> 195 <div class="icon"><img draggable="false" src="/icon/x-solid.svg"></img></div> 196 </div> 197 <div class="settings-container" ref={( el ) => settingsContainer = el}> 198 <div class="settings-block"> 199 <h1>Storage Settings</h1> 200 <p>{ window.PhotoManager.PhotoCount() } Photos ({ bytesToFormatted(window.PhotoManager.PhotoSize(), 0) })</p> 201 202 <div class="selector"> 203 <input type="checkbox" id="start-in-bg-check" ref={async ( el ) => { 204 el.checked = await invoke('get_config_value_string', { key: 'start-in-bg' }) === "true" ? true : false; 205 }} onChange={( el ) => { 206 if(el.target.checked){ 207 invoke('set_config_value_string', { key: 'start-in-bg', value: 'true' }); 208 } else{ 209 invoke('set_config_value_string', { key: 'start-in-bg', value: 'false' }); 210 } 211 }} /> 212 Start in background 213 214 <label for="start-in-bg-check"> 215 <div class="selection-box"> 216 <div class="icon-small" style={{ margin: '0', display: 'inline-flex' }}> 217 <img draggable="false" width="10" height="10" src="/icon/check-solid.svg"></img> 218 </div> 219 </div> 220 </label> 221 </div> 222 223 <div class="selector"> 224 <input type="checkbox" id="close-to-tray-check" ref={async ( el ) => { 225 el.checked = await invoke('get_config_value_string', { key: 'close-to-tray' }) === "true" ? true : false; 226 }} onChange={( el ) => { 227 if(el.target.checked){ 228 invoke('set_config_value_string', { key: 'close-to-tray', value: 'true' }); 229 } else{ 230 invoke('set_config_value_string', { key: 'close-to-tray', value: 'false' }); 231 } 232 }} /> 233 Close to tray 234 235 <label for="close-to-tray-check"> 236 <div class="selection-box"> 237 <div class="icon-small" style={{ margin: '0', display: 'inline-flex' }}> 238 <img draggable="false" width="10" height="10" src="/icon/check-solid.svg"></img> 239 </div> 240 </div> 241 </label> 242 </div> 243 244 <Show when={window.OS === 'windows'}> 245 <div class="selector"> 246 <input type="checkbox" id="start-with-win-check" ref={async ( el ) => { 247 el.checked = await invoke('get_config_value_string', { key: 'start-with-win' }) === "true" ? true : false; 248 }} onChange={( el ) => { 249 if(el.target.checked){ 250 invoke('set_config_value_string', { key: 'start-with-win', value: 'true' }); 251 invoke("start_with_win", { start: true }); 252 } else{ 253 invoke('set_config_value_string', { key: 'start-with-win', value: 'false' }); 254 invoke("start_with_win", { start: false }); 255 } 256 }} /> 257 Start with windows 258 259 <label for="start-with-win-check"> 260 <div class="selection-box"> 261 <div class="icon-small" style={{ margin: '0', display: 'inline-flex' }}> 262 <img draggable="false" width="10" height="10" src="/icon/check-solid.svg"></img> 263 </div> 264 </div> 265 </label> 266 </div> 267 </Show> 268 269 <div class="selector"> 270 <input type="checkbox" id="transparent-check" ref={async ( el ) => { 271 el.checked = await invoke('get_config_value_string', { key: 'transparent' }) === "true" ? true : false; 272 }} onChange={( el ) => { 273 if(el.target.checked){ 274 invoke('set_config_value_string', { key: 'transparent', value: 'true' }); 275 276 animate(document.body, { background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); 277 animate('.settings', { background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); 278 } else{ 279 invoke('set_config_value_string', { key: 'transparent', value: 'false' }); 280 281 animate(document.body, { background: 'rgba(0, 0, 0, 1)', easing: 'linear', duration: 100 }); 282 animate('.settings', { background: 'rgba(0, 0, 0, 0)', easing: 'linear', duration: 100 }); 283 } 284 }} /> 285 Window Transparency 286 287 <label for="transparent-check"> 288 <div class="selection-box"> 289 <div class="icon-small" style={{ margin: '0', display: 'inline-flex' }}> 290 <img draggable="false" width="10" height="10" src="/icon/check-solid.svg"></img> 291 </div> 292 </div> 293 </label> 294 </div> 295 296 <br /> 297 <p> 298 VRChat Photo Path: 299 <span class="path" ref={( el ) => 300 invoke('get_user_photos_path').then(( path: any ) => { 301 el.innerHTML = ''; 302 el.appendChild(<span style={{ outline: 'none' }} ref={( el ) => finalPathInput = el} onInput={( el ) => { 303 finalPathConfirm.style.display = 'inline-block'; 304 finalPathData = el.target.innerHTML; 305 }} contenteditable>{path}</span> as Node); 306 307 finalPathPreviousData = path; 308 }) 309 }> 310 Loading... 311 </span> 312 <span style={{ display: 'none' }} ref={( el ) => finalPathConfirm = el}> 313 <span class="path" style={{ color: 'green' }} onClick={async () => { 314 finalPathPreviousData = finalPathData; 315 finalPathConfirm.style.display = 'none'; 316 317 await invoke('change_final_path', { newPath: finalPathData }); 318 window.location.reload(); 319 320 animate('.settings', { 321 opacity: 0, 322 translateX: '500px', 323 easing: 'easeInOutQuad', 324 duration: 250, 325 onComplete: () => { 326 utils.set('.settings', { display: 'none' }); 327 } 328 }) 329 330 window.location.reload(); 331 }}> 332 Save 333 </span> 334 335 <span class="path" style={{ color: 'red' }} onClick={() => { 336 finalPathData = finalPathPreviousData; 337 finalPathInput.innerHTML = finalPathPreviousData; 338 finalPathConfirm.style.display = 'none'; 339 }}> 340 Cancel 341 </span> 342 </span><br /><br /> 343 344 VRCPM Version: <span ref={( el ) => invoke('get_version').then((ver: any) => el.innerHTML = ver)}>Loading...</span> 345 </p> 346 347 <br /> 348 <p>To change the directory VRChat outputs photos to, you can change the "picture_output_folder" key in the <span style={{ color: '#00ccff', cursor: 'pointer' }} onClick={() => invoke('open_url', { url: 'https://docs.vrchat.com/docs/configuration-file#camera-and-screenshot-settings' })}>config.json file</span><br />Alternitavely, you can use VRCX to edit the config file.</p> 349 350 <br /> 351 <p>VRChat Photo Manager supports photos with extra metadata provided by VRCX.</p> 352 </div> 353 </div> 354 355 {/* <div class="slide-bar-tri"></div> 356 <div class="slide-bar"> 357 <div class="inner-slide-bar" ref={( el ) => sliderBar = el}> 358 <div class="slider-dot"></div> 359 <div class="slider-dot"></div> 360 <div class="slider-dot"></div> 361 <div class="slider-dot"></div> 362 <div class="slider-dot"></div> 363 <div class="slider-text" onMouseDown={() => lastClickedButton = 0}>Program Settings</div> 364 <div class="slider-dot"></div> 365 <div class="slider-dot"></div> 366 <div class="slider-text" onMouseDown={() => lastClickedButton = 1}>Sync Settings</div> 367 <div class="slider-dot"></div> 368 <div class="slider-dot"></div> 369 <div class="slider-dot"></div> 370 <div class="slider-dot"></div> 371 <div class="slider-dot"></div> 372 </div> 373 </div> */} 374 </div> 375 ) 376} 377 378export default SettingsMenu;