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