import { onCleanup, onMount, Show } from "solid-js"; import { bytesToFormatted } from "../utils"; import { invoke } from '@tauri-apps/api/core'; import anime from "animejs"; import { ViewState } from "./Managers/ViewManager"; let SettingsMenu = () => { let sliderBar: HTMLElement; let settingsContainer: HTMLElement; let currentButton = 0; let lastClickedButton = -1; let finalPathConfirm: HTMLElement; let finalPathInput: HTMLElement; let finalPathData: string; let finalPathPreviousData: string; let closeWithKey = ( e: KeyboardEvent ) => { if(e.key === 'Escape'){ window.ViewManager.ChangeState(ViewState.PHOTO_LIST); anime({ targets: '.settings', opacity: 0, translateX: '500px', easing: 'easeInOutQuad', duration: 250, complete: () => { anime.set('.settings', { display: 'none' }); } }) } } onMount(async () => { if(await invoke('get_config_value_string', { key: 'transparent' }) === "true"){ invoke('set_config_value_string', { key: 'transparent', value: 'true' }); anime({ targets: document.body, background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); anime({ targets: '.settings', background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); } else{ invoke('set_config_value_string', { key: 'transparent', value: 'false' }); anime({ targets: document.body, background: 'rgba(0, 0, 0, 1)', easing: 'linear', duration: 100 }); anime({ targets: '.settings', background: 'rgba(0, 0, 0, 0)', easing: 'linear', duration: 100 }); } let sliderMouseDown = false; let mouseStartX = 0; let width = window.innerWidth; let buttons = [ 370, 680 ]; let sliderPos = width / 2 - buttons[currentButton]; let sliderScale = width / (buttons[1] - buttons[0]); let render = () => { requestAnimationFrame(render); if(!sliderMouseDown){ sliderPos = sliderPos + (width / 2 - buttons[currentButton] - sliderPos) * 0.25; anime.set(sliderBar, { translateX: sliderPos }); settingsContainer.style.left = (sliderPos - (width / 2 - buttons[0])) * sliderScale + 'px'; } } render(); anime.set(sliderBar, { translateX: sliderPos }); sliderBar.addEventListener('touchstart', ( e: TouchEvent ) => { sliderMouseDown = true; mouseStartX = e.touches[0].clientX; }) window.addEventListener('touchmove', ( e: TouchEvent ) => { if(sliderMouseDown){ anime.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.touches[0].clientX) }); settingsContainer.style.left = (sliderPos - (mouseStartX - e.touches[0].clientX) - (width / 2 - buttons[0])) * sliderScale + 'px'; } }) window.addEventListener('keyup', closeWithKey); window.addEventListener('touchend', ( e: TouchEvent ) => { if(sliderMouseDown){ sliderPos = sliderPos - (mouseStartX - e.touches[0].clientX); anime.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.touches[0].clientX) }); sliderMouseDown = false; if(Math.abs(mouseStartX - e.touches[0].clientX) > 50){ let shortestDistance = 0; let selectedButton = -1; buttons.forEach(( pos, indx ) => { let dis = Math.abs(sliderPos - (width / 2 - pos)); if(selectedButton === -1){ shortestDistance = dis; selectedButton = indx; } else if(shortestDistance > dis){ shortestDistance = dis; selectedButton = indx; } }) currentButton = selectedButton; } else if(lastClickedButton != -1){ currentButton = lastClickedButton; lastClickedButton = -1 } } }) sliderBar.addEventListener('mousedown', ( e: MouseEvent ) => { sliderMouseDown = true; mouseStartX = e.clientX; }); window.addEventListener('mousemove', ( e: MouseEvent ) => { if(sliderMouseDown){ anime.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.clientX) }); settingsContainer.style.left = sliderPos - (mouseStartX - e.clientX) + 'px'; settingsContainer.style.left = (sliderPos - (mouseStartX - e.clientX) - (width / 2 - buttons[0])) * sliderScale + 'px'; } }) window.addEventListener('mouseup', ( e: MouseEvent ) => { if(sliderMouseDown){ sliderPos = sliderPos - (mouseStartX - e.clientX); anime.set(sliderBar, { translateX: sliderPos - (mouseStartX - e.clientX) }); sliderMouseDown = false; if(Math.abs(mouseStartX - e.clientX) > 50){ let shortestDistance = 0; let selectedButton = -1; buttons.forEach(( pos, indx ) => { let dis = Math.abs(sliderPos - (width / 2 - pos)); if(selectedButton === -1){ shortestDistance = dis; selectedButton = indx; } else if(shortestDistance > dis){ shortestDistance = dis; selectedButton = indx; } }) currentButton = selectedButton; } else if(lastClickedButton != -1){ currentButton = lastClickedButton; lastClickedButton = -1 } } }) window.addEventListener('resize', () => { width = window.innerWidth; sliderPos = width / 2 - buttons[currentButton]; sliderScale = width / (buttons[1] - buttons[0]); anime.set(sliderBar, { translateX: sliderPos }); }) sliderBar.addEventListener('wheel', ( e: WheelEvent ) => { if(e.deltaY > 0){ if(buttons[currentButton + 1]) currentButton++; } else{ if(buttons[currentButton - 1]) currentButton--; } }) }) onCleanup(() => { window.removeEventListener('keyup', closeWithKey); }) return (
settingsContainer = el}>

