Skip to content

Commit 10a38a2

Browse files
committed
feat: add os level notifications
- use tauri api to add OS level notifications - fix scripts bug in package.json
1 parent 60bdff8 commit 10a38a2

File tree

11 files changed

+183
-102
lines changed

11 files changed

+183
-102
lines changed

GUI/ETVR/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
"build": "vite build",
1111
"start": "vite start",
1212
"serve": "vite preview",
13-
"tauri": "yarn tauri",
14-
"tauri:dev": "yarn tauri dev",
15-
"tauri:build": "yarn tauri build",
13+
"tauri": "tauri",
14+
"tauri:dev": "tauri dev",
15+
"tauri:build": "tauri build",
1616
"docs": "jsdoc -c jsdoc.conf.json",
1717
"lint": "eslint --ext .js,.ts,.jsx,.tsx src",
1818
"format": "yarn run lint --fix & yarn prettier --write \"src/**/*.{js,jsx,ts,tsx}\""

GUI/ETVR/src-tauri/Cargo.lock

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

GUI/ETVR/src-tauri/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ tauri-build = { version = "1.1.1", features = [] }
1717
[dependencies]
1818
serde_json = "1.0"
1919
serde = { version = "1.0", features = ["derive"] }
20-
tauri = { version = "1.1.1", features = ["fs-create-dir", "fs-read-dir", "fs-write-file", "icon-ico", "notification-all", "os-all", "path-all", "process-relaunch", "shell-open", "system-tray", "window-center", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-decorations", "window-set-focus", "window-set-fullscreen", "window-set-size", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
20+
tauri = { version = "1.1.1", features = ["fs-create-dir", "fs-read-dir", "fs-write-file", "http-all", "icon-ico", "notification-all", "os-all", "path-all", "process-relaunch", "shell-open", "system-tray", "window-center", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-decorations", "window-set-focus", "window-set-fullscreen", "window-set-size", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
2121
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/tauri-plugin-window-state/", branch = "dev" }
2222
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/tauri-plugin-single-instance/", branch = "dev" }
2323
tauri-plugin-store = { git = "https://github.com/tauri-apps/tauri-plugin-store/", rev = "9bd993a" }
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
#![allow(dead_code, unused_imports, unused_variables)]
1+
/* #![allow(dead_code, unused_imports, unused_variables) ]*/
22
#![cfg_attr(
3-
all(not(debug_assertions), target_os = "windows"),
4-
windows_subsystem = "windows"
3+
all(not(debug_assertions), target_os = "windows"),
4+
windows_subsystem = "windows"
55
)]
66

7-
// 1. Grab the mDNS address of the device(s) you want to query
8-
// 2. Create a new RESTClient instance for each device
9-
// 3. Start a new thread for each RESTClient instance
10-
// 4. Each thread will poll the device for new data
11-
// 5. Each thread will send the new data to the main thread
12-
// 6. The main thread will update the UI with the new data
13-
147
use crate::modules::m_dnsquery;
158
use log::{debug, error, info, warn};
169
use reqwest::Client;
1710
use serde::Deserialize;
1811
use std::collections::hash_map::HashMap;
19-
use std::sync::{Arc, Mutex};
2012

2113
/// A struct to hold the REST client
2214
/// ## Fields
@@ -25,97 +17,97 @@ use std::sync::{Arc, Mutex};
2517
/// - `name`: the name of the url to query
2618
/// - `data`: a hashmap of the data returned from the api
2719
pub struct RESTClient {
28-
http_client: Client,
29-
base_url: String,
30-
method: String,
20+
http_client: Client,
21+
base_url: String,
22+
method: String,
3123
}
3224

3325
/// A function to create a new RESTClient instance
3426
/// ## Arguments
3527
/// - `base_url` The base url of the api to query
3628
impl RESTClient {
37-
pub fn new(base_url: String, method: String) -> Self {
38-
Self {
39-
http_client: Client::new(),
40-
base_url,
41-
method,
42-
}
29+
pub fn new(base_url: String, method: String) -> Self {
30+
Self {
31+
http_client: Client::new(),
32+
base_url,
33+
method,
4334
}
35+
}
4436
}
4537

4638
pub async fn request(rest_client: &RESTClient) -> Result<String, String> {
47-
info!("Making REST request");
39+
info!("Making REST request");
4840

49-
let response: String;
50-
let response = match rest_client.method.as_str() {
51-
"GET" => {
52-
response = rest_client
53-
.http_client
54-
.get(&rest_client.base_url)
55-
.send()
56-
.await
57-
.map_err(|e| e.to_string())?
58-
.text()
59-
.await
60-
.map_err(|e| e.to_string())?;
61-
response
62-
}
63-
"POST" => {
64-
response = rest_client
65-
.http_client
66-
.post(&rest_client.base_url)
67-
.send()
68-
.await
69-
.map_err(|e| e.to_string())?
70-
.text()
71-
.await
72-
.map_err(|e| e.to_string())?;
73-
response
74-
}
75-
_ => {
76-
error!("Invalid method");
77-
return Err("Invalid method".to_string());
78-
}
79-
};
41+
let response: String;
42+
let response = match rest_client.method.as_str() {
43+
"GET" => {
44+
response = rest_client
45+
.http_client
46+
.get(&rest_client.base_url)
47+
.send()
48+
.await
49+
.map_err(|e| e.to_string())?
50+
.text()
51+
.await
52+
.map_err(|e| e.to_string())?;
53+
response
54+
}
55+
"POST" => {
56+
response = rest_client
57+
.http_client
58+
.post(&rest_client.base_url)
59+
.send()
60+
.await
61+
.map_err(|e| e.to_string())?
62+
.text()
63+
.await
64+
.map_err(|e| e.to_string())?;
65+
response
66+
}
67+
_ => {
68+
error!("Invalid method");
69+
return Err("Invalid method".to_string());
70+
}
71+
};
8072

81-
Ok(response)
73+
Ok(response)
8274
}
8375

8476
/// A function to run a REST Client and create a new RESTClient instance for each device found
8577
/// ## Arguments
8678
/// - `endpoint` The endpoint to query for
8779
/// - `device_name` The name of the device to query
8880
pub async fn run_rest_client(
89-
endpoint: String,
90-
device_name: String,
91-
method: String,
81+
endpoint: String,
82+
device_name: String,
83+
method: String,
9284
) -> Result<String, String> {
93-
info!("Starting REST client");
94-
// read the json config file
95-
let data = std::fs::read_to_string("config/config.json").expect("Unable to read config file");
96-
// parse the json config file
97-
let config: serde_json::Value = serde_json::from_str(&data).map_err(|e| e.to_string())?;
98-
debug!("Current Config: {:?}", config);
99-
let mut request_response: String = String::new();
100-
let mut url = config["urls"][device_name].as_str();
101-
let full_url_result = match url {
102-
Some(url) => url,
103-
None => {
104-
error!("Unable to get url");
105-
url = Some("");
106-
return Err(format!("Unable to get url: {:?}", url));
107-
}
108-
};
109-
let full_url = format!("{}{}", full_url_result, endpoint);
110-
//info!("Full url: {}", full_url);
111-
let rest_client = RESTClient::new(full_url, method);
112-
let request_result = request(&rest_client).await;
113-
match request_result {
114-
Ok(response) => {
115-
request_response = response;
116-
println!("Request response: {:?}", request_response);
117-
}
118-
Err(e) => println!("Request failed: {}", e),
85+
info!("Starting REST client");
86+
// read the json config file
87+
let data = std::fs::read_to_string("config/config.json").expect("Unable to read config file");
88+
// parse the json config file
89+
let config: serde_json::Value = serde_json::from_str(&data).map_err(|e| e.to_string())?;
90+
debug!("Current Config: {:?}", config);
91+
let mut request_response: String = String::new();
92+
let mut url = config["urls"][device_name].as_str();
93+
let full_url_result = match url {
94+
Some(url) => url,
95+
None => {
96+
error!("Unable to get url");
97+
url = Some("");
98+
return Err(format!("Unable to get url: {:?}", url));
99+
}
100+
};
101+
let full_url = format!("{}{}", full_url_result, endpoint);
102+
//info!("Full url: {}", full_url);
103+
let rest_client = RESTClient::new(full_url, method);
104+
let request_result = request(&rest_client).await;
105+
match request_result {
106+
Ok(response) => {
107+
request_response = response;
108+
println!("Request response: {:?}", request_response);
119109
}
120-
Ok(request_response)
110+
Err(e) => println!("Request failed: {}", e),
111+
}
112+
Ok(request_response)
121113
}

GUI/ETVR/src-tauri/src/modules/websocket/mod.rs

Whitespace-only changes.

GUI/ETVR/src-tauri/tauri.conf.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
"all": false
4848
},
4949
"http": {
50-
"all": false,
51-
"request": false,
50+
"all": true,
51+
"request": true,
5252
"scope": []
5353
},
5454
"notification": {

GUI/ETVR/src/components/Notifications/index.tsx

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
import { isPermissionGranted, requestPermission } from '@tauri-apps/api/notification'
12
import { Toaster, ToasterStore, Transition, useToaster } from 'solid-headless'
23
import { createEffect, createSignal, For, onCleanup } from 'solid-js'
34
import CustomToast from './CustomToast'
45
import { notifications } from '@src/store/ui/selectors'
56

7+
let permissionGranted = await isPermissionGranted()
8+
if (!permissionGranted) {
9+
const permission = await requestPermission()
10+
permissionGranted = permission === 'granted'
11+
}
12+
613
const ToastNotificationWindow = () => {
714
const notifs = useToaster(notifications() as ToasterStore<string>)
815

@@ -16,16 +23,18 @@ const ToastNotificationWindow = () => {
1623
}
1724

1825
createEffect(() => {
19-
if (notifs().length > 0) {
20-
setIsOpen(true)
26+
if (permissionGranted) {
27+
if (notifs().length > 0) {
28+
setIsOpen(true)
29+
}
30+
const timeout = setTimeout(() => {
31+
closeNotifs()
32+
console.log('[Notifications] Closed - Cleaned up')
33+
}, 5000)
34+
onCleanup(() => {
35+
clearTimeout(timeout)
36+
})
2137
}
22-
const timeout = setTimeout(() => {
23-
closeNotifs()
24-
console.log('[Notifications] Closed - Cleaned up')
25-
}, 5000)
26-
onCleanup(() => {
27-
clearTimeout(timeout)
28-
})
2938
})
3039

3140
return (

GUI/ETVR/src/pages/Settings/index.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import Button from '@components/Button'
22
import { addNotification } from '@src/store/ui/ui'
3+
import { ENotificationAction } from '@utils/enums'
34

45
const SettingsPage = () => {
56
return (
67
<div class="pb-[5rem] h-[95%] xl:h-[100%] xl:pb-[1rem] grow flex-col pt-6 py-6 px-8">
78
<Button
89
color="blue"
910
onClick={() =>
10-
addNotification(`This toast is created on ${new Date().toTimeString()}`)
11+
addNotification(
12+
{
13+
title: 'Test',
14+
message: `This toast is created on ${new Date().toTimeString()}`,
15+
},
16+
ENotificationAction.OS,
17+
)
1118
}
1219
text="Add Notification"
1320
/>

GUI/ETVR/src/store/ui/ui.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { sendNotification } from '@tauri-apps/api/notification'
12
import { ToasterStore } from 'solid-headless'
23
import { createMemo, JSXElement } from 'solid-js'
34
import { createStore, produce } from 'solid-js/store'
4-
import { loaderType, POPOVER_ID } from '@src/utils/enums'
5+
import { loaderType, POPOVER_ID, ENotificationAction } from '@src/utils/enums'
6+
import { NotificationsType } from '@src/utils/utils'
57

68
interface IMenuOpen {
79
x: number
@@ -30,6 +32,16 @@ export interface IUiStore {
3032
displayMode: POPOVER_ID
3133
}
3234

35+
export interface INotificationAction {
36+
callbackOS(): void
37+
callbackApp(): void
38+
}
39+
40+
export interface INotifictionMessage {
41+
title: string
42+
message: string
43+
}
44+
3345
export const defaultState = {
3446
loader: { [loaderType.MDNS_CONNECTING]: false, [loaderType.REST_CLIENT]: false },
3547
connecting: false,
@@ -85,10 +97,25 @@ export const notify = (title: string, body: string | undefined) => {
8597
new Notification(title, { body: body || '' })
8698
}
8799

88-
export const addNotification = (message: string) => {
100+
export const addNotification = (
101+
notification: INotifictionMessage,
102+
actionType: ENotificationAction,
103+
) => {
89104
setState(
90105
produce((s) => {
91-
s.notifications?.create(message)
106+
const { title, message } = notification
107+
const notificationAction = NotificationsType(actionType, {
108+
callbackOS: () => {
109+
sendNotification({
110+
title,
111+
body: message,
112+
})
113+
},
114+
callbackApp: () => {
115+
s.notifications?.create(message)
116+
},
117+
})
118+
return notificationAction
92119
}),
93120
)
94121
}

GUI/ETVR/src/utils/enums.ts

+12
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,15 @@ export enum loaderType {
1414
MDNS_CONNECTING = 'MDNS_CONNECTING',
1515
REST_CLIENT = 'REST_CLIENT',
1616
}
17+
18+
export enum ENotificationType {
19+
ERROR = 'ERROR',
20+
SUCCESS = 'SUCCESS',
21+
INFO = 'INFO',
22+
WARNING = 'WARNING',
23+
}
24+
25+
export enum ENotificationAction {
26+
OS = 'OS',
27+
APP = 'APP',
28+
}

0 commit comments

Comments
 (0)