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;