Compare commits
12 Commits
Author | SHA1 | Date |
---|---|---|
Emerald | 47b33853f3 | |
Emerald | 6ddb397c22 | |
Emerald | b1329697bf | |
Emerald | a7a897495a | |
Emerald | 0c7c57a0fd | |
Emerald | 038c20600d | |
Emerald | 7cc2aa022a | |
Emerald | c12b2af269 | |
Emerald | 594f321c57 | |
Emerald | 7dd67cd8b5 | |
Emerald | 5f51d0b1d6 | |
emerald | 1a27172cc9 |
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
dist/
|
||||
bundle/
|
||||
target/
|
|
@ -0,0 +1,2 @@
|
|||
node 20
|
||||
yarn latest
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -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 -->
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
26
justfile
26
justfile
|
@ -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}}"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Icon=cathode-tube
|
||||
Icon=cathode
|
||||
Name=Cathode
|
||||
Exec=cathode %U
|
||||
Terminal=false
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
10
src/store.ts
10
src/store.ts
|
@ -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());
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue