+2
-2
src-tauri/src/main.rs
+2
-2
src-tauri/src/main.rs
···
100
100
101
101
// On requested sync the photos to the cloud
102
102
#[tauri::command]
103
-
fn sync_photos( token: String ){
103
+
fn sync_photos( token: String, window: tauri::Window ){
104
104
thread::spawn(move || {
105
-
photosync::sync_photos(token, get_photo_path());
105
+
photosync::sync_photos(token, get_photo_path(), window);
106
106
});
107
107
}
108
108
+108
-20
src-tauri/src/photosync.rs
+108
-20
src-tauri/src/photosync.rs
···
1
-
use std::{ path, /*fs*/ };
2
-
// use regex::Regex;
1
+
use std::{ fs, path, time::Duration };
2
+
use regex::Regex;
3
+
use reqwest::{ self, Error };
4
+
use serde::Serialize;
5
+
use serde_json::Value;
6
+
use tauri::Manager;
3
7
4
-
pub fn sync_photos( _token: String, _path: path::PathBuf ){
5
-
// match fs::metadata(&path){
6
-
// Ok(_) => {}
7
-
// Err(_) => {
8
-
// fs::create_dir(&path).unwrap();
9
-
// }
10
-
// };
8
+
#[derive(Clone, Serialize)]
9
+
struct PhotoUploadMeta{
10
+
photos_uploading: usize,
11
+
photos_total: usize
12
+
}
11
13
12
-
// let mut photos: Vec<path::PathBuf> = Vec::new();
13
-
// let mut size: usize = 0;
14
+
pub fn sync_photos( token: String, path: path::PathBuf, window: tauri::Window ){
15
+
match fs::metadata(&path){
16
+
Ok(_) => {}
17
+
Err(_) => {
18
+
fs::create_dir(&path).unwrap();
19
+
}
20
+
};
14
21
15
-
// for folder in fs::read_dir(&path).unwrap() {
16
-
// let f = folder.unwrap();
22
+
let mut photos: Vec<String> = Vec::new();
17
23
18
-
// if f.metadata().unwrap().is_dir() {
19
-
// for photo in fs::read_dir(f.path()).unwrap() {
20
-
// let p = photo.unwrap();
24
+
for folder in fs::read_dir(&path).unwrap() {
25
+
let f = folder.unwrap();
21
26
22
-
// dbg!(p.file_name());
23
-
// }
24
-
// }
25
-
// }
27
+
if f.metadata().unwrap().is_dir() {
28
+
for photo in fs::read_dir(f.path()).unwrap() {
29
+
let p = photo.unwrap();
30
+
31
+
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();
32
+
let re2 = Regex::new(
33
+
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();
34
+
35
+
if
36
+
re1.is_match(p.file_name().to_str().unwrap()) ||
37
+
re2.is_match(p.file_name().to_str().unwrap())
38
+
{
39
+
photos.push(p.file_name().into_string().unwrap());
40
+
}
41
+
}
42
+
}
43
+
}
44
+
45
+
let body: Value = reqwest::blocking::get(format!("https://photos.phazed.xyz/api/v1/photos/exists?token={}", &token)).unwrap()
46
+
.json().unwrap();
47
+
48
+
let mut photos_to_upload: Vec<String> = Vec::new();
49
+
let uploaded_photos = body["files"].as_array().unwrap();
50
+
51
+
let photos_len = photos.len();
52
+
53
+
for photo in photos{
54
+
let mut found_photo = false;
55
+
56
+
for uploaded_photo in uploaded_photos{
57
+
if photo == uploaded_photo.as_str().unwrap(){
58
+
found_photo = true;
59
+
break;
60
+
}
61
+
}
62
+
63
+
if !found_photo {
64
+
photos_to_upload.push(photo);
65
+
}
66
+
}
67
+
68
+
window.emit_all("photos-upload-meta", PhotoUploadMeta { photos_uploading: photos_to_upload.len(), photos_total: photos_len }).unwrap();
69
+
let mut photos_left = photos_to_upload.len();
70
+
71
+
let client = reqwest::blocking::Client::new();
72
+
73
+
loop {
74
+
match photos_to_upload.pop(){
75
+
Some(photo) => {
76
+
let folder_name = photo.clone().replace("VRChat_", "");
77
+
let mut folder_name = folder_name.split("-");
78
+
let folder_name = format!("{}-{}", folder_name.nth(0).unwrap(), folder_name.nth(0).unwrap());
79
+
80
+
let full_path = format!("{}\\{}\\{}", path.to_str().unwrap(), folder_name, photo);
81
+
let file = fs::File::open(full_path).unwrap();
82
+
83
+
let res: Result<Value, Error> = client.put(format!("https://photos.phazed.xyz/api/v1/photos?token={}", &token))
84
+
.header("Content-Type", "image/png")
85
+
.header("filename", photo)
86
+
.body(file)
87
+
.timeout(Duration::from_secs(120))
88
+
.send().unwrap().json();
89
+
90
+
match res {
91
+
Ok(res) => {
92
+
if !res["ok"].as_bool().unwrap(){
93
+
println!("Failed to upload: {}", res["error"].as_str().unwrap());
94
+
window.emit_all("sync-failed", res["error"].as_str().unwrap()).unwrap();
95
+
break;
96
+
}
97
+
}
98
+
Err(err) => {
99
+
dbg!(err);
100
+
}
101
+
}
102
+
103
+
photos_left -= 1;
104
+
window.emit_all("photos-upload-meta", PhotoUploadMeta { photos_uploading: photos_left, photos_total: photos_len }).unwrap();
105
+
}
106
+
None => {
107
+
break;
108
+
}
109
+
}
110
+
}
111
+
112
+
window.emit_all("sync-finished", "h").unwrap();
113
+
println!("Finished Uploading.");
26
114
}
+19
-4
src/Components/App.tsx
+19
-4
src/Components/App.tsx
···
28
28
29
29
let [ requestPhotoReload, setRequestPhotoReload ] = createSignal(false);
30
30
31
+
let isPhotosSyncing = false;
32
+
31
33
let setConfirmationBox = ( text: string, cb: () => void ) => {
32
34
setConfirmationBoxText(text);
33
35
confirmationBoxCallback = cb;
···
48
50
setLoggedIn({ loggedIn: true, username: data.data.user.username, avatar: data.data.user.avatar, id: data.data.user._id, serverVersion: data.data.user.serverVersion });
49
51
setStorageInfo({ storage: data.data.user.storage, used: data.data.user.used, sync: data.data.user.settings.enableSync });
50
52
51
-
invoke('sync_photos', { token: localStorage.getItem('token') });
53
+
if(!isPhotosSyncing){
54
+
isPhotosSyncing = true;
55
+
invoke('sync_photos', { token: localStorage.getItem('token') });
56
+
}
52
57
})
53
58
.catch(e => {
54
59
console.error(e);
···
129
134
setLoggedIn({ loggedIn: true, username: data.data.user.username, avatar: data.data.user.avatar, id: data.data.user._id, serverVersion: data.data.user.serverVersion });
130
135
setStorageInfo({ storage: data.data.user.storage, used: data.data.user.used, sync: data.data.user.settings.enableSync });
131
136
132
-
invoke('sync_photos', { token: localStorage.getItem('token') });
137
+
if(!isPhotosSyncing){
138
+
isPhotosSyncing = true;
139
+
invoke('sync_photos', { token: localStorage.getItem('token') });
140
+
}
133
141
})
134
142
.catch(e => {
135
143
setLoadingType('none');
···
142
150
console.warn('Authetication Denied');
143
151
})
144
152
153
+
listen('sync-finished', () => {
154
+
isPhotosSyncing = false;
155
+
})
156
+
145
157
onMount(() => {
146
158
anime.set('.settings',
147
159
{
···
153
165
154
166
return (
155
167
<div class="container">
156
-
<NavBar setLoadingType={setLoadingType} loggedIn={loggedIn} />
168
+
<NavBar setLoadingType={setLoadingType} loggedIn={loggedIn} setStorageInfo={setStorageInfo} />
157
169
<PhotoList
170
+
isPhotosSyncing={isPhotosSyncing}
158
171
setCurrentPhotoView={setCurrentPhotoView}
159
172
currentPhotoView={currentPhotoView}
160
173
photoNavChoice={photoNavChoice}
···
177
190
photoSize={photoSize}
178
191
setRequestPhotoReload={setRequestPhotoReload}
179
192
loggedIn={loggedIn}
180
-
storageInfo={storageInfo} />
193
+
storageInfo={storageInfo}
194
+
setStorageInfo={setStorageInfo}
195
+
setConfirmationBox={setConfirmationBox} />
181
196
182
197
<div class="copy-notif">Image Copied!</div>
183
198
+6
src/Components/PhotoList.tsx
+6
src/Components/PhotoList.tsx
···
20
20
requestPhotoReload!: () => boolean;
21
21
setRequestPhotoReload!: ( val: boolean ) => boolean;
22
22
loggedIn!: () => { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string };
23
+
isPhotosSyncing!: boolean;
23
24
}
24
25
25
26
let PhotoList = ( props: PhotoListProps ) => {
···
310
311
listen('photo_create', ( event: any ) => {
311
312
let photo = new Photo(event.payload);
312
313
photos.splice(0, 0, photo);
314
+
315
+
if(!props.isPhotosSyncing){
316
+
props.isPhotosSyncing = true;
317
+
invoke('sync_photos', { token: localStorage.getItem('token') });
318
+
}
313
319
})
314
320
315
321
listen('photo_remove', ( event: any ) => {
+9
-6
src/Components/PhotoViewer.tsx
+9
-6
src/Components/PhotoViewer.tsx
···
127
127
128
128
if(photo.metadata){
129
129
let meta = JSON.parse(photo.metadata);
130
-
131
130
let worldData = worldCache.find(x => x.worldData.id === meta.world.id);
132
131
133
132
photoTray.innerHTML = '';
···
157
156
158
157
if(!worldData)
159
158
invoke('find_world_by_id', { worldId: meta.world.id });
160
-
else if(worldData.expiresOn < Date.now())
159
+
else if(worldData.expiresOn < Date.now()){
160
+
worldCache = worldCache.filter(x => x !== worldData)
161
161
invoke('find_world_by_id', { worldId: meta.world.id });
162
-
else
162
+
} else
163
163
loadWorldData(worldData);
164
164
165
165
trayButton.style.display = 'flex';
···
217
217
})
218
218
219
219
let loadWorldData = ( data: WorldCache ) => {
220
+
let meta = props.currentPhotoView().metadata;
221
+
if(!meta)return;
222
+
220
223
worldInfoContainer.innerHTML = '';
221
224
worldInfoContainer.appendChild(
222
225
<div>
223
-
<Show when={ data.worldData.found == false && props.currentPhotoView().metadata }>
226
+
<Show when={ data.worldData.found == false && meta }>
224
227
<div>
225
-
<div class="world-name">{ JSON.parse(props.currentPhotoView().metadata).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>
228
+
<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>
226
229
<div style={{ width: '75%', margin: 'auto' }}>Could not fetch world information... Is the world private?</div>
227
230
</div>
228
231
</Show>
···
237
240
<div>{ tag.replace("author_tag_", "").replace("system_", "") }</div>
238
241
}
239
242
</For>
240
-
</div>
243
+
</div><br />
241
244
</Show>
242
245
</div> as Node
243
246
)
+28
src/Components/SettingsMenu.tsx
+28
src/Components/SettingsMenu.tsx
···
3
3
import { relaunch } from '@tauri-apps/api/process';
4
4
import { invoke } from '@tauri-apps/api/tauri';
5
5
import anime from "animejs";
6
+
import { fetch, ResponseType } from "@tauri-apps/api/http"
6
7
7
8
class SettingsMenuProps{
8
9
photoCount!: () => number;
···
10
11
setRequestPhotoReload!: ( val: boolean ) => boolean;
11
12
loggedIn!: () => { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string };
12
13
storageInfo!: () => { storage: number, used: number, sync: boolean };
14
+
setStorageInfo!: ( info: { storage: number, used: number, sync: boolean } ) => { storage: number, used: number, sync: boolean };
15
+
setConfirmationBox!: ( text: string, cb: () => void ) => void;
13
16
}
14
17
15
18
let SettingsMenu = ( props: SettingsMenuProps ) => {
···
152
155
})
153
156
})
154
157
158
+
let refreshAccount = () => {
159
+
fetch<any>('https://photos.phazed.xyz/api/v1/account', {
160
+
method: 'GET',
161
+
headers: { auth: localStorage.getItem('token')! },
162
+
responseType: ResponseType.JSON
163
+
})
164
+
.then(data => {
165
+
if(!data.data.ok){
166
+
console.error(data);
167
+
return;
168
+
}
169
+
170
+
console.log(data.data);
171
+
props.setStorageInfo({ storage: data.data.user.storage, used: data.data.user.used, sync: data.data.user.settings.enableSync });
172
+
})
173
+
.catch(e => {
174
+
console.error(e);
175
+
})
176
+
}
177
+
155
178
return (
156
179
<div class="settings">
157
180
<div class="settings-container" ref={( el ) => settingsContainer = el}>
···
258
281
<div class="account-profile">
259
282
<div class="account-pfp" style={{ background: `url('https://cdn.phazed.xyz/id/avatars/${props.loggedIn().id}/${props.loggedIn().avatar}.png')` }}></div>
260
283
<div class="account-desc">
284
+
<div class="reload-photos" onClick={() => refreshAccount()}><i class="fa-solid fa-arrows-rotate"></i></div>
261
285
<h2>{ props.loggedIn().username }</h2>
262
286
263
287
<Show when={props.storageInfo().sync}>
···
275
299
</div>
276
300
277
301
<div class="account-notice">To enable cloud storage or get more storage please contact "_phaz" on discord</div>
302
+
303
+
<div class="account-notice" style={{ display: 'flex' }}>
304
+
<div class="button-danger" onClick={() => props.setConfirmationBox("You are about to delete all your photos from the cloud, and disable syncing. This will NOT delete any local files.", () => {})}>Delete All Photos.</div> <div>This deletes all photos stored in the cloud and disables syncing.</div>
305
+
</div>
278
306
</Show>
279
307
</div>
280
308
</div>