Storage Settings

{ window.PhotoManager.PhotoCount() } Photos ({ bytesToFormatted(window.PhotoManager.PhotoSize(), 0) })

{ el.checked = await invoke('get_config_value_string', { key: 'start-in-bg' }) === "true" ? true : false; }} onChange={( el ) => { if(el.target.checked){ invoke('set_config_value_string', { key: 'start-in-bg', value: 'true' }); } else{ invoke('set_config_value_string', { key: 'start-in-bg', value: 'false' }); } }} /> Start in background
{ el.checked = await invoke('get_config_value_string', { key: 'start-with-win' }) === "true" ? true : false; }} onChange={( el ) => { if(el.target.checked){ invoke('set_config_value_string', { key: 'start-with-win', value: 'true' }); invoke("start_with_win", { start: true }); } else{ invoke('set_config_value_string', { key: 'start-with-win', value: 'false' }); invoke("start_with_win", { start: false }); } }} /> Start with windows
{ el.checked = await invoke('get_config_value_string', { key: 'transparent' }) === "true" ? true : false; }} onChange={( el ) => { if(el.target.checked){ invoke('set_config_value_string', { key: 'transparent', value: 'true' }); anime({ targets: document.body, background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); anime({ targets: '.settings', background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); } else{ invoke('set_config_value_string', { key: 'transparent', value: 'false' }); anime({ targets: document.body, background: 'rgba(0, 0, 0, 1)', easing: 'linear', duration: 100 }); anime({ targets: '.settings', background: 'rgba(0, 0, 0, 0)', easing: 'linear', duration: 100 }); } }} /> Window Transparency

VRChat Photo Path: invoke('get_user_photos_path').then(( path: any ) => { el.innerHTML = ''; el.appendChild( finalPathInput = el} onInput={( el ) => { finalPathConfirm.style.display = 'inline-block'; finalPathData = el.target.innerHTML; }} contenteditable>{path} as Node); finalPathPreviousData = path; }) }> Loading... finalPathConfirm = el}> { finalPathPreviousData = finalPathData; finalPathConfirm.style.display = 'none'; await invoke('change_final_path', { newPath: finalPathData }); window.location.reload(); anime({ targets: '.settings', opacity: 0, translateX: '500px', easing: 'easeInOutQuad', duration: 250, complete: () => { anime.set('.settings', { display: 'none' }); } }) window.location.reload(); }}> Save { finalPathData = finalPathPreviousData; finalPathInput.innerHTML = finalPathPreviousData; finalPathConfirm.style.display = 'none'; }}> Cancel

VRCPM Version: invoke('get_version').then((ver: any) => el.innerHTML = ver)}>Loading...


To change the directory VRChat outputs photos to, you can change the "picture_output_folder" key in the invoke('open_url', { url: 'https://docs.vrchat.com/docs/configuration-file#camera-and-screenshot-settings' })}>config.json file
Alternitavely, you can use VRCX to edit the config file.


VRChat Photo Manager supports photos with extra metadata provided by VRCX.

WIP

sliderBar = el}>
lastClickedButton = 0}>Program Settings
lastClickedButton = 1}>Sync Settings
) } export default SettingsMenu;