Compare commits

...

12 Commits
v0.1.0 ... main

Author SHA1 Message Date
Emerald 47b33853f3
add sever url to trigger plugin settings
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/deployment/publish Pipeline failed Details
2023-10-17 19:37:46 -04:00
Emerald 6ddb397c22
fix regex in CI
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/deployment/publish Pipeline failed Details
2023-10-16 22:25:14 -04:00
Emerald b1329697bf
chore(ci): update website on publish
ci/woodpecker/push/build Pipeline was successful Details
2023-10-16 21:49:07 -04:00
Emerald a7a897495a
chore: update binary name everywhere
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/tag/publish Pipeline was successful Details
2023-10-16 21:17:48 -04:00
Emerald 0c7c57a0fd
chore(ci): update binary name in publish workflow
ci/woodpecker/tag/publish Pipeline failed Details
ci/woodpecker/push/build Pipeline was successful Details
2023-10-16 18:25:54 -04:00
Emerald 038c20600d
fix typo in publish workflow
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/tag/publish Pipeline failed Details
2023-10-16 18:13:22 -04:00
Emerald 7cc2aa022a
chore(ci): allow manual execution of publish pipline
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/tag/publish Pipeline failed Details
2023-10-16 00:00:20 -04:00
Emerald c12b2af269
chore(ci): update bundle location in publish workflow
ci/woodpecker/push/build Pipeline was successful Details
2023-10-15 19:32:22 -04:00
Emerald 594f321c57
chore(release): prepare for 0.2.0
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/push/publish Pipeline was successful Details
2023-10-12 01:35:42 -04:00
Emerald 7dd67cd8b5
chore(ci): update woodpecker pipeline syntax 2023-10-12 01:35:42 -04:00
Emerald 5f51d0b1d6
feat: use canvas to render png 2023-10-12 01:35:42 -04:00
emerald 1a27172cc9
fix config not saving 2023-10-12 01:35:42 -04:00
25 changed files with 3105 additions and 3826 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/
dist/
bundle/
target/

2
.tool-versions Normal file
View File

@ -0,0 +1,2 @@
node 20
yarn latest

View File

@ -1,11 +1,12 @@
branches: [main, dev]
pipeline:
when:
- branch: [main, dev]
event: [push, pull_request]
steps:
build:
image: gitea.greenboi.me/emerald/cathode-build:latest
image: forge.greenboi.me/emerald/cathode-build:latest
pull: true
when:
event: [push, pull_request]
commands:
- npm ci
- npm run build
- yarn install --frozen-lockfile
- yarn build
- cd src-tauri && cargo build

View File

