Compare commits

...

24 Commits
main ... mobile

Author SHA1 Message Date
emerald 4e52e95cde
add deps for mobile version 2022-12-19 11:11:19 -05:00
emerald 97bf38165f
make config saving more consistent 2022-12-17 17:15:02 -05:00
emerald 200f71e0ac
fix blink interval not being set correctly 2022-12-17 15:54:08 -05:00
emerald 36db69356c
limit minimum blink threshold 2022-12-17 15:26:28 -05:00
emerald 91ce120921
add js infrastructure for assigning animatinons to frames 2022-11-12 22:14:35 -05:00
emerald 79a31a161c
improve animation translations 2022-11-11 13:00:36 -05:00
emerald 5f7041ddc2
switch to js animations 2022-11-10 13:31:02 -05:00
emerald a51ae66019
fix config not saving
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-11-10 12:48:15 -05:00
emerald f20b32c986
update changelog
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/pr/build Pipeline was successful Details
ci/woodpecker/pr/publish Pipeline was successful Details
2022-10-21 02:14:57 -04:00
emerald 31bbf44171
add mic sensitivity to config 2022-10-21 02:03:18 -04:00
emerald 2489210068
add command to save config
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-20 14:03:59 -04:00
emerald c5c587ad3c
update changelog
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-19 14:27:23 -04:00
emerald 14a86f4b63
add 'new' button, add image loading icon
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-19 14:25:42 -04:00
emerald dbafaa77d6
update changelog
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-19 01:32:23 -04:00
emerald 72ef196223
add config loading 2022-10-19 01:30:58 -04:00
emerald 62253bbeb6
add bundle to gitignore 2022-10-18 23:53:57 -04:00
emerald 468cd79692
add install and uninstall recipes
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-18 14:06:53 -04:00
emerald 9e6e688a97 Update 'README.md'
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-17 18:47:34 -04:00
emerald f60fcf0556
let build pipeline build debug version
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-16 21:31:37 -04:00
emerald 1dd2eac1a0
let build pipeline run on dev branch
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-16 17:46:06 -04:00
emerald 8452a15001
bump version in tauri.conf 2022-10-16 17:42:48 -04:00
emerald 8433a128db
fix rpm deps 2022-10-16 11:13:10 -04:00
emerald 849e437ae5
skip bundling in build pipeline 2022-10-16 01:22:20 -04:00
emerald 990fcfbc79
update frontend deps 2022-10-16 01:17:13 -04:00
29 changed files with 2245 additions and 952 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules/
dist/
bundle/

11
.helix/languages.toml Normal file
View File

@ -0,0 +1,11 @@
[[language]]
name = "svelte"
auto-format = true
[[language]]
name = "typescript"
auto-format = true
[[language]]
name = 'toml'
auto-format = true

View File

@ -1,4 +1,4 @@
branches: main
branches: [main, dev]
pipeline:
build:
image: gitea.greenboi.me/emerald/cathode-build:latest
@ -7,4 +7,5 @@ pipeline:
event: [push, pull_request]
commands:
- npm ci
- npm run tauri build
- npm run build
- cd src-tauri && cargo build

View File

@ -6,11 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [v0.1.0]
### Added
- Load last selected ray file at launch
- Add `blink_interval` to config file
- Add `mic_sens` to config file
- Add `new` button to reset the loaded ray
- Add icon to indicate an image is being loaded
- Add shift-click to clear a frame
### Changed
- Loading rays at launch should be more consistent
### Fixed
- Fixed config not being respected on launch

View File

@ -1,4 +1,4 @@
FROM node
FROM node:bullseye
RUN apt update -yy && apt upgrade -yy
RUN apt install -yy libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
RUN apt install -yy libasound2-dev

View File

