A photo manager for VRChat.

Compare changes

Choose any two refs to compare.

+1 -1
build-release.sh
··· 1 1 #!/bin/bash 2 2 3 - VERSION=0.2.6-hot1 3 + VERSION=0.2.7-hot1 4 4 5 5 # Linux builds 6 6 NO_STRIP=true pnpm tauri build
+10 -1
changelog
··· 121 121 122 122 Hotfix 1: 123 123 - Fixed loading when an image file is corrupted 124 - - Fixed update prompt when not connected to internet 124 + - Fixed update prompt when not connected to internet 125 + 126 + v0.2.7: 127 + - Fixed image resizing when window is thinner than image 128 + - Fixed closing settings with keybinds 129 + - Fixed the behaviour of changing the photo path 130 + - Fixed loading photos in folders that aren't VRChat folders 131 + 132 + Hotfix 1: 133 + - Fixed resizing images (again)
+25 -2
src/Components/App.tsx
··· 1 - import { onMount } from "solid-js"; 1 + import { createSignal, onMount } from "solid-js"; 2 2 3 3 import PhotoList from "./PhotoList"; 4 4 import PhotoViewer from "./PhotoViewer"; 5 5 import SettingsMenu from "./SettingsMenu"; 6 - import { utils } from "animejs"; 6 + import { animate, utils } from "animejs"; 7 + import { listen } from "@tauri-apps/api/event"; 7 8 8 9 let App = () => { 10 + let [ errorText, setErrorText ] = createSignal(''); 11 + 9 12 onMount(() => { 10 13 utils.set('.settings', 11 14 { ··· 13 16 opacity: 0, 14 17 translateX: '500px' 15 18 }) 19 + 20 + listen<string>('vrcpm-error', ( ev ) => { 21 + setErrorText(ev.payload); 22 + 23 + utils.set('.error-notif', { translateX: '-50%', translateY: '-100px' }); 24 + animate('.error-notif', { 25 + ease: 'outElastic', 26 + opacity: 1, 27 + translateY: '0px' 28 + }); 29 + 30 + setTimeout(() => { 31 + animate('.error-notif', { 32 + ease: 'outElastic', 33 + opacity: 0, 34 + translateY: '-100px' 35 + }); 36 + }, 2000); 37 + }); 16 38 }) 17 39 18 40 return ( ··· 23 45 <SettingsMenu /> 24 46 25 47 <div class="copy-notif">Image Copied!</div> 48 + <div class="error-notif">{ errorText() }</div> 26 49 </div> 27 50 ); 28 51 }
+35 -1
src/Components/PhotoViewer.tsx
··· 150 150 }) 151 151 } 152 152 153 + let resizeImage = () => { 154 + let dstWidth; 155 + let dstHeight; 156 + 157 + let imgHeight = imageViewer.naturalHeight; 158 + let imgWidth = imageViewer.naturalWidth; 159 + 160 + if( 161 + imgWidth / window.innerWidth < 162 + imgHeight / window.innerHeight 163 + ) { 164 + dstWidth = imgWidth * (window.innerHeight / imgHeight); 165 + dstHeight = window.innerHeight; 166 + } else{ 167 + dstWidth = window.innerWidth; 168 + dstHeight = imgHeight * (window.innerWidth / imgWidth); 169 + } 170 + 171 + imageViewer.style.width = dstWidth + 'px'; 172 + imageViewer.style.height = dstHeight + 'px'; 173 + } 174 + 153 175 onMount(() => { 154 176 utils.set(photoControls, { translateX: '-50%' }); 155 177 utils.set(photoTrayCloseBtn, { translateX: '-50%', opacity: 0, scale: '0.75', bottom: '10px' }); 156 178 utils.set(photoLayerManager, { translateY: '20px', opacity: 0, display: 'none' }); 157 179 158 180 window.addEventListener('keyup', switchPhotoWithKey); 181 + window.addEventListener('resize', () => resizeImage()); 159 182 160 183 let contextMenuOpen = false; 161 184 window.CloseAllPopups.push(() => { ··· 236 259 if(photo){ 237 260 imageViewer.src = (window.OS === "windows" ? "http://photo.localhost/" : 'photo://localhost/') + window.PhotoViewerManager.CurrentPhoto()?.path.split('\\').join('/') + "?full"; 238 261 imageViewer.crossOrigin = 'anonymous'; 262 + 263 + imageViewer.onload = () => { resizeImage(); } 239 264 240 265 animate(imageViewer, { 241 266 opacity: 1, ··· 465 490 <img draggable="false" src="/icon/x-solid.svg"></img> 466 491 </div> 467 492 </div> 468 - <img class="image-container" ref={( el ) => imageViewer = el} /> 493 + 494 + <div style={{ 495 + width: '100%', 496 + height: '100%', 497 + display: 'flex', 498 + "justify-content": 'center', 499 + 'align-items': 'center' 500 + }}> 501 + <img class="image-container" ref={( el ) => imageViewer = el} /> 502 + </div> 469 503 470 504 <div class="prev-button" onClick={() => { 471 505 window.CloseAllPopups.forEach(p => p());
+24 -18
src/Components/SettingsMenu.tsx
··· 6 6 7 7 let SettingsMenu = () => { 8 8 // let sliderBar: HTMLElement; 9 - let settingsContainer: HTMLElement; 9 + // let settingsContainer: HTMLElement; 10 10 // let currentButton = 0; 11 11 // let lastClickedButton = -1; 12 12 let finalPathConfirm: HTMLElement; ··· 17 17 let closeWithKey = ( e: KeyboardEvent ) => { 18 18 if(e.key === 'Escape'){ 19 19 window.ViewManager.ChangeState(ViewState.PHOTO_LIST); 20 - animate('.settings', { 20 + console.log('h'); 21 + animate('.settings',{ 21 22 opacity: 0, 22 23 translateX: '500px', 23 24 easing: 'easeInOutQuad', 24 25 duration: 250, 25 26 onComplete: () => { 27 + console.log('h'); 26 28 utils.set('.settings', { display: 'none' }); 27 29 } 28 30 }) ··· 77 79 // } 78 80 // }) 79 81 80 - // window.addEventListener('keyup', closeWithKey); 82 + window.addEventListener('keyup', closeWithKey); 81 83 82 84 // window.addEventListener('touchend', ( e: TouchEvent ) => { 83 85 // if(sliderMouseDown){ ··· 194 196 }}> 195 197 <div class="icon"><img draggable="false" src="/icon/x-solid.svg"></img></div> 196 198 </div> 197 - <div class="settings-container" ref={( el ) => settingsContainer = el}> 199 + {/* <div class="settings-container" ref={( el ) => settingsContainer = el}> */} 200 + <div class="settings-container"> 198 201 <div class="settings-block"> 199 202 <h1>Storage Settings</h1> 200 203 <p>{ window.PhotoManager.PhotoCount() } Photos ({ bytesToFormatted(window.PhotoManager.PhotoSize(), 0) })</p> ··· 311 314 </span> 312 315 <span style={{ display: 'none' }} ref={( el ) => finalPathConfirm = el}> 313 316 <span class="path" style={{ color: 'green' }} onClick={async () => { 314 - finalPathPreviousData = finalPathData; 315 - finalPathConfirm.style.display = 'none'; 317 + let changed = await invoke('change_final_path', { newPath: finalPathData }); 316 318 317 - await invoke('change_final_path', { newPath: finalPathData }); 318 - window.location.reload(); 319 + if(changed){ 320 + finalPathPreviousData = finalPathData; 321 + finalPathConfirm.style.display = 'none'; 319 322 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 - }) 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 + }) 329 334 330 - window.location.reload(); 335 + window.location.reload(); 336 + } 331 337 }}> 332 338 Save 333 339 </span>
+2 -1
src/css/viewer.css
··· 59 59 } 60 60 61 61 .image-container{ 62 - height: 100%; 62 + max-width: none; 63 + max-height: none; 63 64 background-size: contain !important; 64 65 background-repeat: no-repeat !important; 65 66 background-position: center !important;
+17
src/styles.css
··· 100 100 img{ 101 101 max-width: 100%; 102 102 max-height: 100%; 103 + } 104 + 105 + .error-notif{ 106 + position: fixed; 107 + top: 40px; 108 + left: 50%; 109 + color: white; 110 + transform: translateX(-50%) translateY(-100px); 111 + background: rgba(43, 43, 43, 0.76); 112 + padding: 10px 40px; 113 + backdrop-filter: blur(10px); 114 + -webkit-backdrop-filter: blur(10px); 115 + border-radius: 50px; 116 + box-shadow: #000 0 0 10px; 117 + z-index: 12; 118 + opacity: 0; 119 + pointer-events: none; 103 120 }
+1 -1
src-tauri/Cargo.lock
··· 4 4 5 5 [[package]] 6 6 name = "VRChatPhotoManager" 7 - version = "0.2.6-hot1" 7 + version = "0.2.7" 8 8 dependencies = [ 9 9 "arboard", 10 10 "dirs",
+1 -1
src-tauri/Cargo.toml
··· 1 1 [package] 2 2 name = "VRChatPhotoManager" 3 - version = "0.2.6-hot1" 3 + version = "0.2.7-hot1" 4 4 description = "VRChat Photo Manager" 5 5 authors = ["_phaz"] 6 6 edition = "2021"
+50 -2
src-tauri/gen/schemas/windows-schema.json
··· 519 519 "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" 520 520 }, 521 521 { 522 - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`", 522 + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", 523 523 "type": "string", 524 524 "const": "core:app:default", 525 - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`" 525 + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" 526 526 }, 527 527 { 528 528 "description": "Enables the app_hide command without any pre-configured scope.", ··· 567 567 "markdownDescription": "Enables the name command without any pre-configured scope." 568 568 }, 569 569 { 570 + "description": "Enables the register_listener command without any pre-configured scope.", 571 + "type": "string", 572 + "const": "core:app:allow-register-listener", 573 + "markdownDescription": "Enables the register_listener command without any pre-configured scope." 574 + }, 575 + { 570 576 "description": "Enables the remove_data_store command without any pre-configured scope.", 571 577 "type": "string", 572 578 "const": "core:app:allow-remove-data-store", 573 579 "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." 574 580 }, 575 581 { 582 + "description": "Enables the remove_listener command without any pre-configured scope.", 583 + "type": "string", 584 + "const": "core:app:allow-remove-listener", 585 + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." 586 + }, 587 + { 576 588 "description": "Enables the set_app_theme command without any pre-configured scope.", 577 589 "type": "string", 578 590 "const": "core:app:allow-set-app-theme", ··· 639 651 "markdownDescription": "Denies the name command without any pre-configured scope." 640 652 }, 641 653 { 654 + "description": "Denies the register_listener command without any pre-configured scope.", 655 + "type": "string", 656 + "const": "core:app:deny-register-listener", 657 + "markdownDescription": "Denies the register_listener command without any pre-configured scope." 658 + }, 659 + { 642 660 "description": "Denies the remove_data_store command without any pre-configured scope.", 643 661 "type": "string", 644 662 "const": "core:app:deny-remove-data-store", 645 663 "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." 664 + }, 665 + { 666 + "description": "Denies the remove_listener command without any pre-configured scope.", 667 + "type": "string", 668 + "const": "core:app:deny-remove-listener", 669 + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." 646 670 }, 647 671 { 648 672 "description": "Denies the set_app_theme command without any pre-configured scope.", ··· 1827 1851 "markdownDescription": "Enables the set_focus command without any pre-configured scope." 1828 1852 }, 1829 1853 { 1854 + "description": "Enables the set_focusable command without any pre-configured scope.", 1855 + "type": "string", 1856 + "const": "core:window:allow-set-focusable", 1857 + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." 1858 + }, 1859 + { 1830 1860 "description": "Enables the set_fullscreen command without any pre-configured scope.", 1831 1861 "type": "string", 1832 1862 "const": "core:window:allow-set-fullscreen", ··· 1897 1927 "type": "string", 1898 1928 "const": "core:window:allow-set-shadow", 1899 1929 "markdownDescription": "Enables the set_shadow command without any pre-configured scope." 1930 + }, 1931 + { 1932 + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", 1933 + "type": "string", 1934 + "const": "core:window:allow-set-simple-fullscreen", 1935 + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." 1900 1936 }, 1901 1937 { 1902 1938 "description": "Enables the set_size command without any pre-configured scope.", ··· 2271 2307 "markdownDescription": "Denies the set_focus command without any pre-configured scope." 2272 2308 }, 2273 2309 { 2310 + "description": "Denies the set_focusable command without any pre-configured scope.", 2311 + "type": "string", 2312 + "const": "core:window:deny-set-focusable", 2313 + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." 2314 + }, 2315 + { 2274 2316 "description": "Denies the set_fullscreen command without any pre-configured scope.", 2275 2317 "type": "string", 2276 2318 "const": "core:window:deny-set-fullscreen", ··· 2341 2383 "type": "string", 2342 2384 "const": "core:window:deny-set-shadow", 2343 2385 "markdownDescription": "Denies the set_shadow command without any pre-configured scope." 2386 + }, 2387 + { 2388 + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", 2389 + "type": "string", 2390 + "const": "core:window:deny-set-simple-fullscreen", 2391 + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." 2344 2392 }, 2345 2393 { 2346 2394 "description": "Denies the set_size command without any pre-configured scope.",
+17 -9
src-tauri/src/frontend_calls/change_final_path.rs
··· 1 1 use std::fs; 2 2 3 - #[tauri::command] 4 - pub fn change_final_path(new_path: &str) { 5 - let config_path = dirs::config_dir() 6 - .unwrap() 7 - .join("PhazeDev/VRChatPhotoManager/.photos_path"); 3 + use tauri::{Emitter, State, Window}; 8 4 9 - fs::write(&config_path, new_path.as_bytes()).unwrap(); 5 + use crate::util::cache::Cache; 10 6 7 + #[tauri::command] 8 + pub fn change_final_path(new_path: &str, window: Window, cache: State<Cache>) -> bool { 11 9 match fs::metadata(&new_path) { 12 - Ok(_) => {} 10 + Ok(_) => { 11 + let config_path = dirs::config_dir() 12 + .unwrap() 13 + .join("PhazeDev/VRChatPhotoManager/.photos_path"); 14 + 15 + fs::write(&config_path, new_path.as_bytes()).unwrap(); 16 + cache.insert("photo-path".into(), new_path.to_owned()); 17 + 18 + true 19 + } 13 20 Err(_) => { 14 - fs::create_dir(&new_path).unwrap(); 21 + window.emit("vrcpm-error", "Error Changing Path: Path does not exist.").unwrap(); 22 + false 15 23 } 16 - }; 24 + } 17 25 }
+3 -1
src-tauri/src/frontend_calls/load_photos.rs
··· 16 16 let base_dir = cache.get("photo-path".into()).unwrap(); 17 17 18 18 thread::spawn(move || { 19 - 20 19 let mut photos: Vec<path::PathBuf> = Vec::new(); 21 20 let mut size: usize = 0; 22 21 22 + let re = Regex::new(r"^[0-9]{4}-[0-9]{2}$").unwrap(); 23 + 23 24 for folder in fs::read_dir(&base_dir).unwrap() { 24 25 let f = folder.unwrap(); 26 + if !re.is_match(f.file_name().to_str().unwrap()){ continue; } 25 27 26 28 if f.metadata().unwrap().is_dir() { 27 29 for photo in fs::read_dir(f.path()).unwrap() {
+1
src-tauri/src/main.rs
··· 77 77 78 78 println!("Loading App..."); 79 79 let photos_path = util::get_photo_path::get_photo_path(); 80 + println!("Loading photos from: {:#?}", &photos_path); 80 81 81 82 cache.insert("photo-path".into(), photos_path.to_str().unwrap().to_owned()); 82 83
+7 -1
src-tauri/src/util/get_photo_path.rs
··· 7 7 8 8 match fs::read_to_string(config_path) { 9 9 Ok(path) => { 10 - path::PathBuf::from(path) 10 + let p = path::PathBuf::from(path); 11 + 12 + if fs::exists(&p).unwrap(){ 13 + p 14 + } else{ 15 + dirs::picture_dir().unwrap().join("VRChat") 16 + } 11 17 }, 12 18 Err(_) => { 13 19 let p = dirs::picture_dir().unwrap().join("VRChat");