-1
public/assets/icons/arrow-right-solid-full.svg
-1
public/assets/icons/arrow-right-solid-full.svg
···
1
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#ffffff" d="M566.6 342.6C579.1 330.1 579.1 309.8 566.6 297.3L406.6 137.3C394.1 124.8 373.8 124.8 361.3 137.3C348.8 149.8 348.8 170.1 361.3 182.6L466.7 288L96 288C78.3 288 64 302.3 64 320C64 337.7 78.3 352 96 352L466.7 352L361.3 457.4C348.8 469.9 348.8 490.2 361.3 502.7C373.8 515.2 394.1 515.2 406.6 502.7L566.6 342.7z"/></svg>
+1
public/assets/icons/trash-can-solid-full.svg
+1
public/assets/icons/trash-can-solid-full.svg
···
1
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#ffffff" d="M232.7 69.9C237.1 56.8 249.3 48 263.1 48L377 48C390.8 48 403 56.8 407.4 69.9L416 96L512 96C529.7 96 544 110.3 544 128C544 145.7 529.7 160 512 160L128 160C110.3 160 96 145.7 96 128C96 110.3 110.3 96 128 96L224 96L232.7 69.9zM128 208L512 208L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 208zM216 272C202.7 272 192 282.7 192 296L192 488C192 501.3 202.7 512 216 512C229.3 512 240 501.3 240 488L240 296C240 282.7 229.3 272 216 272zM320 272C306.7 272 296 282.7 296 296L296 488C296 501.3 306.7 512 320 512C333.3 512 344 501.3 344 488L344 296C344 282.7 333.3 272 320 272zM424 272C410.7 272 400 282.7 400 296L400 488C400 501.3 410.7 512 424 512C437.3 512 448 501.3 448 488L448 296C448 282.7 437.3 272 424 272z"/></svg>
+9
src-tauri/src/frontend_calls/get_addresses.rs
+9
src-tauri/src/frontend_calls/get_addresses.rs
+1
src-tauri/src/frontend_calls/mod.rs
+1
src-tauri/src/frontend_calls/mod.rs
···
1
+
pub mod get_addresses;
+10
-3
src-tauri/src/lib.rs
+10
-3
src-tauri/src/lib.rs
···
1
-
use std::fs;
1
+
use std::{fs, sync::Mutex};
2
2
3
3
use sqlx::{ migrate::MigrateDatabase, Sqlite, SqlitePool };
4
+
use frontend_calls::*;
4
5
5
6
use crate::{ setup::setup, utils::config::Config };
6
7
8
+
mod frontend_calls;
7
9
mod setup;
8
10
mod utils;
9
11
mod osc;
···
33
35
let pool = SqlitePool::connect(db_file.to_str().unwrap()).await.unwrap();
34
36
let conf = Config::new(conf_file);
35
37
38
+
static ADDRESSES: Mutex<Vec<String>> = Mutex::new(Vec::new());
39
+
36
40
tauri::Builder::default()
37
41
.plugin(tauri_plugin_opener::init())
38
-
.invoke_handler(tauri::generate_handler![])
42
+
.invoke_handler(tauri::generate_handler![
43
+
get_addresses::get_addresses
44
+
])
39
45
.manage(pool)
40
46
.manage(conf)
47
+
.manage(&ADDRESSES)
41
48
.setup(| app | {
42
-
setup(app);
49
+
setup(app, &ADDRESSES);
43
50
44
51
Ok(())
45
52
})
+8
-4
src-tauri/src/setup.rs
+8
-4
src-tauri/src/setup.rs
···
1
-
use std::sync;
1
+
use std::sync::{self, Mutex};
2
2
3
3
use tauri::{ App, Emitter, Manager };
4
4
5
-
use crate::osc;
5
+
use crate::{ osc };
6
6
7
-
pub fn setup( app: &mut App ){
7
+
pub fn setup( app: &mut App, addresses: &'static Mutex<Vec<String>> ){
8
8
let window = app.get_webview_window("main").unwrap();
9
9
10
10
let ( sender, receiver ) = sync::mpsc::channel();
···
17
17
loop {
18
18
let message = receiver.recv().unwrap();
19
19
20
+
window.emit("osc-message", &message).unwrap();
20
21
22
+
let addr = message.address.clone();
23
+
dbg!(&addr);
21
24
22
-
window.emit("osc-message", message).unwrap();
25
+
let mut addresses = addresses.lock().unwrap();
26
+
if !addresses.contains(&addr){ addresses.push(addr); }
23
27
}
24
28
});
25
29
}
+1
-1
src-tauri/src/utils/config.rs
+1
-1
src-tauri/src/utils/config.rs
+26
-1
src/App.css
+26
-1
src/App.css
···
5
5
6
6
* {
7
7
box-sizing: border-box;
8
+
-webkit-user-select: none;
8
9
}
9
10
10
11
body{
···
23
24
h1, h2, h3, h4, h5, h6, p{
24
25
margin: 0;
25
26
font-weight: 500;
27
+
}
28
+
29
+
span{
30
+
-webkit-user-select: auto;
26
31
}
27
32
28
33
div[app-carousel]{
···
78
83
display: flex;
79
84
}
80
85
81
-
div[app-col] > div{
86
+
div[app-col-50]{
87
+
display: flex;
88
+
}
89
+
90
+
div[app-col-50] > div{
82
91
width: 50%;
92
+
}
93
+
94
+
div[app-icon]{
95
+
display: flex;
96
+
justify-content: center;
97
+
align-items: center;
98
+
width: 30px;
99
+
height: 30px;
100
+
user-select: none;
101
+
-webkit-user-select: none;
102
+
cursor: pointer;
103
+
transition: 0.1s;
104
+
}
105
+
106
+
div[app-icon]:hover{
107
+
opacity: 0.75;
83
108
}
+1
-6
src/App.tsx
+1
-6
src/App.tsx
···
1
1
import "./App.css";
2
2
3
-
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
4
-
import { listen } from "@tauri-apps/api/event";
3
+
import { createEffect, createSignal } from "solid-js";
5
4
6
5
import { Sidebar } from "./Components/Sidebar";
7
6
import { Actions } from "./Components/Actions";
···
13
12
let App = () => {
14
13
let [ page, setPage ] = createSignal(0);
15
14
let carousel!: HTMLDivElement;
16
-
17
-
onMount(async () => {
18
-
19
-
});
20
15
21
16
createEffect(() => {
22
17
let pagenum = page();
+13
src/Components/Actions.css
+13
src/Components/Actions.css
+27
-4
src/Components/Actions.tsx
+27
-4
src/Components/Actions.tsx
···
1
+
import { Accessor, createSignal, For, Setter } from 'solid-js';
1
2
import './Actions.css';
3
+
import { TriggerEl } from './TriggerEl';
4
+
5
+
export interface Trigger{
6
+
address: string,
7
+
actions: Accessor<any>,
8
+
setActions: Setter<any>
9
+
}
2
10
3
11
export let Actions = () => {
12
+
let [ triggers, setTriggers ] = createSignal<Trigger[]>([], { equals: false });
13
+
4
14
return (
5
15
<div app-page>
6
16
<div app-col>
7
-
<div><h1>Actions</h1></div>
8
-
<div app-button style={{ width: 'fit-content', "margin-left": '50%' }}>+</div>
17
+
<div style={{ width: '100%' }}><h1>Actions</h1></div>
18
+
<div app-button style={{ width: 'fit-content', "margin-left": '50%' }} onClick={() => {
19
+
let trig = triggers();
20
+
let [ actions, setActions ] = createSignal([], { equals: false });
21
+
22
+
trig.push({ address: '', actions, setActions });
23
+
setTriggers(trig);
24
+
}}>+</div>
9
25
</div>
10
26
11
-
<div>
27
+
<For each={triggers()}>
28
+
{ ( item ) => <TriggerEl
29
+
trigger={item}
30
+
onDelete={() => {
31
+
let trig = triggers();
32
+
trig = trig.filter(x => x !== item);
12
33
13
-
</div>
34
+
setTriggers(trig);
35
+
}} /> }
36
+
</For>
14
37
</div>
15
38
)
16
39
}
+2
-2
src/Components/Debug.tsx
+2
-2
src/Components/Debug.tsx
···
50
50
el.style.boxShadow = '#00ccff 0 0 10px';
51
51
debugContainer.insertBefore(el, debugContainer.firstChild);
52
52
53
-
el.innerHTML = `<div>${ ev.payload.address }</div><div>${ formatValuesForDebug(ev.payload.values) }</div>`;
53
+
el.innerHTML = `<div><span>${ ev.payload.address }</span></div><div>${ formatValuesForDebug(ev.payload.values) }</div>`;
54
54
setTimeout(() => { el.style.boxShadow = '#00ccff 0 0 0px'; })
55
55
} else{
56
-
el = <div app-debug-el app-col><div>{ ev.payload.address }</div><div>{ formatValuesForDebug(ev.payload.values) }</div></div> as Node;
56
+
el = <div app-debug-el app-col-50><div><span>{ ev.payload.address }</span></div><div>{ formatValuesForDebug(ev.payload.values) }</div></div> as Node;
57
57
58
58
el.style.boxShadow = '#00ccff 0 0 10px';
59
59
debugContainer.insertBefore(el, debugContainer.firstChild);
+35
src/Components/TextInput.css
+35
src/Components/TextInput.css
···
1
+
input[type="text"]{
2
+
outline: none;
3
+
background: none;
4
+
border: none;
5
+
border-bottom: 2px solid #525252;
6
+
font-size: 15px;
7
+
font-family: Rubik, 'Courier New';
8
+
color: #fff;
9
+
width: 400px;
10
+
}
11
+
12
+
div[input-dropdown]{
13
+
position: absolute;
14
+
width: fit-content;
15
+
16
+
margin-top: 5px;
17
+
padding: 10px;
18
+
float: left;
19
+
border-radius: 5px;
20
+
21
+
background: #525252;
22
+
z-index: 50;
23
+
}
24
+
25
+
div[input-dropdown] > div{
26
+
transition: 0.25s;
27
+
}
28
+
29
+
div[input-dropdown] > div:hover{
30
+
color: #aaa;
31
+
}
32
+
33
+
.suggestion-selected{
34
+
color: #aaa;
35
+
}
+95
src/Components/TextInput.tsx
+95
src/Components/TextInput.tsx
···
1
+
import './TextInput.css';
2
+
3
+
import { createSignal, For, Show } from "solid-js"
4
+
5
+
export interface TextInputProps{
6
+
placeholder: string,
7
+
value?: string,
8
+
requestSuggestions?: ( text: string ) => Promise<string[]>
9
+
}
10
+
11
+
export let TextInput = ( props: TextInputProps ) => {
12
+
let [ suggestionsOpen, setSuggestionsOpen ] = createSignal(false);
13
+
let [ suggestions, setSuggestions ] = createSignal<string[]>([])
14
+
15
+
let input!: HTMLInputElement;
16
+
17
+
let suggestionsContainer!: HTMLDivElement;
18
+
let suggestionsIndex = 0;
19
+
20
+
let onInput = async () => {
21
+
let s = null;
22
+
23
+
if(props.requestSuggestions){
24
+
s = await props.requestSuggestions(input.value);
25
+
26
+
if(s != suggestions()){
27
+
setSuggestions(s);
28
+
29
+
setSuggestionsOpen(s !== null && s.length > 0 && input.value.length > 0);
30
+
changeSelection(() => { suggestionsIndex = 0; });
31
+
}
32
+
}
33
+
}
34
+
35
+
let onKeyUp = ( ev: KeyboardEvent ) => {
36
+
switch(ev.key){
37
+
case 'ArrowDown':
38
+
changeSelection(() => {
39
+
suggestionsIndex++;
40
+
if(suggestionsIndex >= suggestionsContainer.children.length)suggestionsIndex = suggestionsContainer.children.length - 1;
41
+
});
42
+
break;
43
+
case 'ArrowUp':
44
+
changeSelection(() => {
45
+
suggestionsIndex--;
46
+
if(suggestionsIndex < 0)suggestionsIndex = 0;
47
+
});
48
+
break;
49
+
case 'Enter':
50
+
let currentDiv = suggestionsContainer.children[suggestionsIndex];
51
+
if(currentDiv)input.value = currentDiv.innerHTML;
52
+
53
+
setSuggestionsOpen(false);
54
+
break;
55
+
}
56
+
}
57
+
58
+
let changeSelection = ( cb: () => void ) => {
59
+
for(let child of suggestionsContainer.children)
60
+
child.classList.remove('suggestion-selected');
61
+
62
+
cb();
63
+
64
+
let currentDiv = suggestionsContainer.children[suggestionsIndex];
65
+
if(currentDiv)currentDiv.classList.add('suggestion-selected');
66
+
}
67
+
68
+
return (
69
+
<>
70
+
<div style={{ width: '100%' }}>
71
+
<input
72
+
style={{ width: '100%' }}
73
+
type="text"
74
+
placeholder={ props.placeholder }
75
+
value={ props.value || "" }
76
+
onInput={onInput}
77
+
onKeyUp={onKeyUp}
78
+
ref={input} />
79
+
80
+
<Show when={suggestionsOpen()}>
81
+
<div input-dropdown ref={suggestionsContainer}>
82
+
<For each={suggestions()}>
83
+
{ item => <div onClick={( el ) => {
84
+
let thisEl = el.target;
85
+
86
+
input.value = thisEl.innerHTML;
87
+
setSuggestionsOpen(false);
88
+
}}>{ item }</div> }
89
+
</For>
90
+
</div>
91
+
</Show>
92
+
</div>
93
+
</>
94
+
)
95
+
}
+56
src/Components/TriggerEl.tsx
+56
src/Components/TriggerEl.tsx
···
1
+
import { For } from "solid-js"
2
+
import { Trigger } from "./Actions"
3
+
import { TextInput } from "./TextInput"
4
+
import { invoke } from "@tauri-apps/api/core"
5
+
6
+
export interface TriggerElProps{
7
+
trigger: Trigger,
8
+
9
+
onDelete: () => void
10
+
}
11
+
12
+
export let TriggerEl = ( { trigger, onDelete }: TriggerElProps ) => {
13
+
let suggestOSCAddresses = async ( text: string ): Promise<string[]> => {
14
+
let addresses = await invoke<string[]>('get_addresses');
15
+
return addresses.filter(x => x.toLowerCase().includes(text.toLowerCase()));
16
+
}
17
+
18
+
return (
19
+
<div app-trigger-el>
20
+
<div app-col>
21
+
OSC Address:
22
+
<div style={{ width: '400px', display: 'inline-block', "margin-left": '10px' }}>
23
+
<TextInput placeholder="/avatar/parameters/MyValue" value={ trigger.address } requestSuggestions={suggestOSCAddresses} />
24
+
</div>
25
+
<div app-icon style={{ 'margin-left': 'calc(100% - 545px)' }} onClick={onDelete}>
26
+
<img src="/assets/icons/trash-can-solid-full.svg" width="18" />
27
+
</div>
28
+
</div>
29
+
30
+
<br />
31
+
<For each={trigger.actions()}>
32
+
{ item => <div app-trigger-action app-col>
33
+
<div style={{ width: 'calc(100% - 40px)', height: '30px', display: 'flex', "justify-content": 'center', 'align-items': 'center' }}>
34
+
<TextInput placeholder="Search Actions..." />
35
+
</div>
36
+
<div app-icon style={{ width: '40px' }} onClick={() => {
37
+
let actions = trigger.actions();
38
+
actions = actions.filter(( x: any ) => x !== item);
39
+
40
+
trigger.setActions(actions);
41
+
}}>
42
+
<img src="/assets/icons/trash-can-solid-full.svg" width="18" />
43
+
</div>
44
+
</div> }
45
+
</For>
46
+
47
+
<br />
48
+
<div app-button onClick={() => {
49
+
let actions = trigger.actions();
50
+
actions.push({});
51
+
52
+
trigger.setActions(actions);
53
+
}}>Add Action +</div>
54
+
</div>
55
+
)
56
+
}