+6
-1
changelog
+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
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
+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
+6
src-tauri/src/frontend_calls/close_splashscreen.rs
+25
src-tauri/src/frontend_calls/delete_photo.rs
+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
+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
+9
src-tauri/src/frontend_calls/get_user_photos_path.rs
src-tauri/src/frontend_calls/get_version.rs
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
+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
+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
+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
+9
src-tauri/src/frontend_calls/open_folder.rs
+6
src-tauri/src/frontend_calls/open_url.rs
+6
src-tauri/src/frontend_calls/open_url.rs
+14
src-tauri/src/frontend_calls/relaunch.rs
+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
+4
src-tauri/src/frontend_calls/start_user_auth.rs
+28
src-tauri/src/frontend_calls/start_with_win.rs
+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
+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
+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
+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
+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
+6
src-tauri/src/util/get_version.rs
+85
src-tauri/src/util/handle_uri_proto.rs
+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
+6
src-tauri/src/util/mod.rs
+28
src-tauri/src/util/setup_deeplink.rs
+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
+
}
+2
-1
src-tauri/tauri.conf.json
+2
-1
src-tauri/tauri.conf.json
+2
src/Components/App.tsx
+2
src/Components/App.tsx
+123
-6
src/Components/PhotoList.tsx
+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
+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
+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;