A photo manager for VRChat.

AAAAAAA

Changed files
+60 -36
src
src-tauri
src
+4 -1
changelog
··· 64 - Fixed world cache not being saved to the config file 65 66 v0.2.3: 67 - - Finally replaced the awful render function in the frontend 68 69 Dev Stuff: 70 - Split frontend up into many smaller files for easier editing
··· 64 - Fixed world cache not being saved to the config file 65 66 v0.2.3: 67 + - Finally replaced the awful render function in the frontend, ( should use less resources when app is open ) 68 + - Fixed photos not being lined up 69 + - Fixed filters not updating photo list 70 + - Fixed adding / removing photos not updating the photo list 71 72 Dev Stuff: 73 - Split frontend up into many smaller files for easier editing
+10 -9
src-tauri/src/main.rs
··· 12 use notify::{EventKind, RecursiveMode, Watcher}; 13 use pngmeta::PNGImage; 14 use regex::Regex; 15 - use util::cache::Cache; 16 use std::{env, fs, thread}; 17 use tauri::{Emitter, Manager, State, WindowEvent}; 18 use tauri_plugin_deep_link::DeepLinkExt; ··· 92 // Listen for file updates, store each update in an mpsc channel and send to the frontend 93 let (sender, receiver) = std::sync::mpsc::channel(); 94 let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | { 95 - // TODO: Fix this, why does it not work?? it does work??? 96 match res { 97 - Ok(event) => { 98 match event.kind{ 99 EventKind::Remove(_) => { 100 let path = event.paths.first().unwrap(); 101 102 let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 103 let re2 = Regex::new(r"(?m)/VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png/gm").unwrap(); 104 105 if 106 - re1.is_match(path.to_str().unwrap()) || 107 - re2.is_match(path.to_str().unwrap()) 108 { 109 - sender.send((2, path.clone().strip_prefix(util::get_photo_path::get_photo_path()).unwrap().to_path_buf())).unwrap(); 110 } 111 }, 112 EventKind::Create(_) => { 113 let path = event.paths.first().unwrap(); 114 115 let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 116 let re2 = Regex::new(r"(?m)/VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png/gm").unwrap(); 117 118 if 119 - re1.is_match(path.to_str().unwrap()) || 120 - re2.is_match(path.to_str().unwrap()) 121 { 122 thread::sleep(time::Duration::from_millis(1000)); 123 - sender.send((1, path.clone().strip_prefix(util::get_photo_path::get_photo_path()).unwrap().to_path_buf())).unwrap(); 124 } 125 }, 126 _ => {}
··· 12 use notify::{EventKind, RecursiveMode, Watcher}; 13 use pngmeta::PNGImage; 14 use regex::Regex; 15 + use util::{cache::Cache, get_photo_path::get_photo_path}; 16 use std::{env, fs, thread}; 17 use tauri::{Emitter, Manager, State, WindowEvent}; 18 use tauri_plugin_deep_link::DeepLinkExt; ··· 92 // Listen for file updates, store each update in an mpsc channel and send to the frontend 93 let (sender, receiver) = std::sync::mpsc::channel(); 94 let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | { 95 match res { 96 + Ok(event) => { 97 match event.kind{ 98 EventKind::Remove(_) => { 99 let path = event.paths.first().unwrap(); 100 + let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 101 102 let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 103 let re2 = Regex::new(r"(?m)/VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png/gm").unwrap(); 104 105 if 106 + re1.is_match(&name) || 107 + re2.is_match(&name) 108 { 109 + sender.send((2, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 110 } 111 }, 112 EventKind::Create(_) => { 113 let path = event.paths.first().unwrap(); 114 + let name = path.file_name().unwrap().to_str().unwrap().to_owned(); 115 116 let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 117 let re2 = Regex::new(r"(?m)/VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png/gm").unwrap(); 118 119 if 120 + re1.is_match(&name) || 121 + re2.is_match(&name) 122 { 123 thread::sleep(time::Duration::from_millis(1000)); 124 + sender.send((1, path.strip_prefix(get_photo_path()).unwrap().to_str().unwrap().to_owned())).unwrap(); 125 } 126 }, 127 _ => {}
+19 -8
src/Components/Managers/PhotoListRenderingManager.tsx
··· 7 8 export class PhotoListRenderingManager{ 9 private _layout: PhotoListRow[] = []; 10 11 constructor(){} 12 13 - public ComputeLayout( canvas: HTMLCanvasElement ){ 14 let lastDateString = null; 15 let row = new PhotoListRow(); 16 row.Height = 100; ··· 36 37 // Check if the current row width plus another photo is too big to fit, push this row to the 38 // layout and add the photo to the next row instead 39 - if(row.Width + photo.scaledWidth! + 10 > canvas.width - 100){ 40 this._layout.push(row); 41 row = new PhotoListRow(); 42 } 43 44 // We should now add this photo to the current row 45 row.Elements.push(new PhotoListPhoto(photo)); 46 - row.Width += photo.scaledWidth!; 47 } 48 49 this._layout.push(row); ··· 81 currentY += row.Height + 10; 82 continue; 83 } 84 85 // Loop through all elements in the row 86 - let rowXPos = 0; 87 for (let j = 0; j < row.Elements.length; j++) { 88 let el = row.Elements[j]; 89 ··· 93 // and then render that text 94 95 // === DEBUG === 96 - ctx.strokeStyle = '#f00'; 97 - ctx.strokeRect(0, currentY - scroll, canvas.width, row.Height); 98 99 ctx.textAlign = 'center'; 100 ctx.textBaseline = 'middle'; ··· 108 let photo = (el as PhotoListPhoto).Photo; 109 110 // === DEBUG === 111 - ctx.strokeStyle = '#f00'; 112 - ctx.strokeRect((rowXPos - row.Width / 2) + canvas.width / 2, currentY - scroll, photo.scaledWidth!, row.Height); 113 114 if(!photo.loaded) 115 // If the photo is not loaded, start a new task and load it in that task
··· 7 8 export class PhotoListRenderingManager{ 9 private _layout: PhotoListRow[] = []; 10 + private _canvas!: HTMLCanvasElement; 11 12 constructor(){} 13 14 + public SetCanvas( canvas: HTMLCanvasElement ){ 15 + this._canvas = canvas; 16 + } 17 + 18 + public ComputeLayout(){ 19 + this._layout = []; 20 + 21 let lastDateString = null; 22 let row = new PhotoListRow(); 23 row.Height = 100; ··· 43 44 // Check if the current row width plus another photo is too big to fit, push this row to the 45 // layout and add the photo to the next row instead 46 + if(row.Width + photo.scaledWidth! + 10 > this._canvas.width - 100){ 47 this._layout.push(row); 48 row = new PhotoListRow(); 49 } 50 51 // We should now add this photo to the current row 52 row.Elements.push(new PhotoListPhoto(photo)); 53 + row.Width += photo.scaledWidth! + 10; 54 } 55 56 this._layout.push(row); ··· 88 currentY += row.Height + 10; 89 continue; 90 } 91 + 92 + // === DEBUG === 93 + // ctx.strokeStyle = '#f00'; 94 + // ctx.strokeRect((canvas.width / 2) - row.Width / 2, currentY - 5 - scroll, row.Width, row.Height + 10); 95 96 // Loop through all elements in the row 97 + let rowXPos = 10; 98 for (let j = 0; j < row.Elements.length; j++) { 99 let el = row.Elements[j]; 100 ··· 104 // and then render that text 105 106 // === DEBUG === 107 + // ctx.strokeStyle = '#f00'; 108 + // ctx.strokeRect(0, currentY - scroll, canvas.width, row.Height); 109 110 ctx.textAlign = 'center'; 111 ctx.textBaseline = 'middle'; ··· 119 let photo = (el as PhotoListPhoto).Photo; 120 121 // === DEBUG === 122 + // ctx.strokeStyle = '#f00'; 123 + // ctx.strokeRect((rowXPos - row.Width / 2) + canvas.width / 2, currentY - scroll, photo.scaledWidth!, row.Height); 124 125 if(!photo.loaded) 126 // If the photo is not loaded, start a new task and load it in that task
+21 -14
src/Components/Managers/PhotoManager.tsx
··· 69 let data: PhotoMetadata = event.payload; 70 71 let photo = this.Photos.find(x => x.path === data.path); 72 - if(!photo)return; 73 74 photo.width = data.width; 75 photo.height = data.height; ··· 84 85 photo.metaLoaded = true; 86 photo.onMetaLoaded(); 87 - 88 - // this.ReloadFilters(); 89 - 90 - console.log(this._amountLoaded, this.Photos.length); 91 - if(this._amountLoaded === this.Photos.length && !this.HasFirstLoaded){ 92 this.FilteredPhotos = this.Photos; 93 this.HasFirstLoaded = true; 94 ··· 100 let photo = new Photo(event.payload); 101 102 this.Photos.splice(0, 0, photo); 103 photo.loadMeta(); 104 105 if(!window.SyncManager.IsSyncing() && window.AccountManager.Storage()?.isSyncing){ ··· 113 114 if(event.payload === window.PhotoViewerManager.CurrentPhoto()?.path) 115 window.PhotoViewerManager.Close() 116 }) 117 } 118 ··· 133 case FilterType.USER: 134 this.Photos.map(p => { 135 if(p.metadata){ 136 - let meta = JSON.parse(p.metadata); 137 - let photo = meta.players.find(( y: any ) => y.displayName.toLowerCase().includes(this._filter) || y.id === this._filter); 138 - 139 - if(photo)this.FilteredPhotos.push(p); 140 } 141 }) 142 break; 143 case FilterType.WORLD: 144 this.Photos.map(p => { 145 if(p.metadata){ 146 - let meta = JSON.parse(p.metadata); 147 - let photo = meta.world.name.toLowerCase().includes(this._filter) || meta.world.id === this._filter; 148 - 149 - if(photo)this.FilteredPhotos.push(p); 150 } 151 }) 152 break; 153 } 154 } 155 156 public Load(){
··· 69 let data: PhotoMetadata = event.payload; 70 71 let photo = this.Photos.find(x => x.path === data.path); 72 + if(!photo)return console.error('Cannot find photo.', data); 73 74 photo.width = data.width; 75 photo.height = data.height; ··· 84 85 photo.metaLoaded = true; 86 photo.onMetaLoaded(); 87 + 88 + if(this._amountLoaded === this.Photos.length - 1 && !this.HasFirstLoaded){ 89 this.FilteredPhotos = this.Photos; 90 this.HasFirstLoaded = true; 91 ··· 97 let photo = new Photo(event.payload); 98 99 this.Photos.splice(0, 0, photo); 100 + 101 + photo.onMetaLoaded = () => this.ReloadFilters(); 102 photo.loadMeta(); 103 104 if(!window.SyncManager.IsSyncing() && window.AccountManager.Storage()?.isSyncing){ ··· 112 113 if(event.payload === window.PhotoViewerManager.CurrentPhoto()?.path) 114 window.PhotoViewerManager.Close() 115 + 116 + this.ReloadFilters(); 117 }) 118 } 119 ··· 134 case FilterType.USER: 135 this.Photos.map(p => { 136 if(p.metadata){ 137 + try{ 138 + let meta = JSON.parse(p.metadata); 139 + let photo = meta.players.find(( y: any ) => y.displayName.toLowerCase().includes(this._filter) || y.id === this._filter); 140 + 141 + if(photo)this.FilteredPhotos.push(p); 142 + } catch(e){} 143 } 144 }) 145 break; 146 case FilterType.WORLD: 147 this.Photos.map(p => { 148 if(p.metadata){ 149 + try{ 150 + let meta = JSON.parse(p.metadata); 151 + let photo = meta.world.name.toLowerCase().includes(this._filter) || meta.world.id === this._filter; 152 + 153 + if(photo)this.FilteredPhotos.push(p); 154 + } catch(e){} 155 } 156 }) 157 break; 158 } 159 + 160 + window.PhotoListRenderingManager.ComputeLayout(); 161 } 162 163 public Load(){
+4 -2
src/Components/PhotoList.tsx
··· 129 easing: 'easeInOutQuad' 130 }) 131 132 - window.PhotoListRenderingManager.ComputeLayout(photoContainer!); 133 render(); 134 }); 135 ··· 163 photoContainerBG.width = window.innerWidth; 164 photoContainerBG.height = window.innerHeight; 165 166 - window.PhotoListRenderingManager.ComputeLayout(photoContainer!); 167 }) 168 169 photoContainer.addEventListener('click', ( e: MouseEvent ) => {
··· 129 easing: 'easeInOutQuad' 130 }) 131 132 + window.PhotoListRenderingManager.SetCanvas(photoContainer!); 133 + window.PhotoListRenderingManager.ComputeLayout(); 134 + 135 render(); 136 }); 137 ··· 165 photoContainerBG.width = window.innerWidth; 166 photoContainerBG.height = window.innerHeight; 167 168 + window.PhotoListRenderingManager.ComputeLayout(); 169 }) 170 171 photoContainer.addEventListener('click', ( e: MouseEvent ) => {
+1 -1
src/Components/Structs/Photo.ts
··· 55 loadImage(){ 56 if(this.loading || this.loaded || imagesLoading >= Vars.MAX_IMAGE_LOAD)return; 57 58 - this.loadMeta(); 59 if(!this.metaLoaded)return; 60 61 this.loading = true;
··· 55 loadImage(){ 56 if(this.loading || this.loaded || imagesLoading >= Vars.MAX_IMAGE_LOAD)return; 57 58 + // this.loadMeta(); 59 if(!this.metaLoaded)return; 60 61 this.loading = true;
+1 -1
src/Components/Structs/PhotoListRow.ts
··· 4 export class PhotoListRow{ 5 public Elements: PhotoListElement[] = []; 6 public Height: number = Vars.PHOTO_HEIGHT; 7 - public Width: number = 0; 8 }
··· 4 export class PhotoListRow{ 5 public Elements: PhotoListElement[] = []; 6 public Height: number = Vars.PHOTO_HEIGHT; 7 + public Width: number = 10; 8 }