@ -1,4 +1,40 @@
# Cathode [![status-badge](https://ci.greenboi.me/api/badges/emerald/cathode/status.svg)](https://ci.greenboi.me/emerald/cathode)
a small app for PNG tubing. Think Veadotube-mini but completely FOSS.
Built with Tauri and Svelte.
Built with Tauri and Svelte.
## Installation
### Packages
There are a few prebuilt packages available [here](https://gitea.greenboi.me/emerald/-/packages/generic/cathode-tube/) for the latest stable release. They are built on Bullseye Debian, so should be compatible with most up to date systems. Debian derived distros will want the `.deb` file, and Fedora users will want the `.rpm` file. Download the correct file and install it with your package manager.
Alternatively, download the AppImage, which should work on any glibc linux distro at the cost of being a considerable larger file. Once you download the `.AppImage` file, give it execution permissions (eg: `chmod +x cathode-tube.AppImage`) and run it like a command or script.
### Building from source
#### Prerequisites
In order to build from source you will need a few things to get started
- Nodejs/npm (I recommend using [nvm](https://github.com/nvm-sh/nvm) for this)
- [Rust](https://rustup.rs/)
- Tauri's [development dependencies](https://tauri.app/v1/guides/getting-started/prerequisites#installing)
#### Building
Once all of these are installed, clone the repo and run
```
npm install
```
This will install everything needed to build the frontend, as well as the tauri cli. Building the project itself is then as simple as
```
npm run tauri build
```
This will build the frontend and backend, and bundle the `.deb` and `.AppImage` packages, found in `src-tauri/target/release/bundle`.
The binary itself (at `src-tauri/target/release/cathode-tube`) is all that is needed to run the program, so if neither bundle works for you simply copy the executable to somewhere in your path, or run
```
cargo install --path src-tauri
```
#### Just
If you have the [just](https://github.com/casey/just) command runner installed, as well as the other prerequisites, then you can run
```
just install
```
Which will build the project and install it to `/usr/bin`, along with the `.desktop` file and icons

View File

@ -13,8 +13,29 @@ log RUST_LOG:
build:
cargo tauri build
cd src-tauri
cargo generate-rpm
cp -r target/release/bundle ..
cp -r target/generate-rpm ../bundle
cd src-tauri && cargo generate-rpm
cp -r src-tauri/target/release/bundle bundle
cp -r src-tauri/target/generate-rpm bundle
install:
@cargo tauri build -b none
@echo Copying binary to /usr/bin/cathode...
@sudo cp src-tauri/target/release/cathode /usr/bin/cathode
@echo Installing desktop file...
@sudo cp src-tauri/cathode-tube.desktop /usr/share/applications/cathode-tube.desktop
@sudo cp src-tauri/application-cathode.xml /usr/share/mime/packages/application-cathode.xml
@echo Installing icons...
@sudo cp src-tauri/icons/128x128.png /usr/share/icons/hicolor/128x128/apps/cathode-tube.png
@sudo cp src-tauri/icons/32x32.png /usr/share/icons/hicolor/32x32/apps/cathode-tube.png
@sudo cp src-tauri/icons/128x128@2x.png /usr/share/icons/hicolor/256x256/apps/cathode-tube.png
@sudo update-desktop-database
uninstall:
@echo Removing cathode...
@sudo rm -f /usr/bin/cathode
@sudo rm -f /usr/share/applications/cathode-tube.desktop
@sudo rm -f /usr/share/mime/packages/application-cathode.xml
@sudo rm -f /usr/share/icons/hicolor/128x128/apps/cathode-tube.png
@sudo rm -f /usr/share/icons/hicolor/32x32/apps/cathode-tube.png
@sudo rm -f /usr/share/icons/hicolor/256x256/apps/cathode-tube.png
@sudo update-desktop-database

1134
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
},
"dependencies": {
"@neodrag/svelte": "^1.2.3",
"@tauri-apps/api": "^1.0.2"
"@tauri-apps/api": "^2.0.0-alpha.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",

View File

@ -1,6 +1,6 @@
[package]
name = "ray_format"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
authors = ["AnActualEmerald"]

View File

@ -1,4 +1,5 @@
use anyhow::Result;
use serde_json::Value;
use std::{
collections::{hash_map::Keys, HashMap},
fs,
@ -11,15 +12,15 @@ use crate::archive::Archive;
pub struct Ray {
frames: [Vec<u8>; 4],
extras: HashMap<String, Vec<u8>>,
meta: HashMap<String, String>,
meta: HashMap<String, Value>,
}
impl Ray {
pub fn get_meta_keys(&self) -> Keys<String, String> {
pub fn get_meta_keys(&self) -> Keys<String, Value> {
self.meta.keys()
}
pub fn get_meta_value(&self, key: &str) -> Option<&String> {
pub fn get_meta_value(&self, key: &str) -> Option<&Value> {
self.meta.get(key)
}
@ -36,17 +37,19 @@ impl Ray {
}
}
pub fn add_meta(&mut self, key: String, value: String) -> bool {
pub fn add_meta(&mut self, key: String, value: Value) -> bool {
self.meta.insert(key, value).is_none()
}
pub fn save(&self, path: &Path) -> Result<()> {
let mut archive = Archive::new();
for (i, f) in self.frames.iter().enumerate() {
if f.len() > 0 {
archive.add_file(&format!("{}", i), f)?
}
}
for (n, f) in self.extras.iter() {
archive.add_file(&n, f)?;
}
@ -71,7 +74,7 @@ impl Ray {
}
if let Ok(buf) = archive.get_file("meta.json") {
let meta: HashMap<String, String> = serde_json::from_slice(&buf)?;
let meta: HashMap<String, Value> = serde_json::from_slice(&buf)?;
ray.meta = meta;
}

1009
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,13 +9,18 @@ edition = "2021"
[package.metadata.generate-rpm]
assets = [
{source= "target/release/cathode-tube", dest= "/usr/bin/cathode", mode= "755"},
{source="cathode-tube.desktop", dest="/usr/share/applications/cathode-tube.desktop", mode="0644"},
{source="application-cathode.xml", dest="/usr/share/mime/packages/application-cathode.xml", mode="0644"},
{source="icons/128x128.png", dest="/usr/share/icons/hicolor/128x128/apps/cathode-tube.png", mode="0644"},
{source="icons/128x128@2x.png", dest="/usr/share/icons/hicolor/256x256@2/apps/cathode-tube.png", mode="0644"},
{ source = "target/release/cathode", dest = "/usr/bin/cathode", mode = "755" },
{ source = "cathode-tube.desktop", dest = "/usr/share/applications/cathode-tube.desktop", mode = "0644" },
{ source = "application-cathode.xml", dest = "/usr/share/mime/packages/application-cathode.xml", mode = "0644" },
{ source = "icons/128x128.png", dest = "/usr/share/icons/hicolor/128x128/apps/cathode-tube.png", mode = "0644" },
{ source = "icons/128x128@2x.png", dest = "/usr/share/icons/hicolor/256x256@2/apps/cathode-tube.png", mode = "0644" },
]
auto-req = "no"
[package.metadata.generate-rpm.requires]
filesystem = ">= 3"
gtk3 = ">= 3"
webkit2gtk3 = ">= 2"
[workspace]
@ -27,28 +32,28 @@ lto = true
codegen-units = 1
[build-dependencies]
tauri-build = { version = "1.1.1", features = [] }
tauri-build = { version = "2.0.0-alpha.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.1.1", features = ["cli", "dialog-all", "fs-create-dir", "fs-read-dir", "fs-read-file", "fs-write-file", "macos-private-api", "window-minimize", "window-set-max-size", "window-set-min-size", "window-unminimize"] }
cpal = { version = "0.14.0", features = ["jack"] }
ray_format = {path = "../ray_format", version = "~0.1.0"}
anyhow = "1.0.65"
tauri = { version = "2.0.0-alpha.0", features = ["cli", "dialog-all", "fs-create-dir", "fs-read-dir", "fs-read-file", "fs-write-file", "macos-private-api", "window-minimize", "window-set-max-size", "window-set-min-size", "window-unminimize"] }
cpal = { version = "0.14.2", features = ["jack"] }
ray_format = { path = "../ray_format", version = ">=0.1.0" }
anyhow = "1.0.67"
log = "0.4.17"
env_logger = "0.9.1"
env_logger = "0.9.3"
rand = "0.8.5"
tokio = { version = "1.21.2", features = ["full"] }
image = "0.24.4"
tokio = { version = "1.23.0", features = ["full"] }
image = "0.24.5"
base64-url = "1.4.13"
toml = "0.5.9"
toml = "0.5.10"
notify = "5.0.0"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = [ "custom-protocol" ]
default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]
custom-protocol = ["tauri/custom-protocol"]

View File

@ -2,7 +2,7 @@
Type=Application
Icon=cathode-tube
Name=Cathode
Exec=cathode-tube %U
Exec=cathode %U
Terminal=false
Hidden=false
Categories=Graphics; Video

View File

@ -12,7 +12,12 @@ use cpal::InputCallbackInfo;
use log::debug;
use tauri::Window;
pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mutex<f32>>) {
pub async fn monitor(
window: Window,
threshold: Arc<Mutex<f32>>,
level: Arc<Mutex<f32>>,
sens: Arc<Mutex<f32>>,
) {
let device = initialize().expect("Unable to init audio");
debug!("Using device {}", device.name().unwrap());
let config = device.default_input_config().unwrap();
@ -20,7 +25,10 @@ pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mute
.build_input_stream(
&config.config(),
move |data: &[f32], _: &InputCallbackInfo| {
if data.iter().any(|e| e.abs() >= *threshold.lock().unwrap()) {
if data
.iter()
.any(|e| (e.abs() * *sens.lock().unwrap()) >= *threshold.lock().unwrap())
{
window.emit("mouth-open", "").unwrap();
} else {
window.emit("mouth-close", "").unwrap();
@ -28,10 +36,9 @@ pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mute
*level.lock().unwrap() = data
.iter()
.map(|e| e.abs())
.max_by(|a, b| a.total_cmp(&b))
.unwrap()
.clone();
.map(|e| e.abs() * *sens.lock().unwrap())
.max_by(|a, b| a.total_cmp(b))
.unwrap();
},
move |err| {
println!("Audio error: {:?}", err);
@ -43,7 +50,7 @@ pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mute
// The stream will end if it goes out of scope, so just dwell here
loop {
sleep(Duration::from_secs(60));
sleep(Duration::from_secs(5000));
}
}
@ -52,3 +59,12 @@ fn initialize() -> Result<Device> {
host.default_input_device()
.ok_or_else(|| anyhow!("No default output device"))
}
#[tauri::command]
pub fn get_devices() -> Vec<String> {
let host = cpal::default_host();
host.input_devices()
.unwrap()
.filter_map(|e| e.name().ok())
.collect()
}

View File

@ -1,6 +1,7 @@
use std::fmt::Display;
use std::{fmt::Display, path::PathBuf};
use log::{debug, error};
use anyhow::Result;
use log::{debug, error, trace, warn};
use serde::{Deserialize, Serialize};
use tauri::api::path::config_dir;
@ -10,6 +11,8 @@ pub struct Config {
pub background_color: BGColor,
#[serde(default = "default_interval")]
pub blink_interval: u64,
#[serde(default = "default_sens")]
pub mic_sens: f32,
}
#[inline(always)]
@ -17,6 +20,11 @@ fn default_interval() -> u64 {
1500
}
#[inline(always)]
fn default_sens() -> f32 {
1.0
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BGColor {
@ -39,18 +47,39 @@ impl Display for BGColor {
Self::Custom(c) => {
write!(f, "{}", c)
}
_ => {
write!(f, "{}", self.to_string().to_lowercase())
}
Self::Green => write!(f, "green"),
Self::Blue => write!(f, "blue"),
Self::Pink => write!(f, "pink"),
Self::Transparent => write!(f, "transparent"),
}
}
}
#[inline]
pub fn get_config_dir() -> Option<PathBuf> {
Some(config_dir()?.join("cathode"))
}
pub fn load_config() -> Config {
if let Some(d) = config_dir() {
if let Some(path) = get_config_dir() {
use std::fs;
let path = d.join("cathode");
if !path.exists() {
if let Ok(true) = path.try_exists() {
let raw = fs::read_to_string(path.join("config.toml")).unwrap_or_else(|f| {
warn!("Failed to load config file: {}", f);
String::new()
});
trace!("Using config: {}", raw);
match toml::from_str::<Config>(&raw) {
Ok(c) => {
trace!("parsed config: {:#?}", c);
c
}
Err(e) => {
error!("Unable to parse config file: {}", e);
Config::default()
}
}
} else {
if let Err(e) = fs::create_dir_all(&path) {
debug!("{:?}", e);
error!("Failed to create config directory");
@ -59,14 +88,24 @@ pub fn load_config() -> Config {
debug!("Created config dir at {}", path.display())
}
Config::default()
} else {
let raw = fs::read_to_string(path.join("config.toml")).unwrap_or_else(|f| {
error!("Failed to load config file: {}", f);
String::new()
});
toml::from_str::<Config>(&raw).unwrap_or_default()
}
} else {
debug!("Using default config");
Config::default()
}
}
pub fn save_config(config: &Config) -> Result<()> {
let raw = toml::to_string_pretty(config)?;
if let Some(dir) = get_config_dir() {
use std::fs;
if !dir.exists() {
fs::create_dir_all(&dir)?;
}
debug!("Writing config");
trace!("Config: {}", raw);
let path = dir.join("config.toml");
fs::write(&path, raw)?;
}
Ok(())
}

View File

@ -8,19 +8,91 @@ use image::ImageFormat;
use log::{debug, trace};
use ray_format::Ray;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use tauri::api::dialog::blocking::FileDialogBuilder;
use tauri::api::path::{cache_dir, home_dir, picture_dir};
const OBJ_URL: &'static str = "data:image/png;base64,";
const OBJ_URL: &str = "data:image/png;base64,";
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Meta {
threshold: Option<String>,
close_threshold: Option<String>,
custom_anims: Option<HashMap<String, Map<String, Value>>>,
frame_meta: Option<Value>,
}
impl Meta {
pub(crate) fn to_map(&self) -> HashMap<String, Value> {
let mut tmp = HashMap::new();
if let Some(t) = &self.threshold {
tmp.insert("threshold".to_string(), Value::String(t.clone()));
}
if let Some(t) = &self.close_threshold {
tmp.insert("closeThreshold".to_string(), Value::String(t.clone()));
}
if let Some(ca) = &self.custom_anims {
let mut t = Map::new();
for (k, v) in ca.iter() {
t.insert(k.clone(), Value::Object(v.clone()));
}
tmp.insert("customAnims".to_string(), Value::Object(t));
}
if let Some(fm) = &self.frame_meta {
tmp.insert("frameMeta".to_string(), fm.clone());
}
tmp
}
pub(crate) fn from_map(map: HashMap<String, Value>) -> Self {
let threshold = if let Some(Value::String(t)) = map.get("threshold") {
Some(t.clone())
} else {
None
};
let close_threshold = if let Some(Value::String(t)) = map.get("closeThreshold") {
Some(t.clone())
} else {
None
};
let custom_anims = if let Some(Value::Object(o)) = map.get("customAnims") {
let mut an = HashMap::new();
for (k, v) in o.iter() {
if let Value::Object(obj) = v {
an.insert(k.clone(), obj.clone());
}
}
Some(an)
} else {
None
};
let frame_meta = map.get("frameMeta").cloned();
Meta {
threshold,
close_threshold,
custom_anims,
frame_meta,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(crate) struct WebRay {
frames: [String; 4],
meta: HashMap<String, String>,
meta: Meta,
}
#[tauri::command]
pub(crate) fn open_image() -> Option<String> {
pub(crate) async fn open_image() -> Option<String> {
debug!("Opening iamge dialog...");
let path = FileDialogBuilder::new()
.add_filter("Images", &["png", "jpg"])
.set_directory(picture_dir().unwrap_or_else(|| home_dir().unwrap()))
@ -68,7 +140,7 @@ pub(crate) fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
}
for k in ray.get_meta_keys() {
if let Some(v) = ray.get_meta_value(&k) {
if let Some(v) = ray.get_meta_value(k) {
meta.insert(k.clone(), v.clone());
}
}
@ -83,7 +155,10 @@ pub(crate) fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
.as_bytes(),
)
.unwrap();
Some(WebRay { frames, meta })
Some(WebRay {
frames,
meta: Meta::from_map(meta),
})
}
#[tauri::command]
@ -103,7 +178,7 @@ pub(crate) async fn save_ray(ray: WebRay) -> Result<(), String> {
res.set_frame(i as usize, decoded);
}
for (k, v) in ray.meta {
for (k, v) in ray.meta.to_map() {
res.add_meta(k, v);
}

View File

@ -16,7 +16,7 @@ use log::{debug, error, trace, warn};
use serde_json::Value;
use tauri::{
api::path::{cache_dir, config_dir},
Manager, State,
Manager, State, WindowEvent,
};
use crate::config::Config;
@ -32,6 +32,7 @@ struct AudioLevel(Arc<Mutex<f32>>);
struct BlinkInterval(Arc<Mutex<u64>>);
struct CurrentConfig(Arc<Mutex<Config>>);
struct RayToLoad(Arc<Mutex<Option<PathBuf>>>);
struct MicSense(Arc<Mutex<f32>>);
fn main() {
env_logger::init();
@ -39,6 +40,7 @@ fn main() {
let level = Arc::new(Mutex::new(0.));
let blink_interval = Arc::new(Mutex::new(1500));
let ray = Arc::new(Mutex::new(None));
let sens = Arc::new(Mutex::new(1.0));
if let Some(d) = cache_dir() {
use std::fs;
@ -52,22 +54,33 @@ fn main() {
let config = config::load_config();
{
*blink_interval.lock().unwrap() = config.blink_interval
*blink_interval.lock().unwrap() = config.blink_interval;
*sens.lock().unwrap() = config.mic_sens;
}
let current_conf = Arc::new(Mutex::new(config));
let config = current_conf.clone();
let c2 = current_conf.clone();
tauri::Builder::default()
.manage(MicThreshold(threshold.clone()))
.manage(AudioLevel(level.clone()))
.manage(BlinkInterval(blink_interval.clone()))
.manage(CurrentConfig(current_conf.clone()))
.manage(CurrentConfig(current_conf))
.manage(RayToLoad(ray.clone()))
.manage(MicSense(sens.clone()))
.on_window_event(move |event| {
if let WindowEvent::CloseRequested { .. } = event.event() {
debug!("close requested");
if let Err(e) = config::save_config(&c2.lock().unwrap()) {
error!("Error writing config file: {}", e);
}
}
})
.setup(move |app| {
let window = app.get_window("main").unwrap();
tauri::async_runtime::spawn(async move {
monitor(window, threshold, level).await;
monitor(window, threshold, level, sens).await;
});
let window = app.get_window("main").unwrap();
@ -79,8 +92,8 @@ fn main() {
warn!("Failed to emit blink event: {}", e);
}
}
let blink = blink_interval.lock().unwrap();
sleep(Duration::from_millis(*blink));
let blink = *blink_interval.lock().unwrap();
sleep(Duration::from_millis(blink));
}
});
@ -130,14 +143,19 @@ fn main() {
log,
set_mic_threshold,
get_mic_threshold,
set_mic_sens,
get_mic_sens,
get_audio_level,
get_blink_interval,
set_blink_interval,
get_config,
set_config,
save_current_config,
get_ray_to_load,
fs::open_image,
fs::save_ray,
fs::open_ray,
audio::get_devices,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
@ -147,15 +165,14 @@ fn main() {
fn get_ray_to_load(ray: State<'_, RayToLoad>) -> Option<WebRay> {
let ray = { (*ray.0.lock().unwrap()).clone() };
if let Some(r) = ray {
let ray = fs::load_ray(r);
ray
fs::load_ray(r)
} else {
None
}
}
#[tauri::command]
fn log(msg: String) {
fn log(msg: String, level: Option<String>) {
debug!("frontend: {}", msg);
}
@ -169,6 +186,16 @@ fn get_mic_threshold(current: State<'_, MicThreshold>) -> f32 {
*current.0.lock().unwrap()
}
#[tauri::command]
fn set_mic_sens(sens: f32, current: State<'_, MicSense>) {
*current.0.lock().unwrap() = sens;
}
#[tauri::command]
fn get_mic_sens(current: State<'_, MicSense>) -> f32 {
*current.0.lock().unwrap()
}
#[tauri::command]
fn get_audio_level(level: State<'_, AudioLevel>) -> f32 {
*level.0.lock().unwrap()
@ -188,3 +215,13 @@ fn set_blink_interval(value: u64, current: State<'_, BlinkInterval>) {
fn get_config(current: State<'_, CurrentConfig>) -> Config {
current.0.lock().unwrap().clone()
}
#[tauri::command]
fn set_config(config: Config, current: State<'_, CurrentConfig>) {
*current.0.lock().unwrap() = config
}
#[tauri::command]
fn save_current_config(current: State<'_, CurrentConfig>) -> Result<(), String> {
config::save_config(&current.0.lock().unwrap()).map_err(|e| format!("{}", e))
}

View File

@ -6,17 +6,19 @@
"distDir": "../dist"
},
"package": {
"productName": "cathode-tube",
"version": "0.0.1"
"productName": "cathode",
"version": "0.1.0"
},
"tauri": {
"cli": {
"description": "A small app for PNG-tubing",
"args": [{
"name": "file",
"index": 1,
"takesValue": true
}]
"args": [
{
"name": "file",
"index": 1,
"takesValue": true
}
]
},
"macOSPrivateApi": true,
"allowlist": {
@ -24,7 +26,10 @@
"all": true
},
"fs": {
"scope": ["$PUBLIC/*", "$CONFIG/*"],
"scope": [
"$PUBLIC/*",
"$CONFIG/*"
],
"readFile": true,
"readDir": true,
"createDir": true,
@ -42,10 +47,10 @@
"category": "DeveloperTool",
"copyright": "",
"deb": {
"files" : {
"/usr/share/applications/cathode-tube.desktop": "cathode-tube.desktop",
"/usr/share/mime/packages/application-cathode.xml": "application-cathode.xml",
"/usr/share/icons/hicolor/256x256/mimetypes/aplication-cathode.png": "icons/application-cathode-256.png"
"files": {
"/usr/share/applications/cathode-tube.desktop": "cathode-tube.desktop",
"/usr/share/mime/packages/application-cathode.xml": "application-cathode.xml",
"/usr/share/icons/hicolor/256x256/mimetypes/aplication-cathode.png": "icons/application-cathode-256.png"
},
"depends": []
},
@ -92,4 +97,4 @@
}
]
}
}
}

View File

@ -1,37 +1,43 @@
<script lang="ts">
import MainView from "./views/main.svelte";
import {listen} from "@tauri-apps/api/event";
import {invoke} from "@tauri-apps/api";
import {onMount} from "svelte";
import {config} from "./store";
import type {Config} from "./store";
$: transparent = $config.background_color === "transparent";
$: color = typeof $config.background_color === "object" ? $config.background_color.custom : $config.background_color;
$: {
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api";
import { onDestroy, onMount } from "svelte";
import { config } from "./store";
import type { Config } from "./store";
invoke("log", {msg: `color: ${color} trasnparent: ${transparent}`
$: transparent = $config.background_color === "transparent";
$: color =
typeof $config.background_color === "object"
? $config.background_color.custom
: $config.background_color;
$: {
invoke("log", {
msg: `color: ${color} trasnparent: ${transparent}`,
}).catch();
}
onMount(async () => {
$config = await invoke("get_config") as Config;
$config = (await invoke("get_config")) as Config;
await listen("reload-config", async () => {
$config = await invoke("get_config") as Config;
$config = (await invoke("get_config")) as Config;
});
});
onDestroy(async () => {
await invoke("save_current_config");
});
</script>
<main>
<MainView --active-color={transparent ? 'lightblue' : color} --inactive-color={color}/>
<MainView
--active-color={transparent ? "lightblue" : color}
--inactive-color={color}
/>
</main>
<style lang="scss">
:global(*) {
-moz-user-select: none;
-webkit-user-select: none;

View File

@ -1,12 +1,20 @@
<script lang="ts" context="module">
const hints = ["Eyes open | Mouth closed", "Eyes open | Mouth open", "Eyes closed | Mouth closed", "Eyes closed | Mouth open"];
const hints = [
"Eyes open | Mouth closed",
"Eyes open | Mouth open",
"Eyes closed | Mouth closed",
"Eyes closed | Mouth open",
];
let loading = [false, false, false, false];
</script>
<script lang="ts">
import { frames } from "../store";
import { invoke } from "@tauri-apps/api";
import { fade } from "svelte/transition";
import Context from "../components/context.svelte";
import Context from "./context.svelte";
import LoadingSVG from "/src/loading.svg";
export let index: number;
let menuTimeout: NodeJS.Timeout | null = null;
@ -14,24 +22,32 @@
let src = "";
$: {
src = $frames[index];
src = $frames.get(index)?.img;
}
const openImage = async () => {
loading[index] = true;
const path = (await invoke("open_image")) as string;
if (path) {
$frames[index] = path;
$frames.get(index).img = path;
}
loading[index] = false;
};
const clearImage = () => {
$frames.get(index).img = "";
};
//TODO: load frame from ray
</script>
<div class="box">
<div
<button
class="preview"
on:click={openImage}
on:contextmenu={openImage}
on:click={(e) => {
if (e.shiftKey) {
clearImage();
} else {
openImage();
}
}}
on:mouseenter={() =>
(menuTimeout = setTimeout(() => (showMenu = true), 200))}
on:mouseleave={() => {
@ -41,10 +57,12 @@
showMenu = false;
}}
>
{#if src}
{#if loading[index]}
<img class="loading" src={LoadingSVG} alt="Loading" />
{:else if src}
<img {src} alt="Frame {{ index }}" />
{/if}
</div>
</button>
{#if showMenu}
<div transition:fade={{ duration: 50 }} class="context">
<Context>
@ -55,6 +73,35 @@
</div>
<style lang="scss">
@-webkit-keyframes rotating {
from {
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading {
-webkit-animation: rotating 2s linear infinite;
-moz-animation: rotating 2s linear infinite;
-ms-animation: rotating 2s linear infinite;
-o-animation: rotating 2s linear infinite;
animation: rotating 2s linear infinite;
}
$bg: rgba(150, 150, 150, 0.5);
.preview {
&:hover {
@ -76,8 +123,13 @@
user-select: none;
gap: 10px;
display: flex;
align-content: center;
justify-content: center;
.context {
align-self: center;
z-index: 900;
position: absolute;
left: 105%;
width: max-content;
}
}

View File

@ -3,7 +3,7 @@
import { onMount } from "svelte";
import { tweened } from "svelte/motion";
import { sineIn, sineOut } from "svelte/easing";
import { invoke } from "@tauri-apps/api";
// import { invoke } from "@tauri-apps/api";
export let progress = 0;
export let withSetpoint = false;
@ -13,7 +13,7 @@
let bar: HTMLDivElement;
let point: HTMLDivElement;
let pos = { x: 0, y: 0 };
const tweenedProgress = tweened(0, { duration: 40, easing: sineOut });
const tweenedProgress = tweened(0, { duration: 100, easing: sineOut });
onMount(async () => {
let rect = bar.getBoundingClientRect();
@ -26,20 +26,21 @@
if (pxProgress > $tweenedProgress) {
tweenedProgress
.set(pxProgress, {
duration: 20,
duration: 30,
easing: sineOut,
})
.then();
} else {
$tweenedProgress = pxProgress;
}
$tweenedProgress = pxProgress;
}
</script>
<svelte:window
on:resize={async () => {
await invoke("log", {
msg: `Resized`,
});
// await invoke("log", {
// msg: `Resized`,
// });
pos = { x: 0, y: 0 };
let rect = bar.getBoundingClientRect();

View File

@ -0,0 +1,13 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api";
const devices: Promise<string[]> = invoke("get_devices");
</script>
{#await devices}
<span>...</span>
{:then d}
{#each d as dev}
<p>{dev}</p>
{/each}
{/await}

View File

@ -1,75 +1,127 @@
<script lang="ts">
import {config} from "../store";
import {createEventDispatcher} from "svelte";
import {invoke} from "@tauri-apps/api";
import { Config, config } from "../store";
import { createEventDispatcher } from "svelte";
import { invoke } from "@tauri-apps/api";
const dispatch = createEventDispatcher();
const log = async (msg: string) => {
await invoke("log", { msg });
};
let org = $config;
const dispatch = createEventDispatcher();
const onClose = () => {
if(org != $config) {
invoke("set_blink_interval", {value: $config.blink_interval});
org = $config;
}
dispatch('close');
}
const onSave = async () => {
await invoke("set_config", { config: $config });
await invoke("set_blink_interval", { value: $config.blink_interval });
await invoke("set_mic_sens", { sens: $config.mic_sens });
await invoke("save_current_config");
};
const reset = async () => {
$config = (await invoke("get_config")) as Config;
};
const onClose = async () => {
//TODO: alert if unsaved changes
reset();
dispatch("close");
};
</script>
<div class="settings">
<span class="close" on:click={onClose}>X</span>
<div>
<label for="bg-color">Background color:</label>
<select bind:value={$config.background_color} id="bg-color">
<option value="transparent">Transparent</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="red">Red</option>
<option value="pink">Pink</option>
</select>
<button class="close" on:click={onClose}>X</button>
<div class="setting">
<label for="bg-color">Background color:</label>
<select bind:value={$config.background_color} id="bg-color">
<option value="transparent">Transparent</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="red">Red</option>
<option value="pink">Pink</option>
</select>
</div>
<div>
<label for="blink">Blink interval: </label>
<input value={$config.blink_interval} on:change={(e) => {$config.blink_interval = e.currentTarget.valueAsNumber}} id="blink" type="number" min="0"/>
<div class="setting">
<label for="blink">Blink interval: </label>
<input
value={$config.blink_interval}
on:change={async (e) => {
if (e.currentTarget.checkValidity()) {
$config.blink_interval = e.currentTarget.valueAsNumber;
} else {
$config.blink_interval = Number.parseInt(e.currentTarget.min);
}
}}
id="blink"
type="number"
min="100"
/>
</div>
<div class="setting">
<label for="sens">Mic sensitivity: </label>
<input
type="range"
id="sens"
step="0.01"
min="0.01"
max="1.0"
value={$config.mic_sens}
on:change={(e) => {
$config.mic_sens = e.currentTarget.valueAsNumber;
}}
/>
<span>{Math.trunc($config.mic_sens * 100)}%</span>
</div>
<footer>
<button on:click={onSave}>Save</button><button on:click={reset}
>Cancel</button
>
</footer>
</div>
<style lang="scss" >
.settings {
position: absolute;
transform: translateX(-50%) translateY(-50%);
top: 50%;
left: 50%;
z-index: 1000;
width: 90%;
height: 90%;
display: flex;
flex-direction: column;
gap: 5vh;
background-color: rgba(63, 176, 252, 0.9);
border-radius: 15px;
border: solid black 2px;
padding: 15px;
label{
font-weight: bold;
}
.close {
<style lang="scss">
.settings {
position: absolute;
right: 15px;
top: 15px;
font-size: 20px;
cursor: pointer;
width: 2rem;
height: 2rem;
background-color: black;
color: white;
border: solid black 1px;
text-align: center;
vertical-align: center;
transform: translateX(-50%) translateY(-50%);
top: 50%;
left: 50%;
z-index: 1000;
width: 90%;
height: 90%;
display: flex;
flex-direction: column;
gap: 5vh;
background-color: rgba(63, 176, 252, 0.9);
border-radius: 15px;
border: solid black 2px;
padding: 20px;
label {
font-weight: bold;
}
.close {
&:focus {
border: solid white 2px;
}
position: absolute;
right: 15px;
top: 15px;
font-size: 20px;
cursor: pointer;
width: 2rem;
height: 2rem;
background-color: black;
color: white;
border: solid black 1px;
text-align: center;
vertical-align: center;
border-radius: 5px;
}
}
.setting {
display: flex;
align-content: center;
justify-content: space-between;
width: 50%;