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;