@ -1,20 +1,34 @@
branches: main
pipeline:
when:
- event: [ tag, manual ]
branch: main
- event: deployment
environment: production
steps:
build:
image: gitea.greenboi.me/emerald/cathode-build:latest
image: forge.greenboi.me/emerald/cathode-build:latest
pull: true
when:
event: [tag]
commands:
- npm ci
- npm run tauri build
- yarn install --frozen-lockfile
- yarn tauri build
- cd src-tauri && cargo generate-rpm
upload:
image: alpine/curl
when:
event: [tag]
secrets: [gitea_key]
when:
- evaluate: 'CI_COMMIT_TAG matches "^v[[:digit:]]+[.][[:digit:]]+[.][[:digit:]]+.* "'
commands:
- curl --user Emerald:$GITEA_KEY --upload-file target/release/bundle/deb/cathode-tube_${CI_COMMIT_TAG##v}_amd64.deb https://gitea.greenboi.me/api/packages/Emerald/generic/cathode/${CI_COMMIT_TAG##v}/cathode-tube.deb
- curl --user Emerald:$GITEA_KEY --upload-file target/release/bundle/appimage/cathode-tube_${CI_COMMIT_TAG##v}_amd64.AppImage https://gitea.greenboi.me/api/packages/Emerald/generic/cathode/${CI_COMMIT_TAG##v}/cathode-tube.AppImage
- curl --user Emerald:$GITEA_KEY --upload-file src-tauri/target/release/bundle/deb/cathode_${CI_COMMIT_TAG##v}_amd64.deb https://forge.greenboi.me/api/packages/emerald/generic/cathode-tube/${CI_COMMIT_TAG##v}/cathode.deb
- curl --user Emerald:$GITEA_KEY --upload-file src-tauri/target/release/bundle/appimage/cathode_${CI_COMMIT_TAG##v}_amd64.AppImage https://forge.greenboi.me/api/packages/emerald/generic/cathode-tube/${CI_COMMIT_TAG##v}/cathode.AppImage
- curl --user Emerald:$GITEA_KEY --upload-file src-tauri/target/generate-rpm/cathode-${CI_COMMIT_TAG##v}-1.x86_64.rpm https://forge.greenboi.me/api/packages/emerald/generic/cathode-tube/${CI_COMMIT_TAG##v}/cathode.rpm
update_site:
image: woodpeckerci/plugin-trigger
settings:
server: https://ci.greenboi.me
repositories:
- emerald/cathode_dot_tube
deploy: produciton
token:
from_secret: woodpecker_token

View File

@ -1,25 +1,15 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.0] - 2023-10-12
## [Unreleased]
### Features
- Use canvas to render png
## [v0.1.0]
### Miscellaneous Tasks
### 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
- Update woodpecker pipeline syntax
<!-- generated by git-cliff -->

View File

@ -1,7 +1,9 @@
FROM node:bullseye
FROM node:20-bookworm
RUN corepack enable yarn
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
RUN apt install -yy libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libasound2-dev
RUN curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
RUN mv /root/.cargo/bin/* /usr/bin
RUN cargo install cargo-generate-rpm
RUN mv /root/.cargo/bin/* /usr/bin
ENTRYPOINT ["/bin/bash"]

82
cliff.toml Normal file
View File

@ -0,0 +1,82 @@
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# postprocessors
postprocessors = [
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"}, # replace issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^style", group = "Styling" },
{ message = "^test", group = "Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|ci", group = "Miscellaneous Tasks" },
{ body = ".*security", group = "Security" },
{ message = "^revert", group = "Revert" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = "v\\d+\\.\\d+\\.\\d+-.*"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42

View File

@ -6,16 +6,14 @@ dev:
-cargo tauri dev
debug:
RUST_LOG=debug cargo tauri dev
RUST_LOG=debug yarn tauri dev
log RUST_LOG:
cargo tauri dev
yarn tauri dev
build:
cargo tauri build
yarn tauri build
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
@ -39,3 +37,21 @@ uninstall:
@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
release tag:
#!/usr/bin/env bash
set -euxo pipefail
just update-version {{tag}}
git cliff -o CHANGELOG.md --tag {{tag}}
git commit -am "chore(release): prepare for {{tag}}" -S
git tag -s -a "{{tag}}" -m "$(git cliff -u --strip all --tag {{tag}})"
echo "Ready to push release (git push && git push --tag {{tag}})"
update-version tag:
#!/usr/bin/env bash
set -euxo pipefail
v="{{trim_start_match(tag, "v")}}"
sed -i 's/version = "*.*.*" # managed by release.sh/version = "'"$v"'" # managed by release.sh/g' -i src-tauri/Cargo.toml
sed -i 's/"version": "*.*.*"/"version": "'"$v"'"/g' package.json
sed -i 's/"version": "*.*.*"/"version": "'"$v"'"/g' src-tauri/tauri.conf.json
echo "Updated versions to {{tag}}"

2706
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "cathode-tube",
"private": true,
"version": "0.0.1",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
@ -12,12 +12,15 @@
},
"dependencies": {
"@neodrag/svelte": "^1.2.3",
"@tauri-apps/api": "^1.0.2"
"@tauri-apps/api": "^1.5.0",
"animejs": "^3.2.1",
"image-js": "^0.35.4"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tauri-apps/cli": "^1.1.1",
"@tauri-apps/cli": "^1.5.2",
"@tsconfig/svelte": "^3.0.0",
"@types/animejs": "^3.1.8",
"@types/node": "^18.7.10",
"sass": "^1.54.8",
"svelte": "^3.49.0",

2302
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[package]
name = "cathode-tube"
version = "0.1.0"
description = "A Tauri App"
name = "cathode"
version = "0.2.0" # managed by release.sh
description = "A small PNGTubing app"
authors = ["AnActualEmerald"]
license = "GPL-3.0-or-later"
repository = "https://github.com/AnActualEmerald/cathode"
@ -10,10 +10,12 @@ edition = "2021"
[package.metadata.generate-rpm]
assets = [
{source= "target/release/cathode", dest= "/usr/bin/cathode", mode= "755"},
{source="cathode-tube.desktop", dest="/usr/share/applications/cathode-tube.desktop", mode="0644"},
{source="cathode-tube.desktop", dest="/usr/share/applications/cathode.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="icons/128x128.png", dest="/usr/share/icons/hicolor/128x128/apps/cathode.png", mode="0644"},
{source="icons/128x128@2x.png", dest="/usr/share/icons/hicolor/256x256/apps/cathode.png", mode="0644"},
{source="icons/application-cathode-128.png", dest="/usr/share/icons/hicolor/128x128/mimetypes/application-cathode.png", mode="0644"},
{source="icons/application-cathode-256.png", dest="/usr/share/icons/hicolor/256x256/mimetypes/application-cathode.png", mode="0644"},
]
auto-req = "no"
@ -32,23 +34,24 @@ lto = true
codegen-units = 1
[build-dependencies]
tauri-build = { version = "1.1.1", features = [] }
tauri-build = { version = "1", 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"] }
tauri = { version = "1", features = ["cli", "dialog-all", "fs-create-dir", "fs-read-dir", "fs-read-file", "fs-write-file", "macos-private-api", "protocol-asset", "window-center", "window-minimize", "window-set-max-size", "window-set-min-size", "window-unminimize"] }
cpal = { version = "0.14.1", features = ["jack"] }
ray_format = {path = "../ray_format", version = "~0.1.0"}
anyhow = "1.0.65"
anyhow = "1.0.66"
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"
base64-url = "1.4.13"
toml = "0.5.9"
notify = "5.0.0"
figment = { version = "0.10.11", features = ["toml", "env"] }
base64 = "0.21.4"
[features]
# by default Tauri runs in production mode

View File

@ -1,6 +1,6 @@
[Desktop Entry]
Type=Application
Icon=cathode-tube
Icon=cathode
Name=Cathode
Exec=cathode %U
Terminal=false

View File

@ -12,12 +12,12 @@ use cpal::InputCallbackInfo;
use log::debug;
use tauri::Window;
pub async fn monitor(
pub 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();
@ -48,13 +48,14 @@ pub async fn monitor(
stream.play().expect("Error creating input stream");
// The stream will end if it goes out of scope, so just dwell here
// The stream is closed when dropped, so just dwell here
loop {
sleep(Duration::from_secs(5000));
sleep(Duration::from_millis(10000));
}
}
fn initialize() -> Result<Device> {
debug!("Hey there");
let host = cpal::default_host();
host.default_input_device()
.ok_or_else(|| anyhow!("No default output device"))

View File

@ -1,7 +1,11 @@
use std::{fmt::Display, path::PathBuf};
use anyhow::Result;
use log::{debug, error, trace};
use figment::{
providers::{Env, Format, Toml},
Figment,
};
use log::{debug, error, trace, warn};
use serde::{Deserialize, Serialize};
use tauri::api::path::config_dir;
@ -55,53 +59,33 @@ impl Display for BGColor {
}
#[inline]
pub fn get_config_dir() -> Option<PathBuf> {
Some(config_dir()?.join("cathode"))
pub fn get_config_dir() -> PathBuf {
config_dir()
.expect("Unable to get config directory")
.join("cathode")
}
pub fn load_config() -> Config {
if let Some(path) = get_config_dir() {
use std::fs;
if !path.exists() {
if let Err(e) = fs::create_dir_all(&path) {
debug!("{:?}", e);
error!("Failed to create config directory");
error!("{}", e);
} else {
debug!("Created config dir at {}", path.display())
}
Figment::new()
.merge(Toml::file(get_config_dir().join("config.toml")))
.merge(Env::prefixed("CATHODE_"))
.extract()
.unwrap_or_else(|e| {
error!("Error while loading config: {e}");
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()
});
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 {
debug!("Using default config");
Config::default()
}
})
}
pub fn save_config(config: &Config) -> Result<()> {
use std::fs;
let raw = toml::to_string_pretty(config)?;
if let Some(dir) = get_config_dir() {
use std::fs;
debug!("Writing config");
trace!("Config: {}", raw);
let path = dir.join("config.toml");
fs::write(&path, raw)?;
let dir = get_config_dir();
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

@ -1,18 +1,18 @@
use std::collections::HashMap;
use std::fs;
use std::io::Cursor;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::path::Path;
use base64_url as base64;
use anyhow::Result;
use base64::prelude::*;
use image::ImageFormat;
use log::{debug, trace};
use log::{debug, error};
use ray_format::Ray;
use serde::{Deserialize, Serialize};
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,";
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(crate) struct WebRay {
frames: [String; 4],
@ -24,16 +24,25 @@ 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()))
.set_directory(
picture_dir().unwrap_or_else(|| home_dir().expect("No home directory for user")),
)
.set_title("Select an image")
.pick_file()?;
if let Ok(b) = image::open(path) {
let mut buf = Cursor::new(vec![]);
b.write_to(&mut buf, ImageFormat::Png).unwrap();
let encoded = base64::encode(buf.get_ref());
trace!("Encoded: {:?}", encoded);
if let Ok(mut b) = image::open(&path) {
if b.width() > 600 || b.height() > 400 {
b = b.resize(600, 400, image::imageops::FilterType::Lanczos3);
}
let mut loaded = cache_dir()
.expect("Unable to get cache dir")
.join("cathode")
.join("loaded")
.join(path.file_stem().unwrap_or_else(|| OsStr::new("image")));
loaded.set_extension("png");
let mut file = File::create(&loaded).expect("Unable to create file");
b.write_to(&mut file, ImageFormat::Png).unwrap();
Some(format!("{}{}", OBJ_URL, encoded))
loaded.to_str().map(|v| v.to_string())
} else {
None
}
@ -62,9 +71,9 @@ pub(crate) fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
debug!("Frame {} was empty", i);
continue;
}
let encoded = base64::encode(&f);
let encoded = BASE64_STANDARD.encode(f);
frames[i as usize] = format!("{}{}", OBJ_URL, encoded);
frames[i as usize] = encoded;
}
}
@ -74,16 +83,10 @@ pub(crate) fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
}
}
fs::write(
cache_dir().unwrap().join("cathode").join("last_selected"),
path.as_ref()
.canonicalize()
.unwrap()
.to_str()
.unwrap()
.as_bytes(),
)
.unwrap();
if let Err(e) = cache_loaded_ray(path.as_ref()) {
error!("Error caching loaded ray: {e}");
}
Some(WebRay { frames, meta })
}
@ -99,18 +102,42 @@ pub(crate) async fn save_ray(ray: WebRay) -> Result<(), String> {
let mut res = Ray::default();
for (i, f) in ray.frames.iter().enumerate() {
let stripped = f.strip_prefix(OBJ_URL).unwrap_or(f);
let decoded = base64::decode(stripped).unwrap();
res.set_frame(i as usize, decoded);
if f == "" {
continue;
}
match BASE64_STANDARD.decode(f) {
Ok(decoded) => {
res.set_frame(i as usize, decoded);
}
Err(e) => {
error!("{e:?}");
}
}
}
for (k, v) in ray.meta {
res.add_meta(k, v);
}
cache_loaded_ray(&path).map_err(|e| e.to_string())?;
res.save(&path)
.map_err(|e| format!("Failed to save ray file: {}", e))
} else {
Ok(())
}
}
pub fn cache_loaded_ray(path: impl AsRef<Path>) -> Result<()> {
fs::write(
cache_dir().unwrap().join("cathode").join("last_selected"),
path.as_ref()
.canonicalize()
.unwrap()
.to_str()
.unwrap()
.as_bytes(),
)?;
Ok(())
}

View File

@ -16,8 +16,7 @@ use log::{debug, error, trace, warn};
use serde_json::Value;
use tauri::{
api::path::{cache_dir, config_dir},
Manager,
State, //, WindowEvent,
Manager, State, WindowEvent,
};
use crate::config::Config;
@ -45,24 +44,22 @@ fn main() {
if let Some(d) = cache_dir() {
use std::fs;
debug!("Ensuring cache directories exist");
fs::create_dir_all(d.join("cathode").join("loaded")).expect("Unable to create cache dir");
if let Ok(s) = fs::read_to_string(d.join("cathode").join("last_selected")) {
debug!("Found selected ray in cache");
*ray.lock().unwrap() = Some(Path::new(&s).to_path_buf());
}
}
// Create the config directory if needed
debug!("Loading existing config");
let config = config::load_config();
{
*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();
let c2 = current_conf.clone();
tauri::Builder::default()
.manage(MicThreshold(threshold.clone()))
.manage(AudioLevel(level.clone()))
@ -70,22 +67,28 @@ fn main() {
.manage(CurrentConfig(current_conf.clone()))
.manage(RayToLoad(ray.clone()))
.manage(MicSense(sens.clone()))
// .on_window_event(move |event| match event.event() {
// WindowEvent::CloseRequested { .. } => {
// debug!("close requested");
// if let Err(e) = config::save_config(&*c2.lock().unwrap()) {
// error!("Error writing config file: {}", e);
// }
// }
// _ => {}
// })
.on_window_event(move |event| match event.event() {
WindowEvent::CloseRequested { .. } => {
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, sens).await;
monitor(window, threshold, level, sens);
});
let window = app.get_window("main").unwrap();
window
.emit_all("reload-config", "")
.expect("Failed to send window event");
#[cfg(debug_assertions)]
window.open_devtools();
tauri::async_runtime::spawn(async move {
loop {
if rand::random() {

View File

@ -7,7 +7,7 @@
},
"package": {
"productName": "cathode",
"version": "0.1.0"
"version": "0.2.0"
},
"tauri": {
"cli": {
@ -28,18 +28,27 @@
"fs": {
"scope": [
"$PUBLIC/*",
"$CONFIG/*"
"$CONFIG/*",
"$CACHE/cathode/*",
"$CACHE/cathode/loaded/*"
],
"readFile": true,
"readDir": true,
"createDir": true,
"writeFile": true
},
"protocol": {
"assetScope": [
"$CACHE/cathode/loaded/*"
],
"asset": true
},
"window": {
"setMinSize": true,
"setMaxSize": true,
"minimize": true,
"unminimize": true
"unminimize": true,
"center": true
}
},
"bundle": {
@ -80,7 +89,7 @@
}
},
"security": {
"csp": null
"csp": "default-src 'self'; img-src 'self'; asset: https://asset.localhost"
},
"updater": {
"active": false

View File

@ -11,30 +11,32 @@
<script lang="ts">
import { frames } from "../store";
import { invoke } from "@tauri-apps/api";
import { fs, invoke } from "@tauri-apps/api";
import { fade } from "svelte/transition";
import Context from "./context.svelte";
import LoadingSVG from "/src/loading.svg";
import Image from "image-js";
export let index: number;
let menuTimeout: NodeJS.Timeout | null = null;
let showMenu = false;
let src = "";
let src: string | null;
$: {
src = $frames[index];
src = $frames[index] ? $frames[index].toDataURL() : null;
}
const openImage = async () => {
loading[index] = true;
const path = (await invoke("open_image")) as string;
if (path) {
$frames[index] = path;
const data = await fs.readBinaryFile(path);
$frames[index] = await Image.load(data);
}
loading[index] = false;
};
const clearImage = () => {
$frames[index] = "";
$frames[index] = null;
};
</script>
@ -108,18 +110,20 @@
background-color: darken($color: $bg, $amount: 5%);
}
display: flex;
align-content: center;
align-items: center;
justify-content: center;
border-radius: 10px;
border: 2px solid black;
width: 100%;
height: 100%;
background-color: $bg;
width: 15vh;
height: 15vh;
user-select: none;
cursor: pointer;
}
.box {
width: 15vh;
height: 15vh;
user-select: none;
gap: 10px;
display: flex;

View File

@ -55,14 +55,19 @@
id="sens"
step="0.01"
min="0.01"
max="1.0"
value={$config.mic_sens}
on:change={(e) => {
$config.mic_sens = e.currentTarget.valueAsNumber;
}}
max="5.0"
bind:value={$config.mic_sens}
/>
<span>{Math.trunc($config.mic_sens * 100)}%</span>
</div>
<button
id="save"
on:click={async () => {
await invoke("save_current_config");
onClose();
}}>Save</button
>
</div>
<style lang="scss">
@ -110,4 +115,12 @@
justify-content: space-between;
width: 50%;
}
#save {
position: absolute;
right: 0.25rem;
bottom: 0.25rem;
width: 25%;
height: 5rem;
}
</style>

View File

@ -2,15 +2,20 @@
import { frames } from "../store";
import { onMount } from "svelte";
import { appWindow } from "@tauri-apps/api/window";
import type { Image } from "image-js";
import anime from "animejs";
let src = "";
export let buf = 0;
export let open = false;
export let threshold = 50;
let closed = false;
let blink = false;
let inAnim = "jump-in";
let outAnim = "none";
$: bitMaps = [null, null, null, null];
$: currFrame = 0;
$: src = bitMaps[currFrame];
let pos = { x: 0, y: 0 };
$: {
if (buf < threshold) {
@ -21,19 +26,58 @@
$: {
if (closed) {
src = $frames[0];
currFrame = 0;
} else if (open) {
src = $frames[1];
currFrame = 1;
}
if (blink && closed) {
src = $frames[2] ? $frames[2] : $frames[0];
currFrame = $frames[2] ? 2 : 0;
} else if (blink && open) {
src = $frames[3] ? $frames[3] : $frames[1];
currFrame = $frames[3] ? 3 : 1;
}
}
$: {
if (open) {
anime({
targets: pos,
y: [
{ value: 50, duration: 200 },
{ value: 0, duration: 200 },
],
autoplay: true,
easing: "easeOutCubic",
});
}
}
// $: {
// if (closed)
// anime({
// targets: pos,
// y: [
// { value: 100, duration: 100 },
// { value: 0, duration: 100 },
// ],
// autoplay: true,
// easing: "easeInOutCubic",
// });
// }
const createBitmaps = async (f: Array<Image | null>) => {
return await Promise.all(
f.map(async (v) => (v ? await createImageBitmap(await v.toBlob()) : null))
);
};
onMount(async () => {
bitMaps = await createBitmaps($frames);
console.log("bitMaps: ", bitMaps);
frames.subscribe(async (f) => {
bitMaps = await createBitmaps(f);
});
await appWindow.listen("mouth-open", () => {
buf = 100;
open = true;
@ -49,57 +93,43 @@
blink = true;
setTimeout(() => (blink = false), 100);
});
await appWindow.onResized(updateCanvasSize);
updateCanvasSize();
const ctx = canvas.getContext("2d");
const update = async () => {
try {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(
src,
canvas.width / 2 - pos.x - src.width / 2,
canvas.height / 2 - pos.y - src.height / 2
);
} catch {}
requestAnimationFrame(update);
};
requestAnimationFrame(update);
});
const updateCanvasSize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
let canvas: HTMLCanvasElement;
</script>
{#if src}
<img {src} alt="tuber" class:open class:closed class="{inAnim} {outAnim}" />
{/if}
<canvas bind:this={canvas} />
<style lang="scss">
@keyframes jump-out {
0% {
transform: translateY(-50%);
}
50% {
transform: translateY(-52%);
}
100% {
transform: translateY(-50%);
}
}
@keyframes jump-in {
0% {
transform: translateY(-50%);
}
50% {
transform: translateY(-52%);
}
100% {
transform: translateY(-50%);
}
}
img {
canvas {
position: absolute;
transform: translateY(-50%);
top: 50vh;
left: calc(50vw - 200px);
width: 400px;
}
.closed.jump-out {
animation: jump-out;
animation-duration: 0.2s;
}
.open.jump-in {
animation: jump-in;
animation-duration: 0.2s;
translate: -50% -50%;
top: 50%;
left: 50%;
}
</style>

View File

@ -1,16 +1,18 @@
import {writable} from "svelte/store";
import type { Image } from "image-js";
export type BGColor ="transparent" | "blue" | "green" | "pink" | {custom: string} ;
export type Meta = {
export type Meta = {
threshold: string | null;
closeThreshold: string | null;
};
export class WebRay {
meta: Meta;
frames: string[];
frames: Array<string | null>;
public constructor(
frames: string[] = [],
frames: Array<string | null> = [null, null, null, null],
meta: Meta = { threshold: null, closeThreshold: null }
) {
this.frames = frames;
@ -33,5 +35,5 @@ export class Config {
export let frames = writable(new Array<string>(4));
export let frames = writable<Array<Image | null>>([null, null, null, null]);
export let config = writable(new Config());

View File

@ -8,13 +8,13 @@
<script lang="ts">
import { appWindow, PhysicalSize } from "@tauri-apps/api/window";
// import { listen } from "@tauri-apps/api/event";
import { onMount, onDestroy } from "svelte";
import { fly } from "svelte/transition";
import { invoke } from "@tauri-apps/api/tauri";
import { tick } from "svelte";
import { frames, WebRay } from "../store";
import { quintInOut } from "svelte/easing";
import { Image } from "image-js";
import type { UnlistenFn } from "@tauri-apps/api/event";
//components
@ -24,7 +24,7 @@
import Settings from "../components/settings.svelte";
// import Devices from "../components/devices.svelte";
let monitorTimer: NodeJS.Timer;
let monitorTimer: string | number | NodeJS.Timeout;
let active = false;
let activation = 0;
@ -52,9 +52,20 @@
});
}
const openRay = (ray: WebRay) => {
const openRay = async (ray: WebRay) => {
for (let i = 0; i < 4; i++) {
$frames[i] = ray.frames[i];
if (!ray.frames[i]) {
$frames[i] = null;
continue;
}
try {
const image = await Image.load(
`data:image/png;base64,${ray.frames[i]}`
);
$frames[i] = image;
} catch (e) {
await invoke("log", { msg: "Error loading blob: " + e.toString() });
}
}
if (ray.meta.threshold) {
$threshold = parseFloat(ray.meta.threshold);
@ -65,8 +76,8 @@
};
onMount(async () => {
await appWindow.center();
await appWindow.setMinSize(new PhysicalSize(720, 600));
focusUnlisten = await appWindow.onFocusChanged(({ payload: focused }) => {
$transparent = !focused;
});
@ -100,26 +111,28 @@
const saveRay = async () => {
let fr = new Array<string>(4);
for (let i = 0; i < 4; i++) {
$frames[i] ? (fr[i] = $frames[i]) : (fr[i] = "");
$frames[i] ? (fr[i] = $frames[i].toBase64() as string) : (fr[i] = "");
}
fr.map((e) => (e ? e : ""));
const ray = {
frames: [fr[0], fr[1], fr[2], fr[3]],
frames: fr,
meta: {
threshold: $threshold.toString(),
closeThreshold: closeThreshold.toString(),
},
};
// await invoke("log", { msg: `Saving ray: ${JSON.stringify(ray)}` });
invoke("save_ray", { ray })
.then()
.catch((e) => invoke("log", { msg: e }).then());
await invoke("log", { msg: `Saving ray: ${JSON.stringify(ray)}` });
try {
await invoke("save_ray", { ray });
} catch (e) {
await invoke("log", { msg: e });
}
};
const loadRay = async () => {
const ray: WebRay = await invoke("open_ray");
openRay(ray);
await openRay(ray);
};
</script>

View File

@ -12,6 +12,7 @@ export default defineConfig({
server: {
port: 1420,
strictPort: true,
cors: true
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand

1248
yarn.lock Normal file

File diff suppressed because it is too large Load Diff