A photo manager for VRChat.

dfkjhdsakljfhdskljh

+5 -1
changelog
··· 47 47 48 48 v0.2.1: 49 49 - Fixed app using GPU while minimised, might use a tiny bit, but should be much better than before 50 - - Fixed a load a bugs todo with 50 + - Fixed a load a bugs todo with (something? apparently i forgot to write this bit...) 51 + 52 + v0.2.2: 53 + - Use more linux friendly directories 54 + - Move away from localstorage and use the .config file
+2 -2
src-tauri/src/frontend_calls/change_final_path.rs
··· 2 2 3 3 #[tauri::command] 4 4 pub fn change_final_path(new_path: &str) { 5 - let config_path = dirs::home_dir() 5 + let config_path = dirs::config_dir() 6 6 .unwrap() 7 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 7 + .join("PhazeDev\\VRChatPhotoManager\\.photos_path"); 8 8 9 9 fs::write(&config_path, new_path.as_bytes()).unwrap(); 10 10
+56
src-tauri/src/frontend_calls/config.rs
··· 1 + use std::{ fs, path::PathBuf }; 2 + 3 + use serde_json::Value; 4 + 5 + pub fn get_config_path() -> PathBuf{ 6 + let path = dirs::config_dir() 7 + .unwrap() 8 + .join("PhazeDev\\VRChatPhotoManager\\.config"); 9 + 10 + match fs::metadata(&path){ 11 + Ok(_) => {} 12 + Err(_) => { 13 + fs::write(&path, b"{}").unwrap(); 14 + } 15 + } 16 + 17 + path 18 + } 19 + 20 + #[tauri::command] 21 + pub fn set_config_value_string( key: String, value: String ){ 22 + let path = get_config_path(); 23 + 24 + let mut config: Value = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); 25 + config[key] = Value::from(value); 26 + 27 + fs::write(path, config.to_string()).unwrap(); 28 + } 29 + 30 + #[tauri::command] 31 + pub fn get_config_value_string( key: String ) -> Option<String>{ 32 + let config: Value = serde_json::from_str(&fs::read_to_string(get_config_path()).unwrap()).unwrap(); 33 + let string = config[key].as_str(); 34 + 35 + if string.is_some(){ 36 + Some(string.unwrap().to_owned()) 37 + } else{ 38 + None 39 + } 40 + } 41 + 42 + #[tauri::command] 43 + pub fn set_config_value_int( key: String, value: i64 ){ 44 + let path = get_config_path(); 45 + 46 + let mut config: Value = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); 47 + config[key] = Value::from(value); 48 + 49 + fs::write(path, config.to_string()).unwrap(); 50 + } 51 + 52 + #[tauri::command] 53 + pub fn get_config_value_int( key: String ) -> Option<i64>{ 54 + let config: Value = serde_json::from_str(&fs::read_to_string(get_config_path()).unwrap()).unwrap(); 55 + config[key].as_i64() 56 + }
+2 -1
src-tauri/src/frontend_calls/mod.rs
··· 10 10 pub mod load_photo_meta; 11 11 pub mod change_final_path; 12 12 pub mod delete_photo; 13 - pub mod relaunch; 13 + pub mod relaunch; 14 + pub mod config;
+2 -2
src-tauri/src/frontend_calls/relaunch.rs
··· 2 2 3 3 #[tauri::command] 4 4 pub fn relaunch() { 5 - let container_folder = dirs::home_dir() 5 + let container_folder = dirs::config_dir() 6 6 .unwrap() 7 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 7 + .join("PhazeDev\\VRChatPhotoManager"); 8 8 9 9 let mut cmd = Command::new(&container_folder.join("./vrchat-photo-manager.exe")); 10 10 cmd.current_dir(container_folder);
+20 -16
src-tauri/src/frontend_calls/start_with_win.rs
··· 5 5 // create and delete the shortcut from the startup folder 6 6 #[tauri::command] 7 7 pub fn start_with_win(start: bool) { 8 - thread::spawn(move || { 9 - if start { 10 - let target = dirs::home_dir() 11 - .unwrap() 12 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\vrchat-photo-manager.exe"); 8 + if cfg!(windows) { 9 + thread::spawn(move || { 10 + if start { 11 + let target = dirs::config_dir() 12 + .unwrap() 13 + .join("PhazeDev\\VRChatPhotoManager\\vrchat-photo-manager.exe"); 13 14 14 - match fs::metadata(&target) { 15 - Ok(_) => { 16 - let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 15 + match fs::metadata(&target) { 16 + Ok(_) => { 17 + let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 17 18 18 - let sl = ShellLink::new(target).unwrap(); 19 - sl.create_lnk(lnk).unwrap(); 19 + let sl = ShellLink::new(target).unwrap(); 20 + sl.create_lnk(lnk).unwrap(); 21 + } 22 + Err(_) => {} 20 23 } 21 - Err(_) => {} 24 + } else { 25 + let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 26 + fs::remove_file(lnk).unwrap(); 22 27 } 23 - } else { 24 - let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 25 - fs::remove_file(lnk).unwrap(); 26 - } 27 - }); 28 + }); 29 + } else { 30 + panic!("Cannot start with windows... on not windows..."); 31 + } 28 32 }
+9 -5
src-tauri/src/main.rs
··· 21 21 tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 22 22 23 23 // Double check the app has an install directory 24 - let container_folder = dirs::home_dir() 24 + let container_folder = dirs::config_dir() 25 25 .unwrap() 26 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 26 + .join("PhazeDev\\VRChatPhotoManager"); 27 27 28 28 match fs::metadata(&container_folder) { 29 29 Ok(meta) => { ··· 36 36 } 37 37 } 38 38 39 - let sync_lock_path = dirs::home_dir() 39 + let sync_lock_path = dirs::config_dir() 40 40 .unwrap() 41 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.sync_lock"); 41 + .join("PhazeDev\\VRChatPhotoManager\\.sync_lock"); 42 42 43 43 match fs::metadata(&sync_lock_path) { 44 44 Ok(_) => { ··· 159 159 change_final_path::change_final_path, 160 160 sync_photos::sync_photos, 161 161 util::get_version::get_version, 162 - relaunch::relaunch 162 + relaunch::relaunch, 163 + config::set_config_value_string, 164 + config::get_config_value_string, 165 + config::set_config_value_int, 166 + config::get_config_value_int 163 167 ]) 164 168 .run(tauri::generate_context!()) 165 169 .expect("error while running tauri application");
+2 -2
src-tauri/src/photosync.rs
··· 12 12 } 13 13 14 14 pub fn sync_photos(token: String, path: path::PathBuf, window: tauri::Window) { 15 - let sync_lock_path = dirs::home_dir() 15 + let sync_lock_path = dirs::config_dir() 16 16 .unwrap() 17 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.sync_lock"); 17 + .join("PhazeDev\\VRChatPhotoManager\\.sync_lock"); 18 18 19 19 match fs::metadata(&sync_lock_path) { 20 20 Ok(_) => {
+2 -2
src-tauri/src/util/get_photo_path.rs
··· 1 1 use std::{ fs, path }; 2 2 3 3 pub fn get_photo_path() -> path::PathBuf { 4 - let config_path = dirs::home_dir() 4 + let config_path = dirs::config_dir() 5 5 .unwrap() 6 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 6 + .join("PhazeDev\\VRChatPhotoManager\\.photos_path"); 7 7 8 8 match fs::read_to_string(config_path) { 9 9 Ok(path) => {
+30 -26
src/Components/App.tsx
··· 12 12 // TODO: Clean up frontend files, split up into smaller files PLEASE 13 13 14 14 function App() { 15 - if(!localStorage.getItem('start-in-bg')){ 16 - invoke('close_splashscreen') 17 - } 15 + invoke('get_config_value_string', { key: 'start-in-bg' }) 16 + .then(str => { 17 + if(str === "false")invoke('close_splashscreen') 18 + }) 18 19 19 20 let [ loggedIn, setLoggedIn ] = createSignal({ loggedIn: false, username: '', avatar: '', id: '', serverVersion: '0.0' }); 20 21 let [ storageInfo, setStorageInfo ] = createSignal({ storage: 0, used: 0, sync: false }); ··· 37 38 confirmationBoxCallback = cb; 38 39 } 39 40 40 - if(localStorage.getItem('token')){ 41 - fetch('https://photos.phazed.xyz/api/v1/account?token='+localStorage.getItem('token')) 42 - .then(data => data.json()) 43 - .then(data => { 44 - if(!data.ok){ 45 - return console.error(data); 46 - } 47 - 48 - console.log(data.data); 49 - setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); 50 - setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); 51 - 52 - if(!isPhotosSyncing() && data.user.settings.enableSync){ 53 - setIsPhotosSyncing(true); 54 - invoke('sync_photos', { token: localStorage.getItem('token') }); 55 - } 56 - }) 57 - .catch(e => { 58 - console.error(e); 59 - }) 60 - } 41 + invoke('get_config_value_string', { key: 'token' }) 42 + .then(token => { 43 + if(token){ 44 + fetch('https://photos.phazed.xyz/api/v1/account?token='+token) 45 + .then(data => data.json()) 46 + .then(data => { 47 + if(!data.ok){ 48 + return console.error(data); 49 + } 50 + 51 + console.log(data.data); 52 + setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); 53 + setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); 54 + 55 + if(!isPhotosSyncing() && data.user.settings.enableSync){ 56 + setIsPhotosSyncing(true); 57 + invoke('sync_photos', { token: token }); 58 + } 59 + }) 60 + .catch(e => { 61 + console.error(e); 62 + }) 63 + } 64 + }) 61 65 62 66 setTimeout(() => { 63 67 setLoadingType('none'); ··· 124 128 } 125 129 126 130 console.log(data); 127 - localStorage.setItem('token', token); 131 + invoke('set_config_value_string', { key: 'token', value: token }); 128 132 129 133 setLoadingType('none'); 130 134 setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); ··· 132 136 133 137 if(!isPhotosSyncing() && data.user.settings.enableSync){ 134 138 setIsPhotosSyncing(true); 135 - invoke('sync_photos', { token: localStorage.getItem('token') }); 139 + invoke('sync_photos', { token: token }); 136 140 } 137 141 }) 138 142 .catch(e => {
+6 -6
src/Components/NavBar.tsx
··· 147 147 </div> 148 148 149 149 <div class="dropdown" ref={( el ) => dropdown = el}> 150 - <div class="dropdown-button" onClick={() => { 150 + <div class="dropdown-button" onClick={async () => { 151 151 anime.set('.settings', { display: 'block' }); 152 152 anime({ 153 153 targets: '.settings', ··· 157 157 duration: 250 158 158 }) 159 159 160 - fetch('https://photos.phazed.xyz/api/v1/account?token='+localStorage.getItem('token')!) 160 + fetch('https://photos.phazed.xyz/api/v1/account?token='+ (await invoke('get_config_value_string', { key: 'token' }))!) 161 161 .then(data => data.json()) 162 162 .then(data => { 163 163 if(!data.ok){ ··· 176 176 }}>Settings</div> 177 177 178 178 <Show when={props.loggedIn().loggedIn == false} fallback={ 179 - <div class="dropdown-button" onClick={() => { 180 - fetch('https://photos.phazed.xyz/api/v1/deauth?token='+localStorage.getItem('token')!) 179 + <div class="dropdown-button" onClick={async () => { 180 + fetch('https://photos.phazed.xyz/api/v1/deauth?token='+(await invoke('get_config_value_string', { key: 'token' }))!) 181 181 .then(data => data.json()) 182 182 .then(data => { 183 183 console.log(data); 184 184 185 - localStorage.removeItem('token'); 185 + invoke('set_config_value_string', { key: 'token', value: '' }); 186 186 window.location.reload(); 187 187 188 188 setDropdownVisibility(false); ··· 190 190 .catch(e => { 191 191 console.error(e); 192 192 193 - localStorage.removeItem('token'); 193 + invoke('set_config_value_string', { key: 'token', value: '' }); 194 194 window.location.reload(); 195 195 196 196 setDropdownVisibility(false);
+2 -2
src/Components/PhotoList.tsx
··· 423 423 } 424 424 }) 425 425 426 - listen('photo_create', ( event: any ) => { 426 + listen('photo_create', async ( event: any ) => { 427 427 let photo = new Photo(event.payload); 428 428 429 429 photos.splice(0, 0, photo); ··· 431 431 432 432 if(!props.isPhotosSyncing() && props.storageInfo().sync){ 433 433 props.setIsPhotosSyncing(true); 434 - invoke('sync_photos', { token: localStorage.getItem('token') }); 434 + invoke('sync_photos', { token: (await invoke('get_config_value_string', { key: 'token' })) }); 435 435 } 436 436 }) 437 437
+4 -4
src/Components/PhotoViewer.tsx
··· 31 31 } 32 32 } 33 33 34 - let worldCache: WorldCache[] = JSON.parse(localStorage.getItem('worldCache') || "[]"); 34 + let worldCache: WorldCache[] = JSON.parse(await invoke('get_config_value_string', { key: 'worldcache' }) || "[]"); 35 35 36 36 let PhotoViewer = ( props: PhotoViewerProps ) => { 37 37 let viewer: HTMLElement; ··· 430 430 } 431 431 432 432 worldCache.push(worldData); 433 - localStorage.setItem("worldCache", JSON.stringify(worldCache)); 433 + invoke('set_config_value_string', { key: 'worldcache', value: worldCache }); 434 434 435 435 loadWorldData(worldData); 436 436 }) ··· 534 534 <div class="viewer-button" 535 535 onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })} 536 536 onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })} 537 - onClick={() => props.setConfirmationBox("Are you sure you want to delete this photo?", () => { invoke("delete_photo", { 537 + onClick={() => props.setConfirmationBox("Are you sure you want to delete this photo?", async () => { invoke("delete_photo", { 538 538 path: props.currentPhotoView().path, 539 - token: localStorage.getItem("token") || "none", 539 + token: (await invoke('get_config_value_string', { key: 'token' })) || "none", 540 540 isSyncing: props.loggedIn().loggedIn ? props.storageInfo().sync : false 541 541 }); 542 542 })}>
+20 -20
src/Components/SettingsMenu.tsx
··· 43 43 } 44 44 } 45 45 46 - onMount(() => { 47 - if(localStorage.getItem('transparent')){ 48 - localStorage.setItem('transparent', 'true'); 46 + onMount(async () => { 47 + if(await invoke('get_config_value_string', { key: 'transparent' }) === "true"){ 48 + invoke('set_config_value_string', { key: 'transparent', value: 'true' }); 49 49 50 50 anime({ targets: document.body, background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); 51 51 anime({ targets: '.settings', background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); 52 52 } else{ 53 - localStorage.removeItem('transparent') 53 + invoke('set_config_value_string', { key: 'transparent', value: 'false' }); 54 54 55 55 anime({ targets: document.body, background: 'rgba(0, 0, 0, 1)', easing: 'linear', duration: 100 }); 56 56 anime({ targets: '.settings', background: 'rgba(0, 0, 0, 0)', easing: 'linear', duration: 100 }); ··· 191 191 window.removeEventListener('keyup', closeWithKey); 192 192 }) 193 193 194 - let refreshAccount = () => { 195 - fetch('https://photos.phazed.xyz/api/v1/account?token='+localStorage.getItem('token')!) 194 + let refreshAccount = async () => { 195 + fetch('https://photos.phazed.xyz/api/v1/account?token='+(await invoke('get_config_value_string', { key: 'token' }))!) 196 196 .then(data => data.json()) 197 197 .then(data => { 198 198 if(!data.ok){ ··· 217 217 <p>{ props.photoCount() } Photos ({ bytesToFormatted(props.photoSize(), 0) })</p> 218 218 219 219 <div class="selector"> 220 - <input type="checkbox" id="start-in-bg-check" ref={( el ) => { 221 - el.checked = localStorage.getItem('start-in-bg') ? true : false; 220 + <input type="checkbox" id="start-in-bg-check" ref={async ( el ) => { 221 + el.checked = await invoke('get_config_value_string', { key: 'start-in-bg' }) === "true" ? true : false; 222 222 }} onChange={( el ) => { 223 223 if(el.target.checked){ 224 - localStorage.setItem('start-in-bg', 'true'); 224 + invoke('set_config_value_string', { key: 'start-in-bg', value: 'true' }); 225 225 } else{ 226 - localStorage.removeItem('start-in-bg') 226 + invoke('set_config_value_string', { key: 'start-in-bg', value: 'false' }); 227 227 } 228 228 }} /> 229 229 Start in background ··· 238 238 </div> 239 239 240 240 <div class="selector"> 241 - <input type="checkbox" id="start-with-win-check" ref={( el ) => { 242 - el.checked = localStorage.getItem('start-with-win') ? true : false; 241 + <input type="checkbox" id="start-with-win-check" ref={async ( el ) => { 242 + el.checked = await invoke('get_config_value_string', { key: 'start-with-win' }) === "true" ? true : false; 243 243 }} onChange={( el ) => { 244 244 if(el.target.checked){ 245 - localStorage.setItem('start-with-win', 'true'); 245 + invoke('set_config_value_string', { key: 'start-with-win', value: 'true' }); 246 246 invoke("start_with_win", { start: true }); 247 247 } else{ 248 - localStorage.removeItem('start-with-win') 248 + invoke('set_config_value_string', { key: 'start-with-win', value: 'false' }); 249 249 invoke("start_with_win", { start: false }); 250 250 } 251 251 }} /> ··· 261 261 </div> 262 262 263 263 <div class="selector"> 264 - <input type="checkbox" id="transparent-check" ref={( el ) => { 265 - el.checked = localStorage.getItem('transparent') ? true : false; 264 + <input type="checkbox" id="transparent-check" ref={async ( el ) => { 265 + el.checked = await invoke('get_config_value_string', { key: 'transparent' }) === "true" ? true : false; 266 266 }} onChange={( el ) => { 267 267 if(el.target.checked){ 268 - localStorage.setItem('transparent', 'true'); 268 + invoke('set_config_value_string', { key: 'transparent', value: 'true' }); 269 269 270 270 anime({ targets: document.body, background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); 271 271 anime({ targets: '.settings', background: 'rgba(0, 0, 0, 0.5)', easing: 'linear', duration: 100 }); 272 272 } else{ 273 - localStorage.removeItem('transparent') 273 + invoke('set_config_value_string', { key: 'transparent', value: 'false' }); 274 274 275 275 anime({ targets: document.body, background: 'rgba(0, 0, 0, 1)', easing: 'linear', duration: 100 }); 276 276 anime({ targets: '.settings', background: 'rgba(0, 0, 0, 0)', easing: 'linear', duration: 100 }); ··· 390 390 391 391 <div class="account-notice" style={{ display: 'flex' }}> 392 392 <Show when={!deletingPhotos()} fallback={ "We are deleting your photos, please leave this window open while we delete them." }> 393 - <div class="button-danger" onClick={() => props.setConfirmationBox("You are about to delete all your photos from the cloud, and disable syncing. This will NOT delete any local files.", () => { 393 + <div class="button-danger" onClick={() => props.setConfirmationBox("You are about to delete all your photos from the cloud, and disable syncing. This will NOT delete any local files.", async () => { 394 394 props.setStorageInfo({ used: 0, storage: 0, sync: false }); 395 395 setDeletingPhotos(true); 396 396 397 397 fetch('https://photos-cdn.phazed.xyz/api/v1/allphotos', { 398 398 method: 'DELETE', 399 - headers: { auth: localStorage.getItem("token")! } 399 + headers: { auth: (await invoke('get_config_value_string', { key: 'token' }))! } 400 400 }) 401 401 .then(data => data.json()) 402 402 .then(data => {
-2
src/index.tsx
··· 1 1 /* @refresh reload */ 2 2 import { render } from "solid-js/web"; 3 3 4 - // TODO: Rewrite storage stuff localstorage is dumb. 5 - 6 4 declare global{ 7 5 interface Window { 8 6 CloseAllPopups: (() => void)[]