+70
src-tauri/Cargo.lock
+70
src-tauri/Cargo.lock
···
753
753
]
754
754
755
755
[[package]]
756
+
name = "fsevent-sys"
757
+
version = "4.1.0"
758
+
source = "registry+https://github.com/rust-lang/crates.io-index"
759
+
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
760
+
dependencies = [
761
+
"libc",
762
+
]
763
+
764
+
[[package]]
756
765
name = "futf"
757
766
version = "0.1.5"
758
767
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1398
1407
]
1399
1408
1400
1409
[[package]]
1410
+
name = "inotify"
1411
+
version = "0.9.6"
1412
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1413
+
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
1414
+
dependencies = [
1415
+
"bitflags 1.3.2",
1416
+
"inotify-sys",
1417
+
"libc",
1418
+
]
1419
+
1420
+
[[package]]
1421
+
name = "inotify-sys"
1422
+
version = "0.1.5"
1423
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1424
+
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
1425
+
dependencies = [
1426
+
"libc",
1427
+
]
1428
+
1429
+
[[package]]
1401
1430
name = "instant"
1402
1431
version = "0.1.12"
1403
1432
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1521
1550
]
1522
1551
1523
1552
[[package]]
1553
+
name = "kqueue"
1554
+
version = "1.0.8"
1555
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1556
+
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
1557
+
dependencies = [
1558
+
"kqueue-sys",
1559
+
"libc",
1560
+
]
1561
+
1562
+
[[package]]
1563
+
name = "kqueue-sys"
1564
+
version = "1.0.4"
1565
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1566
+
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
1567
+
dependencies = [
1568
+
"bitflags 1.3.2",
1569
+
"libc",
1570
+
]
1571
+
1572
+
[[package]]
1524
1573
name = "kuchikiki"
1525
1574
version = "0.8.2"
1526
1575
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1718
1767
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
1719
1768
dependencies = [
1720
1769
"libc",
1770
+
"log",
1721
1771
"wasi 0.11.0+wasi-snapshot-preview1",
1722
1772
"windows-sys 0.48.0",
1723
1773
]
···
1779
1829
version = "0.1.14"
1780
1830
source = "registry+https://github.com/rust-lang/crates.io-index"
1781
1831
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
1832
+
1833
+
[[package]]
1834
+
name = "notify"
1835
+
version = "6.1.1"
1836
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1837
+
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
1838
+
dependencies = [
1839
+
"bitflags 2.4.2",
1840
+
"crossbeam-channel",
1841
+
"filetime",
1842
+
"fsevent-sys",
1843
+
"inotify",
1844
+
"kqueue",
1845
+
"libc",
1846
+
"log",
1847
+
"mio",
1848
+
"walkdir",
1849
+
"windows-sys 0.48.0",
1850
+
]
1782
1851
1783
1852
[[package]]
1784
1853
name = "nu-ansi-term"
···
3603
3672
version = "0.0.0"
3604
3673
dependencies = [
3605
3674
"dirs",
3675
+
"notify",
3606
3676
"open 5.0.1",
3607
3677
"regex",
3608
3678
"serde",
+1
src-tauri/Cargo.toml
+1
src-tauri/Cargo.toml
+82
-3
src-tauri/src/main.rs
+82
-3
src-tauri/src/main.rs
···
3
3
mod pngmeta;
4
4
5
5
use tauri::{ CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowEvent, http::ResponseBuilder };
6
+
use core::time;
6
7
use std::{ fs, io::Read, path, thread };
7
8
use regex::Regex;
8
9
use pngmeta::PNGImage;
10
+
use notify::{ EventKind, RecursiveMode, Watcher };
9
11
10
12
#[derive(Clone, serde::Serialize)]
11
13
struct PhotoLoadResponse{
···
23
25
open::that("https://id.phazed.xyz?oauth=79959294626406").unwrap();
24
26
}
25
27
28
+
// Scans all files under the "Pictures/VRChat" path
29
+
// then sends the list of photos to the frontend
26
30
#[tauri::command]
27
31
fn load_photos(window: tauri::Window) {
28
32
thread::spawn(move || {
···
43
47
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();
44
48
let re2 = Regex::new(
45
49
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();
46
-
50
+
47
51
if
48
52
re1.is_match(p.file_name().to_str().unwrap()) ||
49
53
re2.is_match(p.file_name().to_str().unwrap())
50
54
{
51
55
let path = fname.to_path_buf().clone();
52
56
let path = path.strip_prefix(dirs::home_dir().unwrap().join("Pictures\\VRChat")).unwrap().to_path_buf();
53
-
57
+
54
58
photos.push(path);
55
59
}
56
60
}
···
62
66
});
63
67
}
64
68
69
+
// Reads the PNG file and loads the image metadata from it
70
+
// then sends the metadata to the frontend, returns width, height, colour depth and so on... more info "pngmeta.rs"
65
71
#[tauri::command]
66
72
fn load_photo_meta( photo: &str, window: tauri::Window ){
67
73
let photo = photo.to_string();
···
78
84
});
79
85
}
80
86
87
+
// Delete a photo when the users confirms the prompt in the ui
88
+
#[tauri::command]
89
+
fn delete_photo( path: &str ){
90
+
let p = dirs::home_dir().unwrap().join("Pictures\\VRChat").join(path);
91
+
fs::remove_file(p).unwrap();
92
+
}
93
+
81
94
fn main() {
82
95
std::env::set_var("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--ignore-gpu-blacklist");
83
96
84
97
tauri_plugin_deep_link::prepare("uk.phaz.vrcpm");
85
98
99
+
// Setup the tray icon and menu buttons
86
100
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
87
101
let hide = CustomMenuItem::new("hide".to_string(), "Hide / Show");
88
102
···
93
107
94
108
let tray = SystemTray::new().with_menu(tray_menu);
95
109
110
+
// Listen for file updates, store each update in an mpsc channel and send to the frontend
111
+
let (sender, receiver) = std::sync::mpsc::channel();
112
+
let mut watcher = notify::recommended_watcher(move | res: Result<notify::Event, notify::Error> | {
113
+
match res {
114
+
Ok(event) => {
115
+
match event.kind{
116
+
EventKind::Remove(_) => {
117
+
let path = event.paths.first().unwrap();
118
+
119
+
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();
120
+
let re2 = Regex::new(r"(?m)/VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png/gm").unwrap();
121
+
122
+
if
123
+
re1.is_match(path.to_str().unwrap()) ||
124
+
re2.is_match(path.to_str().unwrap())
125
+
{
126
+
sender.send((2, path.clone().strip_prefix(dirs::home_dir().unwrap().join("Pictures\\VRChat")).unwrap().to_path_buf())).unwrap();
127
+
}
128
+
},
129
+
EventKind::Create(_) => {
130
+
let path = event.paths.first().unwrap();
131
+
132
+
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();
133
+
let re2 = Regex::new(r"(?m)/VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png/gm").unwrap();
134
+
135
+
if
136
+
re1.is_match(path.to_str().unwrap()) ||
137
+
re2.is_match(path.to_str().unwrap())
138
+
{
139
+
sender.send((1, path.clone().strip_prefix(dirs::home_dir().unwrap().join("Pictures\\VRChat")).unwrap().to_path_buf())).unwrap();
140
+
}
141
+
},
142
+
_ => {}
143
+
}
144
+
},
145
+
Err(e) => println!("watch error: {:?}", e),
146
+
}
147
+
}).unwrap();
148
+
149
+
watcher.watch(&dirs::home_dir().unwrap().join("Pictures\\VRChat"), RecursiveMode::Recursive).unwrap();
150
+
96
151
tauri::Builder::default()
97
152
.system_tray(tray)
98
153
.on_system_tray_event(|app, event| match event {
···
131
186
_ => {}
132
187
})
133
188
.register_uri_scheme_protocol("photo", | _app, request | {
189
+
// Loads the requested image file, sends data back to the user
134
190
let uri = request.uri();
135
191
136
192
if request.method() != "GET" {
137
193
return ResponseBuilder::new()
138
194
.status(404)
195
+
.header("Access-Control-Allow-Origin", "*")
139
196
.body(Vec::new());
140
197
}
141
198
···
154
211
155
212
ResponseBuilder::new()
156
213
.status(200)
214
+
.header("Access-Control-Allow-Origin", "*")
157
215
.body(buffer)
158
216
},
159
217
Err(_) => {
160
218
ResponseBuilder::new()
161
219
.status(404)
220
+
.header("Access-Control-Allow-Origin", "*")
162
221
.body("File Not Found".into())
163
222
}
164
223
}
···
166
225
.setup(|app| {
167
226
let handle = app.handle();
168
227
228
+
// Register "deep link" for authentication via vrcpm://
169
229
tauri_plugin_deep_link::register(
170
230
"vrcpm",
171
231
move | request | {
···
190
250
}
191
251
).unwrap();
192
252
253
+
// I hate this approach but i have no clue how else to do this...
254
+
// reads the mpsc channel and sends the events to the frontend
255
+
let window = app.get_window("main").unwrap();
256
+
thread::spawn(move || {
257
+
thread::sleep(time::Duration::from_millis(100));
258
+
259
+
for event in receiver {
260
+
match event.0 {
261
+
1 => {
262
+
window.emit("photo_create", event.1).unwrap();
263
+
},
264
+
2 => {
265
+
window.emit("photo_remove", event.1).unwrap();
266
+
},
267
+
_ => {}
268
+
}
269
+
}
270
+
});
271
+
193
272
Ok(())
194
273
})
195
-
.invoke_handler(tauri::generate_handler![start_user_auth, load_photos, close_splashscreen, load_photo_meta])
274
+
.invoke_handler(tauri::generate_handler![start_user_auth, load_photos, close_splashscreen, load_photo_meta, delete_photo])
196
275
.run(tauri::generate_context!())
197
276
.expect("error while running tauri application");
198
277
}
+39
-2
src/Components/App.tsx
+39
-2
src/Components/App.tsx
···
16
16
let [ currentPhotoView, setCurrentPhotoView ] = createSignal<any>(null);
17
17
let [ photoNavChoice, setPhotoNavChoice ] = createSignal<string>('');
18
18
19
+
let [ confirmationBoxText, setConfirmationBoxText ] = createSignal<string>('');
20
+
let confirmationBoxCallback = () => {}
21
+
22
+
let setConfirmationBox = ( text: string, cb: () => void ) => {
23
+
setConfirmationBoxText(text);
24
+
confirmationBoxCallback = cb;
25
+
}
26
+
19
27
if(localStorage.getItem('token')){
20
28
fetch<any>('https://photos.phazed.xyz/api/v1/account', {
21
29
method: 'GET',
···
41
49
let loadingBlackout: HTMLElement;
42
50
let loadingShown = false;
43
51
52
+
let confirmationBox: HTMLElement;
53
+
54
+
createEffect(() => {
55
+
if(confirmationBoxText() !== ''){
56
+
confirmationBox.style.display = 'block';
57
+
58
+
setTimeout(() => {
59
+
confirmationBox.style.opacity = '1';
60
+
}, 1);
61
+
} else{
62
+
confirmationBox.style.opacity = '0';
63
+
64
+
setTimeout(() => {
65
+
confirmationBox.style.display = 'none';
66
+
}, 250);
67
+
}
68
+
})
69
+
44
70
createEffect(() => {
45
71
let type = loadingType();
46
72
···
102
128
return (
103
129
<div class="container">
104
130
<NavBar setLoadingType={setLoadingType} loggedIn={loggedIn} />
105
-
<PhotoList setCurrentPhotoView={setCurrentPhotoView} photoNavChoice={photoNavChoice} setPhotoNavChoice={setPhotoNavChoice} />
106
-
<PhotoViewer setPhotoNavChoice={setPhotoNavChoice} currentPhotoView={currentPhotoView} setCurrentPhotoView={setCurrentPhotoView} />
131
+
<PhotoList setCurrentPhotoView={setCurrentPhotoView} currentPhotoView={currentPhotoView} photoNavChoice={photoNavChoice} setPhotoNavChoice={setPhotoNavChoice} setConfirmationBox={setConfirmationBox} />
132
+
<PhotoViewer setPhotoNavChoice={setPhotoNavChoice} currentPhotoView={currentPhotoView} setCurrentPhotoView={setCurrentPhotoView} setConfirmationBox={setConfirmationBox} />
133
+
134
+
<div class="copy-notif">Image Copied!</div>
107
135
108
136
<div class="loading" ref={( el ) => loadingBlackout = el}>
109
137
<Switch>
···
114
142
<p>Loading App...</p>
115
143
</Match>
116
144
</Switch>
145
+
</div>
146
+
147
+
<div class="confirmation-box" ref={( el ) => confirmationBox = el}>
148
+
<div class="confirmation-box-container">
149
+
{ confirmationBoxText() }<br /><br />
150
+
151
+
<div class="button-danger" onClick={() => { confirmationBoxCallback(); setConfirmationBoxText('') }}>Confirm</div>
152
+
<div class="button" onClick={() => setConfirmationBoxText('') }>Deny</div>
153
+
</div>
117
154
</div>
118
155
</div>
119
156
);
+65
-4
src/Components/PhotoList.tsx
+65
-4
src/Components/PhotoList.tsx
···
1
1
import { createEffect, onMount } from "solid-js";
2
2
import { invoke } from '@tauri-apps/api/tauri';
3
-
import { listen } from '@tauri-apps/api/event'
3
+
import { listen } from '@tauri-apps/api/event';
4
4
5
5
import anime from "animejs";
6
6
···
11
11
12
12
class PhotoListProps{
13
13
setCurrentPhotoView!: ( view: any ) => any;
14
+
currentPhotoView!: () => any;
14
15
photoNavChoice!: () => string;
15
16
setPhotoNavChoice!: ( view: any ) => any;
17
+
setConfirmationBox!: ( text: string, cb: () => void ) => void;
16
18
}
17
19
18
20
let PhotoList = ( props: PhotoListProps ) => {
···
32
34
let scroll: number = 0;
33
35
let targetScroll: number = 0;
34
36
37
+
let quitRender: boolean = false;
38
+
35
39
class PhotoMetadata{
36
40
width!: number;
37
41
height!: number;
···
43
47
path: string;
44
48
loaded: boolean = false;
45
49
loading: boolean = false;
50
+
metaLoaded: boolean = false;
46
51
image?: HTMLCanvasElement;
47
52
imageEl?: HTMLImageElement;
48
53
width?: number;
···
68
73
}
69
74
70
75
loadImage(){
71
-
if(this.loading || this.loaded || imagesLoading >= MAX_IMAGE_LOAD)return;
76
+
if(this.loading || this.loaded || !this.metaLoaded || imagesLoading >= MAX_IMAGE_LOAD)return;
72
77
this.loading = true;
73
78
74
79
imagesLoading++;
···
76
81
this.image = document.createElement('canvas');
77
82
78
83
this.imageEl = document.createElement('img');
84
+
this.imageEl.crossOrigin = 'anonymous';
79
85
this.imageEl.src = 'https://photo.localhost/' + this.path;
80
86
81
87
this.imageEl.onload = () => {
···
114
120
})
115
121
116
122
let render = () => {
117
-
requestAnimationFrame(render);
123
+
if(!quitRender)
124
+
requestAnimationFrame(render);
125
+
else
126
+
return quitRender = false;
118
127
119
128
if(!ctx)return;
120
129
ctx.clearRect(0, 0, photoContainer.width, photoContainer.height);
···
237
246
amountLoaded++;
238
247
239
248
photoMetaDataLoadingBar.style.width = (amountLoaded / photos.length) * 100 + '%';
249
+
photo.metaLoaded = true;
240
250
241
251
if(amountLoaded / photos.length === 1){
242
252
render();
···
251
261
photoMetaDataLoadingContainer.style.display = 'none';
252
262
}
253
263
})
264
+
265
+
anime({
266
+
targets: '.reload-photos',
267
+
opacity: 1,
268
+
duration: 150,
269
+
easing: 'easeInOutQuad'
270
+
})
254
271
}
255
272
})
256
273
274
+
listen('photo_create', ( event: any ) => {
275
+
console.log(event);
276
+
277
+
let photo = new Photo(event.payload);
278
+
photos.splice(0, 0, photo);
279
+
})
280
+
281
+
listen('photo_remove', ( event: any ) => {
282
+
photos = photos.filter(x => x.path !== event.payload);
283
+
284
+
if(event.payload === props.currentPhotoView().path){
285
+
currentPhotoIndex = -1;
286
+
props.setCurrentPhotoView(null);
287
+
}
288
+
})
289
+
290
+
let reloadPhotos = () => {
291
+
photoTreeLoadingContainer.style.opacity = '1';
292
+
photoTreeLoadingContainer.style.height = '100%';
293
+
photoTreeLoadingContainer.style.display = 'flex';
294
+
295
+
photoMetaDataLoadingContainer.style.opacity = '1';
296
+
photoMetaDataLoadingContainer.style.height = '100%';
297
+
photoMetaDataLoadingContainer.style.display = 'flex';
298
+
299
+
photoMetaDataLoadingBar.style.width = '0%';
300
+
quitRender = true;
301
+
302
+
amountLoaded = 0;
303
+
scroll = 0;
304
+
photos = [];
305
+
306
+
anime({
307
+
targets: '.reload-photos',
308
+
opacity: 0,
309
+
duration: 150,
310
+
easing: 'easeInOutQuad'
311
+
})
312
+
313
+
invoke('load_photos');
314
+
}
315
+
257
316
let loadPhotos = async () => {
258
317
invoke('load_photos')
259
318
···
287
346
288
347
if(targetScroll < 0)
289
348
targetScroll = 0;
290
-
})
349
+
});
291
350
292
351
photoContainer.width = window.innerWidth;
293
352
photoContainer.height = window.innerHeight;
···
323
382
<div class="loading-bar"><div class="loading-bar-inner" ref={( el ) => photoMetaDataLoadingBar = el}></div></div>
324
383
</div>
325
384
</div>
385
+
386
+
<div class="reload-photos" onClick={() => props.setConfirmationBox("Are you sure you want to reload all photos? This can cause the application to slow down while it is loading...", reloadPhotos)}><i class="fa-solid fa-arrows-rotate"></i></div>
326
387
327
388
<canvas class="photo-container" ref={( el ) => photoContainer = el}></canvas>
328
389
</div>
+80
-1
src/Components/PhotoViewer.tsx
+80
-1
src/Components/PhotoViewer.tsx
···
1
1
import { createEffect, onMount } from "solid-js";
2
+
import { invoke } from '@tauri-apps/api/tauri';
2
3
import anime from 'animejs';
3
4
4
5
class PhotoViewerProps{
5
6
currentPhotoView!: () => any;
6
7
setCurrentPhotoView!: ( view: any ) => any;
7
8
setPhotoNavChoice!: ( view: any ) => any;
9
+
setConfirmationBox!: ( text: string, cb: () => void ) => void;
8
10
}
9
11
10
12
let PhotoViewer = ( props: PhotoViewerProps ) => {
···
44
46
targets: '.navbar',
45
47
top: '-50px'
46
48
})
47
-
49
+
50
+
anime.set('.prev-button', { left: '-50px', top: '50%' });
51
+
anime.set('.next-button', { right: '-50px', top: '50%' });
52
+
53
+
anime({ targets: '.prev-button', left: '0', easing: 'easeInOutQuad', duration: 100 });
54
+
anime({ targets: '.next-button', right: '0', easing: 'easeInOutQuad', duration: 100 });
55
+
48
56
window.CloseAllPopups.forEach(p => p());
49
57
} else if(!photo && isOpen){
50
58
anime({
···
63
71
})
64
72
65
73
window.CloseAllPopups.forEach(p => p());
74
+
75
+
anime({ targets: '.prev-button', top: '75%', easing: 'easeInOutQuad', duration: 100 });
76
+
anime({ targets: '.next-button', top: '75%', easing: 'easeInOutQuad', duration: 100 });
66
77
}
67
78
68
79
isOpen = photo != null;
···
76
87
77
88
<div class="prev-button" onClick={() => props.setPhotoNavChoice('prev')}><i class="fa-solid fa-arrow-left"></i></div>
78
89
<div class="next-button" onClick={() => props.setPhotoNavChoice('next')}><i class="fa-solid fa-arrow-right"></i></div>
90
+
91
+
<div class="control-buttons">
92
+
<div class="viewer-button"
93
+
onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })}
94
+
onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })}
95
+
onClick={() => {
96
+
let canvas = document.createElement('canvas');
97
+
let ctx = canvas.getContext('2d')!;
98
+
99
+
canvas.width = props.currentPhotoView().width;
100
+
canvas.height = props.currentPhotoView().height;
101
+
102
+
ctx.drawImage(props.currentPhotoView().imageEl, 0, 0);
103
+
104
+
canvas.toBlob(( blob ) => {
105
+
navigator.clipboard.write([
106
+
new ClipboardItem({
107
+
'image/png': blob!
108
+
})
109
+
]);
110
+
111
+
canvas.remove();
112
+
113
+
anime.set('.copy-notif', { translateX: '-50%', translateY: '-100px' });
114
+
anime({
115
+
targets: '.copy-notif',
116
+
opacity: 1,
117
+
translateY: '0px'
118
+
});
119
+
120
+
setTimeout(() => {
121
+
anime({
122
+
targets: '.copy-notif',
123
+
opacity: 0,
124
+
translateY: '-100px'
125
+
});
126
+
}, 2000);
127
+
});
128
+
}}
129
+
>
130
+
<i class="fa-solid fa-copy"></i>
131
+
</div>
132
+
<div class="viewer-button"
133
+
onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })}
134
+
onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })}
135
+
>
136
+
<i class="fa-solid fa-info"></i>
137
+
</div>
138
+
<div class="viewer-button"
139
+
onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })}
140
+
onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })}
141
+
>
142
+
<i class="fa-solid fa-users"></i>
143
+
</div>
144
+
<div class="viewer-button"
145
+
onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })}
146
+
onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })}
147
+
>
148
+
<i class="fa-solid fa-file"></i>
149
+
</div>
150
+
<div class="viewer-button"
151
+
onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })}
152
+
onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })}
153
+
onClick={() => props.setConfirmationBox("Are you sure you want to delete this photo?", () => { invoke("delete_photo", { path: props.currentPhotoView().path }); })}
154
+
>
155
+
<i class="fa-solid fa-trash"></i>
156
+
</div>
157
+
</div>
79
158
</div>
80
159
)
81
160
}
+103
src/styles.css
+103
src/styles.css
···
197
197
user-select: none;
198
198
cursor: pointer;
199
199
z-index: 7;
200
+
box-shadow: #0008 0 0 10px;
200
201
}
201
202
202
203
.viewer-close{
···
253
254
254
255
.next-button:hover{
255
256
background: rgba(255, 255, 255, 0.349);
257
+
}
258
+
259
+
.reload-photos{
260
+
position: fixed;
261
+
top: 70px;
262
+
right: 20px;
263
+
color: white;
264
+
user-select: none;
265
+
cursor: pointer;
266
+
opacity: 0;
267
+
}
268
+
269
+
.confirmation-box{
270
+
position: fixed;
271
+
top: 0;
272
+
left: 0;
273
+
width: 100%;
274
+
height: 100%;
275
+
z-index: 15;
276
+
background: #0005;
277
+
transition: 0.25s;
278
+
backdrop-filter: blur(10px);
279
+
}
280
+
281
+
.confirmation-box-container{
282
+
position: fixed;
283
+
top: 50%;
284
+
left: 50%;
285
+
transform: translate(-50%, -50%);
286
+
color: white;
287
+
text-align: center;
288
+
background: #9995;
289
+
padding: 10px;
290
+
width: 60%;
291
+
border-radius: 10px;
292
+
box-shadow: #000 0 0 10px;
293
+
font-size: 18px;
294
+
backdrop-filter: blur(10px);
295
+
}
296
+
297
+
.button-danger{
298
+
display: inline-block;
299
+
backdrop-filter: blur(10px);
300
+
padding: 10px;
301
+
background: rgba(255, 0, 0, 0.333);
302
+
box-shadow: #0005 inset 0 0 10px;
303
+
border-radius: 50px;
304
+
margin: 0 10px;
305
+
cursor: pointer;
306
+
user-select: none;
307
+
width: 200px;
308
+
transition: 0.25s;
309
+
}
310
+
311
+
.button{
312
+
display: inline-block;
313
+
padding: 10px;
314
+
backdrop-filter: blur(10px);
315
+
background: #9995;
316
+
box-shadow: #0005 inset 0 0 10px;
317
+
border-radius: 50px;
318
+
margin: 0 10px;
319
+
cursor: pointer;
320
+
user-select: none;
321
+
width: 200px;
322
+
transition: 0.25s;
323
+
}
324
+
325
+
.button:hover{
326
+
box-shadow: #000a inset 0 0 10px;
327
+
}
328
+
329
+
.button-danger:hover{
330
+
box-shadow: #000a inset 0 0 10px;
331
+
}
332
+
333
+
.control-buttons{
334
+
position: fixed;
335
+
bottom: 10px;
336
+
left: 50%;
337
+
transform: translateX(-50%);
338
+
display: flex;
339
+
}
340
+
341
+
.control-buttons div{
342
+
margin: 0 20px;
343
+
}
344
+
345
+
.copy-notif{
346
+
position: fixed;
347
+
top: 40px;
348
+
left: 50%;
349
+
color: white;
350
+
transform: translateX(-50%) translateY(-100px);
351
+
background: #8885;
352
+
padding: 10px 40px;
353
+
backdrop-filter: blur(10px);
354
+
border-radius: 50px;
355
+
box-shadow: #000 0 0 10px;
356
+
z-index: 12;
357
+
opacity: 0;
358
+
pointer-events: none;
256
359
}