Compare commits
45 Commits
Author | SHA1 | Date |
---|---|---|
|
92b9d8f752 | |
|
4449bacaa1 | |
|
54972b7cf7 | |
|
a22f7f0b8f | |
|
a8e9f72b49 | |
|
6b5a12c5a7 | |
|
2d03f1b5ff | |
|
828bf6bb75 | |
|
83b3b3d3ec | |
|
176eadf6d0 | |
|
40e996a7e8 | |
|
ea98b54199 | |
|
3346ed22fa | |
|
6f22890d07 | |
|
09769de7a1 | |
|
d822a1a8c0 | |
|
547dad2060 | |
|
29d9e6c4e0 | |
|
2ad75e8e90 | |
|
1557399e34 | |
|
87789399cd | |
|
6579c6a599 | |
|
97bf38165f | |
|
200f71e0ac | |
|
36db69356c | |
|
91ce120921 | |
|
79a31a161c | |
|
5f7041ddc2 | |
|
a51ae66019 | |
|
f20b32c986 | |
|
31bbf44171 | |
|
2489210068 | |
|
c5c587ad3c | |
|
14a86f4b63 | |
|
dbafaa77d6 | |
|
72ef196223 | |
|
62253bbeb6 | |
|
468cd79692 | |
|
9e6e688a97 | |
|
f60fcf0556 | |
|
1dd2eac1a0 | |
|
8452a15001 | |
|
8433a128db | |
|
849e437ae5 | |
|
990fcfbc79 |
|
@ -1,2 +1,10 @@
|
|||
node_modules/
|
||||
dist/
|
||||
bundle/
|
||||
|
||||
target/
|
||||
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[[language]]
|
||||
name = "svelte"
|
||||
auto-format = true
|
||||
|
||||
[[language]]
|
||||
name = "typescript"
|
||||
auto-format = true
|
||||
|
||||
[[language]]
|
||||
name = 'toml'
|
||||
auto-format = true
|
|
@ -0,0 +1 @@
|
|||
rust-analyzer latest
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "cathode"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[workspace]
|
||||
members = ["ray_format"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
bevy = { version = "0.10.1" }
|
||||
bevy_eventlistener = "0.2.2"
|
||||
bevy_framepace = "0.12.1"
|
||||
bevy_mod_picking = { version = "0.14", default-features = false, features = ["backend_bevy_ui", "debug"] }
|
||||
bevy_mod_scripting = { version = "0.3.0", features = ["rhai"] }
|
||||
bevy_ninepatch = "0.10.0"
|
||||
bevy_tweening = "0.7.0"
|
||||
cpal = { version = "0.15.2" }
|
||||
directories = "5.0.1"
|
||||
ray_format = { version = "0.2.0", path = "ray_format", features = ["async_std"] }
|
||||
rhai = { version = "1.14.0", features = ["sync"] }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["wayland"]
|
||||
jack = ["cpal/jack"]
|
||||
wayland = ["bevy/wayland"]
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
|
@ -1,7 +0,0 @@
|
|||
FROM node
|
||||
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 curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
|
||||
RUN mv /root/.cargo/bin/* /usr/bin
|
||||
ENTRYPOINT ["/bin/bash"]
|
38
README.md
|
@ -1,4 +1,40 @@
|
|||
# Cathode [](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
|
After Width: | Height: | Size: 500 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 302 B |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,3 @@
|
|||
pub fn update(transform) {
|
||||
|
||||
}
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -1,7 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
41
justfile
|
@ -2,19 +2,40 @@ set export
|
|||
|
||||
alias d := debug
|
||||
|
||||
watch := "cargo watch -q -c -x run"
|
||||
|
||||
dev:
|
||||
-cargo tauri dev
|
||||
{{watch}}
|
||||
|
||||
debug:
|
||||
RUST_LOG=debug cargo tauri dev
|
||||
RUST_LOG=debug {{watch}}
|
||||
|
||||
log RUST_LOG:
|
||||
cargo tauri dev
|
||||
log RUST_LOG: dev
|
||||
|
||||
build:
|
||||
cargo tauri build
|
||||
cd src-tauri
|
||||
cargo generate-rpm
|
||||
cp -r target/release/bundle ..
|
||||
cp -r target/generate-rpm ../bundle
|
||||
|
||||
cargo build
|
||||
|
||||
build-release:
|
||||
cargo build --release
|
||||
|
||||
install: build-release
|
||||
@echo Copying binary to /usr/bin/cathode...
|
||||
@sudo install target/release/cathode /usr/bin/cathode
|
||||
@echo Installing desktop file...
|
||||
@sudo install desktop/cathode-tube.desktop /usr/share/applications/cathode-tube.desktop
|
||||
@sudo install desktop/application-cathode.xml /usr/share/mime/packages/application-cathode.xml
|
||||
@echo Installing icons...
|
||||
@sudo install desktop/icons/128x128.png /usr/share/icons/hicolor/128x128/apps/cathode-tube.png
|
||||
@sudo install desktop/icons/32x32.png /usr/share/icons/hicolor/32x32/apps/cathode-tube.png
|
||||
@sudo install desktop/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
|
||||
|
|
30
package.json
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"name": "cathode-tube",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@neodrag/svelte": "^1.2.3",
|
||||
"@tauri-apps/api": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"@tauri-apps/cli": "^1.1.1",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/node": "^18.7.10",
|
||||
"sass": "^1.54.8",
|
||||
"svelte": "^3.49.0",
|
||||
"svelte-check": "^2.8.0",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.2"
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 330 330" style="enable-background:new 0 0 330 330;" xml:space="preserve">
|
||||
<g id="XMLID_894_">
|
||||
<g id="XMLID_895_">
|
||||
<path id="XMLID_896_" d="M164.998,210c35.888,0,65.085-29.195,65.085-65.12l-0.204-80c0-35.775-29.105-64.88-64.881-64.88
|
||||
c-35.773,0-64.877,29.105-64.877,64.842l-0.203,80.076C99.918,180.805,129.112,210,164.998,210z M130.121,64.88
|
||||
c0-19.233,15.646-34.88,34.877-34.88c19.233,0,34.881,15.647,34.881,34.919l0.204,80c0,19.344-15.739,35.081-35.085,35.081
|
||||
c-19.343,0-35.08-15.737-35.08-35.044L130.121,64.88z"/>
|
||||
</g>
|
||||
<g id="XMLID_899_">
|
||||
<path id="XMLID_900_" d="M280.084,154.96c0-8.284-6.716-15-15-15c-8.284,0-15,6.716-15,15c0,46.732-37.878,84.774-84.546,85.068
|
||||
c-0.181-0.006-0.357-0.027-0.54-0.027c-0.183,0-0.359,0.021-0.541,0.027c-46.665-0.293-84.541-38.335-84.541-85.068
|
||||
c0-8.284-6.716-15-15-15s-15,6.716-15,15c0,58.373,43.688,106.731,100.082,114.104V300H117c-8.284,0-15,6.716-15,15
|
||||
s6.716,15,15,15h96.001c8.284,0,15-6.716,15-15s-6.716-15-15-15h-33.003v-30.936C236.395,261.69,280.084,213.332,280.084,154.96z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1 +0,0 @@
|
|||
<svg width="64px" height="64px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--emojione-monotone" preserveAspectRatio="xMidYMid meet"><path d="M32 2C15.432 2 2 15.432 2 32s13.432 30 30 30s30-13.432 30-30S48.568 2 32 2zm0 57.5C16.836 59.5 4.5 47.164 4.5 32S16.836 4.5 32 4.5S59.5 16.836 59.5 32S47.164 59.5 32 59.5z" fill="currentColor"></path><circle cx="32" cy="45.139" r="7" fill="currentColor"></circle><circle cx="20.248" cy="25.045" r="4.5" fill="currentColor"></circle><circle cx="42.75" cy="25.045" r="4.5" fill="currentColor"></circle></svg>
|
Before Width: | Height: | Size: 654 B |
|
@ -0,0 +1 @@
|
|||
DATABASE_URL=sqlite:test.db
|
|
@ -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"]
|
||||
|
@ -8,9 +8,20 @@ authors = ["AnActualEmerald"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.62"
|
||||
log = "0.4.17"
|
||||
qoi = "0.4.0"
|
||||
serde = "1.0.144"
|
||||
serde_json = "1.0.85"
|
||||
image = "0.24.6"
|
||||
serde = {version="1.0.144", features = ["derive"]}
|
||||
serde_json = "1.0.96"
|
||||
sha256 = "1.1.2"
|
||||
thiserror = "1.0.40"
|
||||
tracing = "0.1.37"
|
||||
uuid = { version = "1.3.3", features = ["v4", "serde"] }
|
||||
zip = "0.6.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.2"
|
||||
tracing-test = { version = "0.2.4"}
|
||||
|
||||
[features]
|
||||
tokio = []
|
||||
async_std = []
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
-- Add migration script here
|
||||
CREATE TABLE IF NOT EXISTS images
|
||||
(
|
||||
id VARCHAR PRIMARY KEY,
|
||||
image BLOB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS animations
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(32) NOT NULL,
|
||||
animation VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS expressions
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
imageId INTEGER NOT NULL REFERENCES images(id),
|
||||
inAnimation INTEGER REFERENCES animations(id),
|
||||
outAnimation INTEGER REFERENCES animations(id),
|
||||
idleAnimation INTEGER REFERENCES animations(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meta
|
||||
(
|
||||
format_version INTEGER PRIMARY KEY
|
||||
);
|
||||
|
||||
INSERT INTO meta VALUES (1) ON CONFLICT DO NOTHING;
|
|
@ -1,53 +1,236 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
|
||||
use zip::{write::FileOptions, ZipArchive, ZipWriter};
|
||||
use crate::error::Error;
|
||||
use crate::model::Expression;
|
||||
use crate::model::Frame;
|
||||
use image::io::Reader as ImageReader;
|
||||
use uuid::Uuid;
|
||||
use zip::write::FileOptions;
|
||||
use zip::{ZipArchive, ZipWriter};
|
||||
|
||||
pub(crate) struct Archive {
|
||||
buffer: Vec<u8>,
|
||||
#[derive(Debug)]
|
||||
pub struct Archive<T> {
|
||||
buffer: T,
|
||||
}
|
||||
|
||||
impl Archive {
|
||||
pub(crate) fn open(data: &[u8]) -> Self {
|
||||
Self {
|
||||
buffer: data.to_vec(),
|
||||
impl Archive<Vec<u8>> {
|
||||
pub fn open_buf(mut buffer: Vec<u8>) -> Result<Self, Error> {
|
||||
ZipWriter::new(Cursor::new(&mut buffer));
|
||||
let archive = Self { buffer };
|
||||
|
||||
Ok(archive)
|
||||
}
|
||||
|
||||
pub fn get_image(&self, id: &str) -> Result<Vec<u8>, Error> {
|
||||
let mut archive = self.get_reader()?;
|
||||
let mut img = archive.by_name(format!("images/{}", id).as_str())?;
|
||||
let mut buf = vec![];
|
||||
img.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn add_image(&mut self, data: &[u8]) -> Result<String, Error> {
|
||||
let decoded = ImageReader::new(Cursor::new(data))
|
||||
.with_guessed_format()?
|
||||
.decode()?;
|
||||
|
||||
// all images are re-encoded to qoi
|
||||
let mut qoi = vec![];
|
||||
decoded.write_to(&mut Cursor::new(&mut qoi), image::ImageOutputFormat::Qoi)?;
|
||||
|
||||
let id = sha256::digest(qoi.as_slice());
|
||||
|
||||
let mut archive = self.get_writer()?;
|
||||
|
||||
archive.start_file(format!("images/{id}"), FileOptions::default())?;
|
||||
archive.write_all(&qoi)?;
|
||||
archive.finish()?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn get_frame(&self, id: &str) -> Result<Frame, Error> {
|
||||
let mut archive = self.get_reader()?;
|
||||
|
||||
let file = archive.by_name(format!("frames/{id}.frame").as_str())?;
|
||||
let raw = file.extra_data();
|
||||
|
||||
serde_json::from_slice(raw).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn get_expression(&self, name: Option<&str>) -> Result<Expression, Error> {
|
||||
let name = name.unwrap_or("default");
|
||||
if let Ok(mut file) = self.get_reader()?.by_name(format!("{name}.exp").as_str()) {
|
||||
let mut buf = vec![];
|
||||
file.read_to_end(&mut buf)?;
|
||||
|
||||
Ok(serde_json::from_slice(&buf)?)
|
||||
} else {
|
||||
Ok(Expression::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut buffer = vec![];
|
||||
ZipWriter::new(Cursor::new(&mut buffer));
|
||||
Self { buffer }
|
||||
pub fn show_expressions(&self) -> Result<Vec<String>, Error> {
|
||||
let archive = self.get_reader()?;
|
||||
|
||||
let mut res = vec![];
|
||||
for name in archive.file_names() {
|
||||
if name.ends_with(".exp") && !name.contains('/') {
|
||||
res.push(
|
||||
name.strip_suffix(".exp")
|
||||
.ok_or_else(|| {
|
||||
Error::OtherError("Expected file name to end in '.exp'".into())
|
||||
})?
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) fn buffer(&self) -> &[u8] {
|
||||
self.buffer.as_ref()
|
||||
}
|
||||
pub fn write_frame(&mut self, frame: &Frame) -> Result<(), Error> {
|
||||
let mut archive = self.get_writer()?;
|
||||
|
||||
pub(crate) fn add_file(&mut self, file_name: &str, data: &[u8]) -> Result<()> {
|
||||
let mut zip = ZipWriter::new_append(Cursor::new(&mut self.buffer))?;
|
||||
let options = FileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Bzip2)
|
||||
.compression_level(Some(9));
|
||||
zip.start_file(file_name, options)?;
|
||||
zip.write_all(data)?;
|
||||
zip.finish()?;
|
||||
let stringy = serde_json::to_string(frame)?;
|
||||
|
||||
archive.start_file(format!("frames/{}.frame", frame.id), FileOptions::default())?;
|
||||
archive.write_all(stringy.as_bytes())?;
|
||||
archive.finish()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_file(&self, file_name: &str) -> Result<Vec<u8>> {
|
||||
let mut buffer = vec![];
|
||||
let mut zip = ZipArchive::new(Cursor::new(&self.buffer))?;
|
||||
pub fn new_frame(
|
||||
&mut self,
|
||||
image: &[u8],
|
||||
in_animation: Option<String>,
|
||||
out_animation: Option<String>,
|
||||
idle_animation: Option<String>,
|
||||
) -> Result<Frame, Error> {
|
||||
let image_id = self.add_image(image)?;
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
let mut f = zip
|
||||
.by_name(file_name)
|
||||
.context("Couldn't find a file by that name")?;
|
||||
io::copy(&mut f, &mut Cursor::new(&mut buffer))?;
|
||||
let frame = Frame {
|
||||
id,
|
||||
image: image_id,
|
||||
in_animation,
|
||||
out_animation,
|
||||
idle_animation,
|
||||
};
|
||||
|
||||
Ok(buffer)
|
||||
self.write_frame(&frame)?;
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
pub fn write_expression(&mut self, name: Option<&str>, exp: &Expression) -> Result<(), Error> {
|
||||
let mut archive = self.get_writer()?;
|
||||
|
||||
archive.start_file(
|
||||
format!("{}.exp", name.unwrap_or("default")),
|
||||
FileOptions::default(),
|
||||
)?;
|
||||
archive.write_all(&serde_json::to_vec(exp)?)?;
|
||||
archive.finish()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_reader(&self) -> Result<ZipArchive<Cursor<&Vec<u8>>>, Error> {
|
||||
ZipArchive::new(Cursor::new(&self.buffer)).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn get_writer(&mut self) -> Result<ZipWriter<Cursor<&mut Vec<u8>>>, Error> {
|
||||
ZipWriter::new_append(Cursor::new(&mut self.buffer)).map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::io::Cursor;
|
||||
|
||||
use image::io::Reader as ImageReader;
|
||||
use image::{codecs::png::PngEncoder, ImageEncoder};
|
||||
use tracing_test::traced_test;
|
||||
|
||||
use crate::model::Expression;
|
||||
|
||||
use super::Archive;
|
||||
|
||||
#[test]
|
||||
fn get_default_expression() {
|
||||
let archive = Archive::open_buf(Vec::with_capacity(1024)).unwrap();
|
||||
|
||||
let res = archive.get_expression(None).unwrap();
|
||||
|
||||
assert_eq!(res, Expression::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_frame() {
|
||||
let mut archive = Archive::open_buf(Vec::with_capacity(1024)).unwrap();
|
||||
|
||||
let test_data = image::RgbImage::from_fn(256, 256, |x, y| {
|
||||
if x % 2 == 0 && y % 2 == 0 {
|
||||
[255, 0, 255].into()
|
||||
} else {
|
||||
[255, 0, 255].into()
|
||||
}
|
||||
});
|
||||
|
||||
let mut buf = Cursor::new(vec![]);
|
||||
PngEncoder::new(&mut buf)
|
||||
.write_image(test_data.as_raw(), 256, 256, image::ColorType::Rgb8)
|
||||
.unwrap();
|
||||
|
||||
let exp = archive.new_frame(buf.get_ref(), None, None, None).unwrap();
|
||||
|
||||
let raw_image = archive.get_image(&exp.image).unwrap();
|
||||
|
||||
let image = ImageReader::new(Cursor::new(raw_image))
|
||||
.with_guessed_format()
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(test_data, image.into_rgb8());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[traced_test]
|
||||
fn add_image() {
|
||||
let archive = Archive::open_buf(Vec::with_capacity(1024));
|
||||
|
||||
assert!(archive.is_ok());
|
||||
|
||||
let mut archive = archive.unwrap();
|
||||
|
||||
let test_data = image::RgbImage::from_fn(256, 256, |x, y| {
|
||||
if x % 2 == 0 && y % 2 == 0 {
|
||||
[255, 0, 255].into()
|
||||
} else {
|
||||
[255, 0, 255].into()
|
||||
}
|
||||
});
|
||||
|
||||
let mut buf = Cursor::new(vec![]);
|
||||
PngEncoder::new(&mut buf)
|
||||
.write_image(test_data.as_raw(), 256, 256, image::ColorType::Rgb8)
|
||||
.unwrap();
|
||||
|
||||
let id = archive.add_image(&buf.into_inner());
|
||||
|
||||
let res = archive.get_image(&id.unwrap());
|
||||
|
||||
let returned_image = ImageReader::new(Cursor::new(res.unwrap()))
|
||||
.with_guessed_format()
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(test_data, returned_image.into_rgb8());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
use std::string::FromUtf8Error;
|
||||
|
||||
use thiserror::Error;
|
||||
use zip::result::ZipError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Error with image data: {0}")]
|
||||
ImageError(#[from] image::ImageError),
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Error parsing string: {0}")]
|
||||
Utf8Error(#[from] FromUtf8Error),
|
||||
#[error("Error reading zip file: {0}")]
|
||||
ZipError(#[from] ZipError),
|
||||
#[error("Error parsing JSON: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
#[error("Unknown error: {0}")]
|
||||
OtherError(String),
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
mod archive;
|
||||
mod manage;
|
||||
mod model;
|
||||
|
||||
pub use model::Ray;
|
||||
pub mod error;
|
||||
pub use archive::Archive as Ray;
|
||||
pub use model::*;
|
||||
|
|
|
@ -1,80 +1,20 @@
|
|||
use anyhow::Result;
|
||||
use std::{
|
||||
collections::{hash_map::Keys, HashMap},
|
||||
fs,
|
||||
path::Path,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::archive::Archive;
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Ray {
|
||||
frames: [Vec<u8>; 4],
|
||||
extras: HashMap<String, Vec<u8>>,
|
||||
meta: HashMap<String, String>,
|
||||
/// An frame with the id of its relevant images and animations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Frame {
|
||||
pub id: Uuid,
|
||||
pub image: String,
|
||||
pub in_animation: Option<String>,
|
||||
pub out_animation: Option<String>,
|
||||
pub idle_animation: Option<String>,
|
||||
}
|
||||
|
||||
impl Ray {
|
||||
pub fn get_meta_keys(&self) -> Keys<String, String> {
|
||||
self.meta.keys()
|
||||
}
|
||||
|
||||
pub fn get_meta_value(&self, key: &str) -> Option<&String> {
|
||||
self.meta.get(key)
|
||||
}
|
||||
|
||||
pub fn get_frame(&self, index: usize) -> Option<&Vec<u8>> {
|
||||
self.frames.get(index)
|
||||
}
|
||||
|
||||
pub fn set_frame(&mut self, index: usize, data: Vec<u8>) -> bool {
|
||||
if let Some(i) = self.frames.get_mut(index) {
|
||||
*i = data;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_meta(&mut self, key: String, value: String) -> 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)?;
|
||||
}
|
||||
|
||||
let meta = serde_json::to_string(&self.meta)?;
|
||||
archive.add_file("meta.json", meta.as_bytes())?;
|
||||
|
||||
fs::write(path, archive.buffer())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let mut ray = Self::default();
|
||||
let data = fs::read(path.as_ref())?;
|
||||
let archive = Archive::open(&data);
|
||||
|
||||
for i in 0..4 {
|
||||
if let Ok(buf) = archive.get_file(&i.to_string()) {
|
||||
ray.set_frame(i as usize, buf);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(buf) = archive.get_file("meta.json") {
|
||||
let meta: HashMap<String, String> = serde_json::from_slice(&buf)?;
|
||||
ray.meta = meta;
|
||||
}
|
||||
|
||||
Ok(ray)
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub struct Expression {
|
||||
pub mouth_closed_eyes_open: Option<Uuid>,
|
||||
pub mouth_open_eyes_open: Option<Uuid>,
|
||||
pub mouth_closed_eyes_closed: Option<Uuid>,
|
||||
pub mouth_open_eyes_closed: Option<Uuid>,
|
||||
}
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
use std::io::Cursor;
|
||||
|
||||
use crate::archive::Archive;
|
||||
use image::{DynamicImage, ImageBuffer, ImageFormat};
|
||||
|
||||
#[test]
|
||||
fn add_file_to_archive() {
|
||||
let mut archive = Archive::new();
|
||||
let res = archive.add_file("hello", b"world");
|
||||
#[sqlx::test]
|
||||
async fn add_image() {
|
||||
let mut archive = Archive::open(":memory:").await.expect("Failed to open db");
|
||||
|
||||
let img_buf = DynamicImage::ImageRgb8(ImageBuffer::from_fn(64, 64, |x, _| {
|
||||
if x % 2 == 0 {
|
||||
image::Rgb([0u8, 0u8, 0u8])
|
||||
} else {
|
||||
image::Rgb([255u8, 255u8, 255u8])
|
||||
}
|
||||
}));
|
||||
|
||||
let mut buf = vec![];
|
||||
img_buf
|
||||
.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)
|
||||
.unwrap();
|
||||
|
||||
let res = archive.add_image(&buf).await;
|
||||
println!("{:?}", res);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_file_from_archive() {
|
||||
let mut archive = Archive::new();
|
||||
archive.add_file("hello", b"world").unwrap();
|
||||
let file = archive.get_file("hello");
|
||||
|
||||
assert!(file.is_ok());
|
||||
assert_eq!(
|
||||
String::from("world"),
|
||||
String::from_utf8(file.unwrap()).unwrap()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
[package]
|
||||
name = "cathode-tube"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["AnActualEmerald"]
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/AnActualEmerald/cathode"
|
||||
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"},
|
||||
|
||||
]
|
||||
|
||||
|
||||
[workspace]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.1.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"] }
|
||||
ray_format = {path = "../ray_format", version = "~0.1.0"}
|
||||
anyhow = "1.0.65"
|
||||
log = "0.4.17"
|
||||
env_logger = "0.9.1"
|
||||
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"
|
||||
|
||||
[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" ]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/cathode">
|
||||
<comment>Cathode RAY file</comment>
|
||||
<sub-class-of type="application/zip" />
|
||||
<icon name="application-cathode" />
|
||||
<acronym>RAY</acronym>
|
||||
<glob pattern="*.ray"/>
|
||||
</mime-type>
|
||||
</mime-info>
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Icon=cathode-tube
|
||||
Name=Cathode
|
||||
Exec=cathode-tube %U
|
||||
Terminal=false
|
||||
Hidden=false
|
||||
Categories=Graphics; Video
|
||||
Comment=Small app for PNG-tubing
|
||||
MimeType=application/cathode
|
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 26 KiB |
|
@ -1,54 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use cpal::traits::StreamTrait;
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use cpal::Device;
|
||||
use cpal::InputCallbackInfo;
|
||||
use log::debug;
|
||||
use tauri::Window;
|
||||
|
||||
pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mutex<f32>>) {
|
||||
let device = initialize().expect("Unable to init audio");
|
||||
debug!("Using device {}", device.name().unwrap());
|
||||
let config = device.default_input_config().unwrap();
|
||||
let stream = device
|
||||
.build_input_stream(
|
||||
&config.config(),
|
||||
move |data: &[f32], _: &InputCallbackInfo| {
|
||||
if data.iter().any(|e| e.abs() >= *threshold.lock().unwrap()) {
|
||||
window.emit("mouth-open", "").unwrap();
|
||||
} else {
|
||||
window.emit("mouth-close", "").unwrap();
|
||||
}
|
||||
|
||||
*level.lock().unwrap() = data
|
||||
.iter()
|
||||
.map(|e| e.abs())
|
||||
.max_by(|a, b| a.total_cmp(&b))
|
||||
.unwrap()
|
||||
.clone();
|
||||
},
|
||||
move |err| {
|
||||
println!("Audio error: {:?}", err);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||