A photo manager for VRChat.

dsgkjhakljdsfhgdfskjhkh

+6 -1
changelog
··· 22 22 - Added the context menu back to the photo viewer screen 23 23 - Fixed some weird bugs where the world data cache would be ignored 24 24 - Fixed the ui forgetting the user account in some cases where the token stored it still valid 25 + - Updated no photos text to be kinder 25 26 - Settings menu can now be closed with ESC 27 + - Fixed photos being extremely wide under certain conditions 28 + - Fixed some icons not showing correctly 29 + 26 30 - Photo viewer can now be navigated with keybinds: 27 31 - Up Arrow: Open Tray 28 32 - Down Arrow: Close Tray ··· 31 35 - Escape: Close Image 32 36 33 37 Dev Stuff: 34 - - Fixed indentation to be more constistant 38 + - Fixed indentation to be more constistant 39 + - main.rs is no longer like 400 quintillion lines long
+1
public/icon/up-right-from-square-solid.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#ffffff" d="M352 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9L370.7 96 201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L416 141.3l41.4 41.4c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6l0-128c0-17.7-14.3-32-32-32L352 0zM80 32C35.8 32 0 67.8 0 112L0 432c0 44.2 35.8 80 80 80l320 0c44.2 0 80-35.8 80-80l0-112c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 112c0 8.8-7.2 16-16 16L80 448c-8.8 0-16-7.2-16-16l0-320c0-8.8 7.2-16 16-16l112 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L80 32z"/></svg>
+17
src-tauri/src/frontend_calls/change_final_path.rs
··· 1 + use std::fs; 2 + 3 + #[tauri::command] 4 + pub fn change_final_path(new_path: &str) { 5 + let config_path = dirs::home_dir() 6 + .unwrap() 7 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 8 + 9 + fs::write(&config_path, new_path.as_bytes()).unwrap(); 10 + 11 + match fs::metadata(&new_path) { 12 + Ok(_) => {} 13 + Err(_) => { 14 + fs::create_dir(&new_path).unwrap(); 15 + } 16 + }; 17 + }
+6
src-tauri/src/frontend_calls/close_splashscreen.rs
··· 1 + use tauri::Manager; 2 + 3 + #[tauri::command] 4 + pub fn close_splashscreen(window: tauri::Window) { 5 + window.get_webview_window("main").unwrap().show().unwrap(); 6 + }
+25
src-tauri/src/frontend_calls/delete_photo.rs
··· 1 + use std::{ fs, thread, time::Duration }; 2 + use crate::util::get_photo_path::get_photo_path; 3 + 4 + // Delete a photo when the users confirms the prompt in the ui 5 + #[tauri::command] 6 + pub fn delete_photo(path: String, token: String, is_syncing: bool) { 7 + thread::spawn(move || { 8 + let p = get_photo_path().join(&path); 9 + fs::remove_file(p).unwrap(); 10 + 11 + let photo = path.split("\\").last().unwrap(); 12 + 13 + if is_syncing { 14 + let client = reqwest::blocking::Client::new(); 15 + client 16 + .delete(format!( 17 + "https://photos-cdn.phazed.xyz/api/v1/photos?token={}&photo={}", 18 + token, photo 19 + )) 20 + .timeout(Duration::from_secs(120)) 21 + .send() 22 + .unwrap(); 23 + } 24 + }); 25 + }
+12
src-tauri/src/frontend_calls/find_world_by_id.rs
··· 1 + use std::thread; 2 + use crate::worldscraper::World; 3 + use tauri::Emitter; 4 + 5 + // Load vrchat world data 6 + #[tauri::command] 7 + pub fn find_world_by_id(world_id: String, window: tauri::Window) { 8 + thread::spawn(move || { 9 + let world = World::new(world_id); 10 + window.emit("world_data", world).unwrap(); 11 + }); 12 + }
+9
src-tauri/src/frontend_calls/get_user_photos_path.rs
··· 1 + use std::path; 2 + use crate::util::get_photo_path::get_photo_path; 3 + 4 + // Check if the photo config file exists 5 + // if not just return the default vrchat path 6 + #[tauri::command] 7 + pub fn get_user_photos_path() -> path::PathBuf { 8 + get_photo_path() 9 + }
src-tauri/src/frontend_calls/get_version.rs

This is a binary file and will not be displayed.

+31
src-tauri/src/frontend_calls/load_photo_meta.rs
··· 1 + use std::{ thread, fs, io::Read }; 2 + use crate::util::get_photo_path::get_photo_path; 3 + use tauri::Emitter; 4 + use crate::PNGImage; 5 + 6 + // Reads the PNG file and loads the image metadata from it 7 + // then sends the metadata to the frontend, returns width, height, colour depth and so on... more info "pngmeta.rs" 8 + #[tauri::command] 9 + pub fn load_photo_meta(photo: &str, window: tauri::Window) { 10 + let photo = photo.to_string(); 11 + 12 + thread::spawn(move || { 13 + let base_dir = get_photo_path().join(&photo); 14 + 15 + let file = fs::File::open(base_dir.clone()); 16 + 17 + match file { 18 + Ok(mut file) => { 19 + let mut buffer = Vec::new(); 20 + 21 + let _out = file.read_to_end(&mut buffer); 22 + window 23 + .emit("photo_meta_loaded", PNGImage::new(buffer, photo)) 24 + .unwrap(); 25 + } 26 + Err(_) => { 27 + println!("Cannot read image file"); 28 + } 29 + } 30 + }); 31 + }
+65
src-tauri/src/frontend_calls/load_photos.rs
··· 1 + use std::{ thread, fs, path }; 2 + use crate::util::get_photo_path::get_photo_path; 3 + use regex::Regex; 4 + use tauri::Emitter; 5 + 6 + // Scans all files under the "Pictures/VRChat" path 7 + // then sends the list of photos to the frontend 8 + #[derive(Clone, serde::Serialize)] 9 + struct PhotosLoadedResponse { 10 + photos: Vec<path::PathBuf>, 11 + size: usize, 12 + } 13 + 14 + #[tauri::command] 15 + pub fn load_photos(window: tauri::Window) { 16 + thread::spawn(move || { 17 + let base_dir = get_photo_path(); 18 + 19 + let mut photos: Vec<path::PathBuf> = Vec::new(); 20 + let mut size: usize = 0; 21 + 22 + for folder in fs::read_dir(&base_dir).unwrap() { 23 + let f = folder.unwrap(); 24 + 25 + if f.metadata().unwrap().is_dir() { 26 + for photo in fs::read_dir(f.path()).unwrap() { 27 + let p = photo.unwrap(); 28 + 29 + if p.metadata().unwrap().is_file() { 30 + let fname = p.path(); 31 + 32 + 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(); 33 + let re2 = Regex::new( 34 + 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").unwrap(); 35 + 36 + if re1.is_match(p.file_name().to_str().unwrap()) 37 + || re2.is_match(p.file_name().to_str().unwrap()) 38 + { 39 + let path = fname.to_path_buf().clone(); 40 + let metadata = fs::metadata(&path).unwrap(); 41 + 42 + if metadata.is_file() { 43 + size += metadata.len() as usize; 44 + 45 + let path = path.strip_prefix(&base_dir).unwrap().to_path_buf(); 46 + photos.push(path); 47 + } 48 + } else { 49 + println!("Ignoring {:#?} as it doesn't match regex", p.file_name()); 50 + } 51 + } else { 52 + println!("Ignoring {:#?} as it is a directory", p.file_name()); 53 + } 54 + } 55 + } else { 56 + println!("Ignoring {:#?} as it isn't a directory", f.file_name()); 57 + } 58 + } 59 + 60 + println!("Found {} photos", photos.len()); 61 + window 62 + .emit("photos_loaded", PhotosLoadedResponse { photos, size }) 63 + .unwrap(); 64 + }); 65 + }
+13
src-tauri/src/frontend_calls/mod.rs
··· 1 + pub mod close_splashscreen; 2 + pub mod start_user_auth; 3 + pub mod open_url; 4 + pub mod open_folder; 5 + pub mod get_user_photos_path; 6 + pub mod start_with_win; 7 + pub mod find_world_by_id; 8 + pub mod sync_photos; 9 + pub mod load_photos; 10 + pub mod load_photo_meta; 11 + pub mod change_final_path; 12 + pub mod delete_photo; 13 + pub mod relaunch;
+9
src-tauri/src/frontend_calls/open_folder.rs
··· 1 + use std::process::Command; 2 + 3 + #[tauri::command] 4 + pub fn open_folder(url: &str) { 5 + Command::new("explorer.exe") 6 + .arg(format!("/select,{}", url)) 7 + .spawn() 8 + .unwrap(); 9 + }
+6
src-tauri/src/frontend_calls/open_url.rs
··· 1 + #[tauri::command] 2 + pub fn open_url(url: &str) { 3 + if url.starts_with("https://"){ 4 + open::that(url).unwrap(); 5 + } 6 + }
+14
src-tauri/src/frontend_calls/relaunch.rs
··· 1 + use std::process::{ self, Command }; 2 + 3 + #[tauri::command] 4 + pub fn relaunch() { 5 + let container_folder = dirs::home_dir() 6 + .unwrap() 7 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 8 + 9 + let mut cmd = Command::new(&container_folder.join("./vrchat-photo-manager.exe")); 10 + cmd.current_dir(container_folder); 11 + cmd.spawn().expect("Cannot run updater"); 12 + 13 + process::exit(0); 14 + }
+4
src-tauri/src/frontend_calls/start_user_auth.rs
··· 1 + #[tauri::command] 2 + pub fn start_user_auth() { 3 + open::that("https://photos.phazed.xyz/api/v1/auth").unwrap(); 4 + }
+28
src-tauri/src/frontend_calls/start_with_win.rs
··· 1 + use std::{ thread, fs }; 2 + use mslnk::ShellLink; 3 + 4 + // When the user changes the start with windows toggle 5 + // create and delete the shortcut from the startup folder 6 + #[tauri::command] 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"); 13 + 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"); 17 + 18 + let sl = ShellLink::new(target).unwrap(); 19 + sl.create_lnk(lnk).unwrap(); 20 + } 21 + Err(_) => {} 22 + } 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 + }
+11
src-tauri/src/frontend_calls/sync_photos.rs
··· 1 + use crate::photosync; 2 + use std::thread; 3 + use crate::util::get_photo_path::get_photo_path; 4 + 5 + // On requested sync the photos to the cloud 6 + #[tauri::command] 7 + pub fn sync_photos(token: String, window: tauri::Window) { 8 + thread::spawn(move || { 9 + photosync::sync_photos(token, get_photo_path(), window); 10 + }); 11 + }
+29 -477
src-tauri/src/main.rs
··· 3 3 mod photosync; 4 4 mod pngmeta; 5 5 mod worldscraper; 6 + mod frontend_calls; 7 + mod util; 6 8 7 9 use core::time; 8 - use image::{ codecs::png::{ PngDecoder, PngEncoder }, DynamicImage, ImageEncoder }; 9 - use fast_image_resize::{ images::Image, IntoImageView, ResizeOptions, Resizer }; 10 - use mslnk::ShellLink; 10 + use frontend_calls::*; 11 + 11 12 use notify::{EventKind, RecursiveMode, Watcher}; 12 13 use pngmeta::PNGImage; 13 14 use regex::Regex; 14 - use std::{ 15 - env, fs, 16 - io::{ BufReader, Read }, 17 - path, 18 - process::{self, Command}, 19 - thread, 20 - time::Duration, 21 - }; 22 - use tauri::{ 23 - http::Response, menu::{MenuBuilder, MenuItemBuilder}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, AppHandle, Emitter, Manager, WindowEvent 24 - }; 25 - use worldscraper::World; 15 + use std::{ env, fs, thread, }; 16 + use tauri::{ Emitter, Manager, WindowEvent }; 26 17 27 - // TODO: for the love of fuck please seperate this file out into multiple files at some point 28 18 // TODO: Linux support 29 19 30 - // Scans all files under the "Pictures/VRChat" path 31 - // then sends the list of photos to the frontend 32 - #[derive(Clone, serde::Serialize)] 33 - struct PhotosLoadedResponse { 34 - photos: Vec<path::PathBuf>, 35 - size: usize, 36 - } 37 - 38 - const VERSION: &str = env!("CARGO_PKG_VERSION"); 39 - 40 - pub fn get_photo_path() -> path::PathBuf { 41 - let config_path = dirs::home_dir() 42 - .unwrap() 43 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 44 - 45 - match fs::read_to_string(config_path) { 46 - Ok(path) => { 47 - if path 48 - != dirs::picture_dir() 49 - .unwrap() 50 - .join("VRChat") 51 - .to_str() 52 - .unwrap() 53 - .to_owned() 54 - { 55 - path::PathBuf::from(path) 56 - } else { 57 - dirs::picture_dir().unwrap().join("VRChat") 58 - } 59 - } 60 - Err(_) => dirs::picture_dir().unwrap().join("VRChat"), 61 - } 62 - } 63 - 64 - #[tauri::command] 65 - fn close_splashscreen(window: tauri::Window) { 66 - window.get_webview_window("main").unwrap().show().unwrap(); 67 - } 68 - 69 - #[tauri::command] 70 - fn start_user_auth() { 71 - open::that("https://photos.phazed.xyz/api/v1/auth").unwrap(); 72 - } 73 - 74 - #[tauri::command] 75 - fn open_url(url: &str) { 76 - open::that(url).unwrap(); 77 - } 78 - 79 - #[tauri::command] 80 - fn open_folder(url: &str) { 81 - Command::new("explorer.exe") 82 - .arg(format!("/select,{}", url)) 83 - .spawn() 84 - .unwrap(); 85 - } 86 - 87 - // Check if the photo config file exists 88 - // if not just return the default vrchat path 89 - #[tauri::command] 90 - fn get_user_photos_path() -> path::PathBuf { 91 - get_photo_path() 92 - } 93 - 94 - // When the user changes the start with windows toggle 95 - // create and delete the shortcut from the startup folder 96 - #[tauri::command] 97 - fn start_with_win(start: bool) { 98 - thread::spawn(move || { 99 - if start { 100 - let target = dirs::home_dir() 101 - .unwrap() 102 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\vrchat-photo-manager.exe"); 103 - 104 - match fs::metadata(&target) { 105 - Ok(_) => { 106 - let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 107 - 108 - let sl = ShellLink::new(target).unwrap(); 109 - sl.create_lnk(lnk).unwrap(); 110 - } 111 - Err(_) => {} 112 - } 113 - } else { 114 - let lnk = dirs::home_dir().unwrap().join("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\VRChat Photo Manager.lnk"); 115 - fs::remove_file(lnk).unwrap(); 116 - } 117 - }); 118 - } 119 - 120 - // Load vrchat world data 121 - #[tauri::command] 122 - fn find_world_by_id(world_id: String, window: tauri::Window) { 123 - thread::spawn(move || { 124 - let world = World::new(world_id); 125 - window.emit("world_data", world).unwrap(); 126 - }); 127 - } 128 - 129 - // On requested sync the photos to the cloud 130 - #[tauri::command] 131 - fn sync_photos(token: String, window: tauri::Window) { 132 - thread::spawn(move || { 133 - photosync::sync_photos(token, get_photo_path(), window); 134 - }); 135 - } 136 - 137 - #[tauri::command] 138 - fn load_photos(window: tauri::Window) { 139 - thread::spawn(move || { 140 - let base_dir = get_photo_path(); 141 - 142 - let mut photos: Vec<path::PathBuf> = Vec::new(); 143 - let mut size: usize = 0; 144 - 145 - for folder in fs::read_dir(&base_dir).unwrap() { 146 - let f = folder.unwrap(); 147 - 148 - if f.metadata().unwrap().is_dir() { 149 - for photo in fs::read_dir(f.path()).unwrap() { 150 - let p = photo.unwrap(); 151 - 152 - if p.metadata().unwrap().is_file() { 153 - let fname = p.path(); 154 - 155 - 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(); 156 - let re2 = Regex::new( 157 - 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").unwrap(); 158 - 159 - if re1.is_match(p.file_name().to_str().unwrap()) 160 - || re2.is_match(p.file_name().to_str().unwrap()) 161 - { 162 - let path = fname.to_path_buf().clone(); 163 - let metadata = fs::metadata(&path).unwrap(); 164 - 165 - if metadata.is_file() { 166 - size += metadata.len() as usize; 167 - 168 - let path = path.strip_prefix(&base_dir).unwrap().to_path_buf(); 169 - photos.push(path); 170 - } 171 - } else { 172 - println!("Ignoring {:#?} as it doesn't match regex", p.file_name()); 173 - } 174 - } else { 175 - println!("Ignoring {:#?} as it is a directory", p.file_name()); 176 - } 177 - } 178 - } else { 179 - println!("Ignoring {:#?} as it isn't a directory", f.file_name()); 180 - } 181 - } 182 - 183 - println!("Found {} photos", photos.len()); 184 - window 185 - .emit("photos_loaded", PhotosLoadedResponse { photos, size }) 186 - .unwrap(); 187 - }); 188 - } 189 - 190 - // Reads the PNG file and loads the image metadata from it 191 - // then sends the metadata to the frontend, returns width, height, colour depth and so on... more info "pngmeta.rs" 192 - #[tauri::command] 193 - fn load_photo_meta(photo: &str, window: tauri::Window) { 194 - let photo = photo.to_string(); 195 - 196 - thread::spawn(move || { 197 - let base_dir = get_photo_path().join(&photo); 198 - 199 - let file = fs::File::open(base_dir.clone()); 200 - 201 - match file { 202 - Ok(mut file) => { 203 - let mut buffer = Vec::new(); 204 - 205 - let _out = file.read_to_end(&mut buffer); 206 - window 207 - .emit("photo_meta_loaded", PNGImage::new(buffer, photo)) 208 - .unwrap(); 209 - } 210 - Err(_) => { 211 - println!("Cannot read image file"); 212 - } 213 - } 214 - }); 215 - } 216 - 217 - // Delete a photo when the users confirms the prompt in the ui 218 - #[tauri::command] 219 - fn delete_photo(path: String, token: String, is_syncing: bool) { 220 - thread::spawn(move || { 221 - let p = get_photo_path().join(&path); 222 - fs::remove_file(p).unwrap(); 223 - 224 - let photo = path.split("\\").last().unwrap(); 225 - 226 - if is_syncing { 227 - let client = reqwest::blocking::Client::new(); 228 - client 229 - .delete(format!( 230 - "https://photos-cdn.phazed.xyz/api/v1/photos?token={}&photo={}", 231 - token, photo 232 - )) 233 - .timeout(Duration::from_secs(120)) 234 - .send() 235 - .unwrap(); 236 - } 237 - }); 238 - } 239 - 240 - #[tauri::command] 241 - fn change_final_path(new_path: &str) { 242 - let config_path = dirs::home_dir() 243 - .unwrap() 244 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 245 - 246 - fs::write(&config_path, new_path.as_bytes()).unwrap(); 247 - 248 - match fs::metadata(&new_path) { 249 - Ok(_) => {} 250 - Err(_) => { 251 - fs::create_dir(&new_path).unwrap(); 252 - } 253 - }; 254 - } 255 - 256 - #[tauri::command] 257 - fn get_version() -> String { 258 - String::from(VERSION) 259 - } 260 - 261 - #[tauri::command] 262 - fn relaunch() { 263 - let container_folder = dirs::home_dir() 264 - .unwrap() 265 - .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager"); 266 - 267 - let mut cmd = Command::new(&container_folder.join("./vrchat-photo-manager.exe")); 268 - cmd.current_dir(container_folder); 269 - cmd.spawn().expect("Cannot run updater"); 270 - 271 - process::exit(0); 272 - } 273 - 274 20 fn main() { 275 21 tauri_plugin_deep_link::prepare("uk.phaz.vrcpm"); 276 22 ··· 302 48 } 303 49 304 50 println!("Loading App..."); 305 - let photos_path = get_photo_path(); 51 + let photos_path = util::get_photo_path::get_photo_path(); 306 52 307 53 match fs::metadata(&photos_path) { 308 54 Ok(_) => {} ··· 311 57 } 312 58 }; 313 59 314 - let args: Vec<String> = env::args().collect(); 315 - 316 - let mut update = true; 317 - for arg in args { 318 - if arg == "--no-update" { 319 - update = false; 320 - } 321 - } 322 - 323 - if update { 324 - // Auto update 325 - thread::spawn(move || { 326 - let client = reqwest::blocking::Client::new(); 327 - 328 - let latest_version = client 329 - .get("https://cdn.phaz.uk/vrcpm/latest") 330 - .send() 331 - .unwrap() 332 - .text() 333 - .unwrap(); 334 - 335 - if latest_version != VERSION { 336 - match fs::metadata(&container_folder.join("./updater.exe")) { 337 - Ok(_) => {} 338 - Err(_) => { 339 - let latest_installer = client 340 - .get("https://cdn.phaz.uk/vrcpm/vrcpm-installer.exe") 341 - .timeout(Duration::from_secs(120)) 342 - .send() 343 - .unwrap() 344 - .bytes() 345 - .unwrap(); 346 - 347 - fs::write(&container_folder.join("./updater.exe"), latest_installer) 348 - .unwrap(); 349 - } 350 - } 351 - 352 - let mut cmd = Command::new(&container_folder.join("./updater.exe")); 353 - cmd.current_dir(container_folder); 354 - cmd.spawn().expect("Cannot run updater"); 355 - 356 - process::exit(0); 357 - } 358 - }); 359 - } 60 + util::check_updates::check_updates(container_folder); 360 61 361 62 // Listen for file updates, store each update in an mpsc channel and send to the frontend 362 63 let (sender, receiver) = std::sync::mpsc::channel(); ··· 374 75 re1.is_match(path.to_str().unwrap()) || 375 76 re2.is_match(path.to_str().unwrap()) 376 77 { 377 - sender.send((2, path.clone().strip_prefix(get_photo_path()).unwrap().to_path_buf())).unwrap(); 78 + sender.send((2, path.clone().strip_prefix(util::get_photo_path::get_photo_path()).unwrap().to_path_buf())).unwrap(); 378 79 } 379 80 }, 380 81 EventKind::Create(_) => { ··· 388 89 re2.is_match(path.to_str().unwrap()) 389 90 { 390 91 thread::sleep(time::Duration::from_millis(1000)); 391 - sender.send((1, path.clone().strip_prefix(get_photo_path()).unwrap().to_path_buf())).unwrap(); 92 + sender.send((1, path.clone().strip_prefix(util::get_photo_path::get_photo_path()).unwrap().to_path_buf())).unwrap(); 392 93 } 393 94 }, 394 95 _ => {} ··· 399 100 }).unwrap(); 400 101 401 102 watcher 402 - .watch(&get_photo_path(), RecursiveMode::Recursive) 103 + .watch(&util::get_photo_path::get_photo_path(), RecursiveMode::Recursive) 403 104 .unwrap(); 404 105 405 106 tauri::Builder::default() 406 107 .plugin(tauri_plugin_process::init()) 407 108 .plugin(tauri_plugin_http::init()) 408 109 .plugin(tauri_plugin_shell::init()) 409 - .register_asynchronous_uri_scheme_protocol("photo", move |_app, request, responder| { 410 - // TODO: Fix photos being W I D E 411 - thread::spawn(move || { 412 - // Loads the requested image file, sends data back to the user 413 - let uri = request.uri(); 414 - 415 - if request.method() != "GET" { 416 - responder.respond( 417 - Response::builder() 418 - .status(404) 419 - .header("Access-Control-Allow-Origin", "*") 420 - .body(Vec::new()) 421 - .unwrap(), 422 - ); 423 - 424 - return; 425 - } 426 - 427 - let path = uri.path().split_at(1).1; 428 - let file = fs::File::open(path); 429 - 430 - match file { 431 - Ok(mut file) => { 432 - match uri.query().unwrap(){ 433 - "downscale" => { 434 - let decoder = PngDecoder::new(BufReader::new(&file)).unwrap(); 435 - let src_image = DynamicImage::from_decoder(decoder).unwrap(); 436 - 437 - let size_multiplier: f32 = 200.0 / src_image.height() as f32; 438 - 439 - let dst_width = (src_image.width() as f32 * size_multiplier).floor() as u32; 440 - let dst_height: u32 = 200; 441 - 442 - let mut dst_image = Image::new(dst_width, dst_height, src_image.pixel_type().unwrap()); 443 - let mut resizer = Resizer::new(); 444 - 445 - let opts = ResizeOptions::new() 446 - .resize_alg(fast_image_resize::ResizeAlg::Nearest); 447 - 448 - resizer.resize(&src_image, &mut dst_image, Some(&opts)).unwrap(); 449 - 450 - let mut buf = Vec::new(); 451 - let encoder = PngEncoder::new(&mut buf); 452 - 453 - encoder.write_image(dst_image.buffer(), dst_width, dst_height, src_image.color().into()).unwrap(); 454 - 455 - let res = Response::builder() 456 - .status(200) 457 - .header("Access-Control-Allow-Origin", "*") 458 - .body(buf) 459 - .unwrap(); 460 - 461 - responder.respond(res); 462 - }, 463 - _ => { 464 - let mut buf = Vec::new(); 465 - file.read_to_end(&mut buf).unwrap(); 466 - 467 - let res = Response::builder() 468 - .status(200) 469 - .header("Access-Control-Allow-Origin", "*") 470 - .body(buf) 471 - .unwrap(); 472 - 473 - responder.respond(res); 474 - } 475 - } 476 - } 477 - Err(_) => { 478 - responder.respond( 479 - Response::builder() 480 - .status(404) 481 - .header("Access-Control-Allow-Origin", "*") 482 - .body(b"File Not Found") 483 - .unwrap(), 484 - ); 485 - } 486 - } 487 - }); 488 - }) 110 + .register_asynchronous_uri_scheme_protocol("photo", util::handle_uri_proto::handle_uri_proto) 489 111 .on_window_event(|window, event| match event { 490 112 WindowEvent::CloseRequested { api, .. } => { 491 113 window.hide().unwrap(); ··· 494 116 _ => {} 495 117 }) 496 118 .setup(|app| { 497 - let handle = app.handle().clone(); 498 - 499 - // Setup the tray icon and menu buttons 500 - let quit = MenuItemBuilder::new("Quit") 501 - .id("quit") 502 - .build(&handle) 503 - .unwrap(); 504 - 505 - let hide = MenuItemBuilder::new("Hide / Show") 506 - .id("hide") 507 - .build(&handle) 508 - .unwrap(); 509 - 510 - let tray_menu = MenuBuilder::new(&handle) 511 - .items(&[&quit, &hide]) 512 - .build() 513 - .unwrap(); 514 - 515 - TrayIconBuilder::with_id("main") 516 - .icon(tauri::image::Image::from_bytes(include_bytes!("../icons/32x32.png")).unwrap()) 517 - .menu(&tray_menu) 518 - .on_menu_event(move |app: &AppHandle, event| match event.id().as_ref() { 519 - "quit" => { 520 - std::process::exit(0); 521 - } 522 - "hide" => { 523 - let window = app.get_webview_window("main").unwrap(); 524 - 525 - if window.is_visible().unwrap() { 526 - window.hide().unwrap(); 527 - } else { 528 - window.show().unwrap(); 529 - window.set_focus().unwrap(); 530 - } 531 - } 532 - _ => {} 533 - }) 534 - .on_tray_icon_event(|tray, event| { 535 - if let TrayIconEvent::Click { 536 - button: MouseButton::Left, 537 - button_state: MouseButtonState::Up, 538 - .. 539 - } = event 540 - { 541 - let window = tray.app_handle().get_webview_window("main").unwrap(); 542 - 543 - window.show().unwrap(); 544 - window.set_focus().unwrap(); 545 - } 546 - }) 547 - .build(&handle) 548 - .unwrap(); 549 - // Register "deep link" for authentication via vrcpm:// 550 - tauri_plugin_deep_link::register("vrcpm", move |request| { 551 - let mut command: u8 = 0; 552 - let mut index: u8 = 0; 553 - 554 - for part in request.split('/').into_iter() { 555 - index += 1; 556 - 557 - if index == 3 && part == "auth-callback" { 558 - command = 1; 559 - } 119 + let handle = app.handle(); 560 120 561 - if index == 3 && part == "auth-denied" { 562 - handle.emit("auth-denied", "null").unwrap(); 563 - } 564 - 565 - if index == 4 && command == 1 { 566 - handle.emit("auth-callback", part).unwrap(); 567 - } 568 - } 569 - }) 570 - .unwrap(); 121 + util::setup_traymenu::setup_traymenu(handle); 122 + util::setup_deeplink::setup_deeplink(handle); 571 123 572 124 // I hate this approach but i have no clue how else to do this... 573 125 // reads the mpsc channel and sends the events to the frontend ··· 591 143 Ok(()) 592 144 }) 593 145 .invoke_handler(tauri::generate_handler![ 594 - start_user_auth, 595 - load_photos, 596 - close_splashscreen, 597 - load_photo_meta, 598 - delete_photo, 599 - open_url, 600 - open_folder, 601 - find_world_by_id, 602 - start_with_win, 603 - get_user_photos_path, 604 - change_final_path, 605 - sync_photos, 606 - get_version, 607 - relaunch 146 + start_user_auth::start_user_auth, 147 + load_photos::load_photos, 148 + close_splashscreen::close_splashscreen, 149 + load_photo_meta::load_photo_meta, 150 + delete_photo::delete_photo, 151 + open_url::open_url, 152 + open_folder::open_folder, 153 + find_world_by_id::find_world_by_id, 154 + start_with_win::start_with_win, 155 + get_user_photos_path::get_user_photos_path, 156 + change_final_path::change_final_path, 157 + sync_photos::sync_photos, 158 + util::get_version::get_version, 159 + relaunch::relaunch 608 160 ]) 609 161 .run(tauri::generate_context!()) 610 162 .expect("error while running tauri application");
+51
src-tauri/src/util/check_updates.rs
··· 1 + use std::{ env, fs, path, process::{ self, Command }, thread, time::Duration }; 2 + use crate::util; 3 + 4 + pub fn check_updates( container_folder: path::PathBuf ){ 5 + let args: Vec<String> = env::args().collect(); 6 + 7 + let mut update = true; 8 + for arg in args { 9 + if arg == "--no-update" { 10 + update = false; 11 + } 12 + } 13 + 14 + if update { 15 + // Auto update 16 + thread::spawn(move || { 17 + let client = reqwest::blocking::Client::new(); 18 + 19 + let latest_version = client 20 + .get("https://cdn.phaz.uk/vrcpm/latest") 21 + .send() 22 + .unwrap() 23 + .text() 24 + .unwrap(); 25 + 26 + if latest_version != util::get_version::get_version() { 27 + match fs::metadata(&container_folder.join("./updater.exe")) { 28 + Ok(_) => {} 29 + Err(_) => { 30 + let latest_installer = client 31 + .get("https://cdn.phaz.uk/vrcpm/vrcpm-installer.exe") 32 + .timeout(Duration::from_secs(120)) 33 + .send() 34 + .unwrap() 35 + .bytes() 36 + .unwrap(); 37 + 38 + fs::write(&container_folder.join("./updater.exe"), latest_installer) 39 + .unwrap(); 40 + } 41 + } 42 + 43 + let mut cmd = Command::new(&container_folder.join("./updater.exe")); 44 + cmd.current_dir(container_folder); 45 + cmd.spawn().expect("Cannot run updater"); 46 + 47 + process::exit(0); 48 + } 49 + }); 50 + } 51 + }
+25
src-tauri/src/util/get_photo_path.rs
··· 1 + use std::{ fs, path }; 2 + 3 + pub fn get_photo_path() -> path::PathBuf { 4 + let config_path = dirs::home_dir() 5 + .unwrap() 6 + .join("AppData\\Roaming\\PhazeDev\\VRChatPhotoManager\\.photos_path"); 7 + 8 + match fs::read_to_string(config_path) { 9 + Ok(path) => { 10 + if path 11 + != dirs::picture_dir() 12 + .unwrap() 13 + .join("VRChat") 14 + .to_str() 15 + .unwrap() 16 + .to_owned() 17 + { 18 + path::PathBuf::from(path) 19 + } else { 20 + dirs::picture_dir().unwrap().join("VRChat") 21 + } 22 + } 23 + Err(_) => dirs::picture_dir().unwrap().join("VRChat"), 24 + } 25 + }
+6
src-tauri/src/util/get_version.rs
··· 1 + const VERSION: &str = env!("CARGO_PKG_VERSION"); 2 + 3 + #[tauri::command] 4 + pub fn get_version() -> String { 5 + String::from(VERSION) 6 + }
+85
src-tauri/src/util/handle_uri_proto.rs
··· 1 + use std::{ fs, io::{ BufReader, Read }, thread }; 2 + use fast_image_resize::{ images::Image, IntoImageView, ResizeOptions, Resizer }; 3 + use image::{ codecs::png::{ PngDecoder, PngEncoder }, DynamicImage, ImageEncoder }; 4 + use tauri::{ http::{ Request, Response }, AppHandle, UriSchemeResponder }; 5 + 6 + pub fn handle_uri_proto( _app: &AppHandle, request: Request<Vec<u8>>, responder: UriSchemeResponder ){ 7 + thread::spawn(move || { 8 + // Loads the requested image file, sends data back to the user 9 + let uri = request.uri(); 10 + 11 + if request.method() != "GET" { 12 + responder.respond( 13 + Response::builder() 14 + .status(404) 15 + .header("Access-Control-Allow-Origin", "*") 16 + .body(Vec::new()) 17 + .unwrap(), 18 + ); 19 + 20 + return; 21 + } 22 + 23 + // TODO: Only accept files that are in the vrchat photos folder 24 + let path = uri.path().split_at(1).1; 25 + let file = fs::File::open(path); 26 + 27 + match file { 28 + Ok(mut file) => { 29 + match uri.query().unwrap(){ 30 + "downscale" => { 31 + let decoder = PngDecoder::new(BufReader::new(&file)).unwrap(); 32 + let src_image = DynamicImage::from_decoder(decoder).unwrap(); 33 + 34 + let size_multiplier: f32 = 200.0 / src_image.height() as f32; 35 + 36 + let dst_width = (src_image.width() as f32 * size_multiplier).floor() as u32; 37 + let dst_height: u32 = 200; 38 + 39 + let mut dst_image = Image::new(dst_width, dst_height, src_image.pixel_type().unwrap()); 40 + let mut resizer = Resizer::new(); 41 + 42 + let opts = ResizeOptions::new() 43 + .resize_alg(fast_image_resize::ResizeAlg::Nearest); 44 + 45 + resizer.resize(&src_image, &mut dst_image, Some(&opts)).unwrap(); 46 + 47 + let mut buf = Vec::new(); 48 + let encoder = PngEncoder::new(&mut buf); 49 + 50 + encoder.write_image(dst_image.buffer(), dst_width, dst_height, src_image.color().into()).unwrap(); 51 + 52 + let res = Response::builder() 53 + .status(200) 54 + .header("Access-Control-Allow-Origin", "*") 55 + .body(buf) 56 + .unwrap(); 57 + 58 + responder.respond(res); 59 + }, 60 + _ => { 61 + let mut buf = Vec::new(); 62 + file.read_to_end(&mut buf).unwrap(); 63 + 64 + let res = Response::builder() 65 + .status(200) 66 + .header("Access-Control-Allow-Origin", "*") 67 + .body(buf) 68 + .unwrap(); 69 + 70 + responder.respond(res); 71 + } 72 + } 73 + } 74 + Err(_) => { 75 + responder.respond( 76 + Response::builder() 77 + .status(404) 78 + .header("Access-Control-Allow-Origin", "*") 79 + .body(b"File Not Found") 80 + .unwrap(), 81 + ); 82 + } 83 + } 84 + }); 85 + }
+6
src-tauri/src/util/mod.rs
··· 1 + pub mod get_photo_path; 2 + pub mod get_version; 3 + pub mod check_updates; 4 + pub mod setup_traymenu; 5 + pub mod setup_deeplink; 6 + pub mod handle_uri_proto;
+28
src-tauri/src/util/setup_deeplink.rs
··· 1 + use tauri::{ AppHandle, Emitter }; 2 + 3 + pub fn setup_deeplink( handle: &AppHandle ){ 4 + let handle = handle.clone(); 5 + 6 + // Register "deep link" for authentication via vrcpm:// 7 + tauri_plugin_deep_link::register("vrcpm", move |request| { 8 + let mut command: u8 = 0; 9 + let mut index: u8 = 0; 10 + 11 + for part in request.split('/').into_iter() { 12 + index += 1; 13 + 14 + if index == 3 && part == "auth-callback" { 15 + command = 1; 16 + } 17 + 18 + if index == 3 && part == "auth-denied" { 19 + handle.emit("auth-denied", "null").unwrap(); 20 + } 21 + 22 + if index == 4 && command == 1 { 23 + handle.emit("auth-callback", part).unwrap(); 24 + } 25 + } 26 + }) 27 + .unwrap(); 28 + }
+54
src-tauri/src/util/setup_traymenu.rs
··· 1 + use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, AppHandle, Manager }; 2 + 3 + pub fn setup_traymenu( handle: &AppHandle ){ 4 + // Setup the tray icon and menu buttons 5 + let quit = MenuItemBuilder::new("Quit") 6 + .id("quit") 7 + .build(handle) 8 + .unwrap(); 9 + 10 + let hide = MenuItemBuilder::new("Hide / Show") 11 + .id("hide") 12 + .build(handle) 13 + .unwrap(); 14 + 15 + let tray_menu = MenuBuilder::new(handle) 16 + .items(&[&quit, &hide]) 17 + .build() 18 + .unwrap(); 19 + 20 + TrayIconBuilder::with_id("main") 21 + .icon(tauri::image::Image::from_bytes(include_bytes!("../../icons/32x32.png")).unwrap()) 22 + .menu(&tray_menu) 23 + .on_menu_event(move |app: &AppHandle, event| match event.id().as_ref() { 24 + "quit" => { 25 + std::process::exit(0); 26 + } 27 + "hide" => { 28 + let window = app.get_webview_window("main").unwrap(); 29 + 30 + if window.is_visible().unwrap() { 31 + window.hide().unwrap(); 32 + } else { 33 + window.show().unwrap(); 34 + window.set_focus().unwrap(); 35 + } 36 + } 37 + _ => {} 38 + }) 39 + .on_tray_icon_event(|tray, event| { 40 + if let TrayIconEvent::Click { 41 + button: MouseButton::Left, 42 + button_state: MouseButtonState::Up, 43 + .. 44 + } = event 45 + { 46 + let window = tray.app_handle().get_webview_window("main").unwrap(); 47 + 48 + window.show().unwrap(); 49 + window.set_focus().unwrap(); 50 + } 51 + }) 52 + .build(handle) 53 + .unwrap(); 54 + }
+2 -1
src-tauri/tauri.conf.json
··· 34 34 "minHeight": 400, 35 35 "visible": false, 36 36 "decorations": false, 37 - "transparent": true 37 + "transparent": true, 38 + "titleBarStyle": "Transparent" 38 39 } 39 40 ] 40 41 }
+2
src/Components/App.tsx
··· 9 9 import PhotoViewer from "./PhotoViewer"; 10 10 import SettingsMenu from "./SettingsMenu"; 11 11 12 + // TODO: Clean up frontend files, split up into smaller files PLEASE 13 + 12 14 function App() { 13 15 if(!localStorage.getItem('start-in-bg')){ 14 16 invoke('close_splashscreen')
+123 -6
src/Components/PhotoList.tsx
··· 1 - import { createEffect, onMount } from "solid-js"; 1 + import { createEffect, onCleanup, onMount } from "solid-js"; 2 2 import { invoke } from '@tauri-apps/api/core'; 3 3 import { listen } from '@tauri-apps/api/event'; 4 4 ··· 25 25 setIsPhotosSyncing!: ( syncing: boolean ) => boolean; 26 26 } 27 27 28 + enum ListPopup{ 29 + FILTERS, 30 + DATE, 31 + NONE 32 + } 33 + 28 34 // TODO: Photo filtering / Searching (By users, By date, By world) 29 35 let PhotoList = ( props: PhotoListProps ) => { 30 36 let amountLoaded = 0; ··· 39 45 let photoContainerBG: HTMLCanvasElement; 40 46 41 47 let filterContainer: HTMLDivElement; 48 + let scrollDateContainer: HTMLDivElement; 49 + let dateListContainer: HTMLDivElement; 42 50 43 51 let ctx: CanvasRenderingContext2D; 44 52 let ctxBG: CanvasRenderingContext2D; ··· 46 54 let photos: Photo[] = []; 47 55 let currentPhotoIndex: number = -1; 48 56 57 + let datesList: any = {}; 58 + 49 59 let scroll: number = 0; 50 60 let targetScroll: number = 0; 51 61 52 62 let quitRender: boolean = false; 53 63 let photoPath: string; 54 64 65 + let currentPopup = ListPopup.NONE; 66 + let targetScrollPhoto: Photo | null = null; 67 + 68 + let closeWithKey = ( e: KeyboardEvent ) => { 69 + if(e.key === 'Escape'){ 70 + closeCurrentPopup(); 71 + } 72 + } 73 + 74 + let closeCurrentPopup = () => { 75 + switch(currentPopup){ 76 + case ListPopup.FILTERS: 77 + anime({ 78 + targets: filterContainer, 79 + opacity: 0, 80 + easing: 'easeInOutQuad', 81 + duration: 100, 82 + complete: () => { 83 + filterContainer.style.display = 'none'; 84 + currentPopup = ListPopup.NONE; 85 + } 86 + }); 87 + 88 + break; 89 + case ListPopup.DATE: 90 + anime({ 91 + targets: scrollDateContainer, 92 + opacity: 0, 93 + easing: 'easeInOutQuad', 94 + duration: 100, 95 + complete: () => { 96 + scrollDateContainer.style.display = 'none'; 97 + currentPopup = ListPopup.NONE; 98 + } 99 + }); 100 + 101 + break; 102 + } 103 + } 104 + 55 105 createEffect(() => { 56 106 if(props.requestPhotoReload()){ 57 107 props.setRequestPhotoReload(false); ··· 172 222 let currentRowIndex = -1; 173 223 174 224 scroll = scroll + (targetScroll - scroll) * 0.2; 225 + 226 + if(targetScrollPhoto){ 227 + // TODO: Check if previous date. 228 + targetScroll += 100; 229 + } 175 230 176 231 let lastPhoto; 177 232 for (let i = 0; i < photos.length; i++) { ··· 216 271 ctx.fillStyle = '#fff'; 217 272 ctx.font = '30px Rubik'; 218 273 274 + if(targetScrollPhoto && p.dateString === targetScrollPhoto.dateString){ 275 + targetScrollPhoto = null; 276 + } 277 + 219 278 let dateParts = p.dateString.split('-'); 220 279 ctx.fillText(dateParts[2] + ' ' + months[parseInt(dateParts[1]) - 1] + ' ' + dateParts[0], photoContainer.width / 2, 60 + (currentRowIndex + 1.2) * 210 - scroll); 221 280 ··· 302 361 ctx.fillStyle = '#fff'; 303 362 ctx.font = '50px Rubik'; 304 363 305 - ctx.fillText("You have no bitches", photoContainer.width / 2, photoContainer.height / 2); 364 + ctx.fillText("It's looking empty in here! You have no photos :O", photoContainer.width / 2, photoContainer.height / 2); 306 365 } 307 366 308 367 ctxBG.filter = 'blur(100px)'; ··· 405 464 easing: 'easeInOutQuad' 406 465 }) 407 466 467 + photoPaths.forEach(( path: string ) => { 468 + let date = path.split('_')[1]; 469 + 470 + if(!datesList[date]) 471 + datesList[date] = 1; 472 + }); 473 + 474 + dateListContainer.innerHTML = ''; 475 + 476 + Object.keys(datesList).forEach(( date ) => { 477 + dateListContainer.appendChild(<div onClick={() => { 478 + let p = photos.find(x => x.dateString === date)!; 479 + targetScrollPhoto = p; 480 + }} class="date-list-date">{ date }</div> as HTMLElement); 481 + }) 482 + 408 483 render(); 409 484 }) 410 485 } ··· 422 497 if(targetScroll < 0) 423 498 targetScroll = 0; 424 499 }); 500 + 501 + window.addEventListener('keyup', closeWithKey); 425 502 426 503 photoContainer.width = window.innerWidth; 427 504 photoContainer.height = window.innerHeight; ··· 454 531 }) 455 532 }) 456 533 534 + onCleanup(() => { 535 + window.removeEventListener('keyup', closeWithKey); 536 + }) 537 + 457 538 return ( 458 539 <div class="photo-list"> 459 540 <div ref={filterContainer!} class="filter-container"> 460 541 <div class="filter-title">Filters</div> 461 542 </div> 462 543 544 + <div ref={scrollDateContainer!} class="filter-container"> 545 + <div class="date-list" ref={dateListContainer!}> 546 + <div class="filter-title">Loading Dates...</div> 547 + </div> 548 + </div> 549 + 463 550 <div class="photo-tree-loading" ref={( el ) => photoTreeLoadingContainer = el}>Scanning Photo Tree...</div> 464 551 465 552 <div class="scroll-to-top" ref={( el ) => scrollToTop = el} onClick={() => targetScroll = 0}> ··· 474 561 </div> 475 562 476 563 <div class="filter-options"> 477 - <div class="icon" style={{ width: '20px', height: '5px', padding: '20px' }}> 478 - <img draggable="false" src="/icon/sliders-solid.svg"></img> 564 + <div> 565 + <div onClick={() => { 566 + if(currentPopup != ListPopup.NONE)return closeCurrentPopup(); 567 + currentPopup = ListPopup.FILTERS; 568 + 569 + filterContainer.style.display = 'block'; 570 + 571 + anime({ 572 + targets: filterContainer, 573 + opacity: 1, 574 + easing: 'easeInOutQuad', 575 + duration: 100 576 + }); 577 + }} class="icon" style={{ width: '20px', height: '5px', padding: '20px' }}> 578 + <img draggable="false" src="/icon/sliders-solid.svg"></img> 579 + </div> 580 + <div class="icon-label">Filters</div> 479 581 </div> 480 - <div class="icon" style={{ width: '20px', height: '5px', padding: '20px' }}> 481 - <img draggable="false" src="/icon/clock-regular.svg"></img> 582 + <div> 583 + <div onClick={() => { 584 + if(currentPopup != ListPopup.NONE)return closeCurrentPopup(); 585 + currentPopup = ListPopup.DATE; 586 + 587 + scrollDateContainer.style.display = 'block'; 588 + 589 + anime({ 590 + targets: scrollDateContainer, 591 + opacity: 1, 592 + easing: 'easeInOutQuad', 593 + duration: 100 594 + }); 595 + }} class="icon" style={{ width: '20px', height: '5px', padding: '20px' }}> 596 + <img draggable="false" src="/icon/clock-regular.svg"></img> 597 + </div> 598 + <div class="icon-label">Scroll to Date</div> 482 599 </div> 483 600 </div> 484 601
+4 -4
src/Components/PhotoViewer.tsx
··· 292 292 <div> 293 293 { item.displayName } 294 294 <Show when={item.id}> 295 - <i onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/user/' + item.id })} style={{ "margin-left": '10px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} class="fa-solid fa-arrow-up-right-from-square"></i> 295 + <img width="15" src="/icon/up-right-from-square-solid.svg" onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/user/' + item.id })} style={{ "margin-left": '10px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} /> 296 296 </Show> 297 297 </div> 298 298 } ··· 331 331 } 332 332 333 333 if(photo && !isOpen){ 334 - viewer.style.display = 'block'; 334 + viewer.style.display = 'flex'; 335 335 336 336 anime({ 337 337 targets: viewer, ··· 391 391 <div> 392 392 <Show when={ data.worldData.found == false && meta }> 393 393 <div> 394 - <div class="world-name">{ JSON.parse(meta).world.name } <i onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/world/' + data.worldData.id })} style={{ "margin-left": '0px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} class="fa-solid fa-arrow-up-right-from-square"></i></div> 394 + <div class="world-name">{ JSON.parse(meta).world.name } <img width="15" src="/icon/up-right-from-square-solid.svg" onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/world/' + data.worldData.id })} style={{ "margin-left": '0px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} /></div> 395 395 <div style={{ width: '75%', margin: 'auto' }}>Could not fetch world information... Is the world private?</div> 396 396 </div> 397 397 </Show> 398 398 <Show when={ data.worldData.found == true }> 399 - <div class="world-name">{ data.worldData.name } <i onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/world/' + data.worldData.id })} style={{ "margin-left": '0px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} class="fa-solid fa-arrow-up-right-from-square"></i></div> 399 + <div class="world-name">{ data.worldData.name } <img width="15" src="/icon/up-right-from-square-solid.svg" onClick={() => invoke('open_url', { url: 'https://vrchat.com/home/world/' + data.worldData.id })} style={{ "margin-left": '0px', "font-size": '12px', 'color': '#bbb', cursor: 'pointer' }} /></div> 400 400 <div style={{ width: '75%', margin: 'auto' }}>{ data.worldData.desc }</div> 401 401 402 402 <br />
+40 -1
src/styles.css
··· 99 99 height: 100%; 100 100 } 101 101 102 + .icon-label{ 103 + margin-top: -20px; 104 + margin-right: -200px; 105 + width: 200px; 106 + color: white; 107 + pointer-events: none; 108 + transform: translate(40px, -12px); 109 + opacity: 0; 110 + transition: 0.25s; 111 + user-select: none; 112 + } 113 + 114 + .icon:hover ~ .icon-label{ 115 + opacity: 1; 116 + transform: translate(60px, -12px); 117 + } 118 + 102 119 .user-pfp{ 103 120 width: 35px; 104 121 height: 35px; ··· 192 209 color: #fff; 193 210 text-align: center; 194 211 box-shadow: #0005 0 0 10px; 212 + opacity: 0; 195 213 } 196 214 197 215 .filter-container > .filter-title{ 198 216 font-size: 30px; 199 217 } 200 218 219 + .date-list{ 220 + mask-image: linear-gradient(to bottom, #0000, #000, #0000); 221 + overflow: auto; 222 + scrollbar-width: thin; 223 + height: calc(100% - 100px); 224 + padding: 50px 0; 225 + } 226 + 227 + .date-list-date{ 228 + padding: 10px; 229 + user-select: none; 230 + cursor: pointer; 231 + transition: 0.1s; 232 + border-radius: 10px; 233 + } 234 + 235 + .date-list-date:hover{ 236 + background: #0005; 237 + box-shadow: inset #0005 0 0 10px; 238 + } 239 + 201 240 .photo-tree-loading{ 202 241 width: 100%; 203 242 height: 100%; ··· 244 283 } 245 284 246 285 .photo-viewer{ 286 + justify-content: center; 247 287 width: 100%; 248 288 height: 100%; 249 289 position: fixed; ··· 283 323 } 284 324 285 325 .image-container{ 286 - width: 100%; 287 326 height: 100%; 288 327 background-size: contain !important; 289 328 background-repeat: no-repeat !important;