Compare commits

...

57 Commits

Author SHA1 Message Date
emerald aa72b4a7cf
add just to flake
ci/woodpecker/push/build Pipeline was successful Details
2024-05-05 02:13:22 -04:00
emerald b4ed39965f
nixify
ci/woodpecker/push/build Pipeline was successful Details
2024-05-05 02:03:56 -04:00
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
emerald e3fdda3f0f [CI SKIP] dev (#5)
ci/woodpecker/tag/build Pipeline was successful Details
ci/woodpecker/tag/publish Pipeline failed Details
Co-authored-by: emerald <emerald_actual@proton.me>
Reviewed-on: https://gitea.greenboi.me/emerald/cathode/pulls/5
2022-10-21 02:21:37 -04:00
emerald c3be26e5fa
remove random double method declaration
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/push/publish Pipeline was successful Details
2022-10-16 01:09:35 -04:00
emerald 1711e57f5d
remove package-lock form gitignore
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/push/publish Pipeline was successful Details
2022-10-16 01:06:18 -04:00
emerald efc513a249
merge basic settings work
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/push/publish Pipeline was successful Details
2022-10-16 01:05:28 -04:00
emerald db171e0549
basic settings menu
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-10-15 01:05:33 -04:00
emerald 2ca5380e85 Update 'README.md'
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-10-12 16:56:56 -04:00
emerald a28bf6b63f
Make ray loading more consistent
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-10-12 15:18:49 -04:00
emerald 4b2b0c14ec
Create changelog 2022-10-12 15:18:13 -04:00
emerald 23a700a4d8
add debugging recipes 2022-10-12 11:44:44 -04:00
emerald 562030bed3
move config to store 2022-10-12 10:53:14 -04:00
emerald df884108e2
add blink interval to config 2022-10-12 10:34:19 -04:00
emerald 56f7e1f11b
add configuration with hot reload 2022-10-05 14:10:18 -04:00
emerald b0d82ea050
add CI build image 2022-10-05 11:24:29 -04:00
emerald a5fcfde5af
add justfile 2022-10-05 11:24:02 -04:00
emerald 8654fe5070
shorten blink interval, allow for adjusting blink interval
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-25 20:06:52 -04:00
emerald ce7f3fd010
make everything unselectable, make context menus not useless 2022-09-25 20:06:14 -04:00
emerald 9b105a1f88
remove extra images and unneeded html boilerplate 2022-09-25 19:12:26 -04:00
emerald e08394d68a
add rpm generation 2022-09-25 19:11:32 -04:00
emerald acb76b4441
move publish pipeline to custom image
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/push/publish Pipeline was successful Details
2022-09-25 16:27:22 -04:00
emerald 1703f92a3d
always pull image
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/push/publish Pipeline was successful Details
2022-09-25 15:15:37 -04:00
emerald 09a08554d4
save tauri cli as npm dev dependency
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/push/publish Pipeline was successful Details
2022-09-25 12:58:49 -04:00
emerald 080f8d1edc
move to custom build env image
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/push/publish Pipeline was successful Details
2022-09-25 11:02:38 -04:00
emerald d532f76d01 separate build and publish pipelines
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/push/publish Pipeline was successful Details
2022-09-25 02:22:30 -04:00
Emerald d7912d12e7 Correct build artifact file names
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-20 01:25:06 +00:00
AnActualEmerald 6be45162e3
remove extra images
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-09-17 13:57:58 -04:00
AnActualEmerald ef1211731f
style load and save buttons
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-17 13:52:30 -04:00
AnActualEmerald 20af8b8164
add logic for handling opening files
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-17 12:45:26 -04:00
AnActualEmerald e08e508eda
add custom desktop file
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-17 12:09:10 -04:00
AnActualEmerald 7e991bd5e6
bump deps 2022-09-17 12:07:27 -04:00
Emerald c42ee4933e add mimetype to deb bundle
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-16 20:09:28 +00:00
Emerald 251aa384ad Add mimetype fie
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-16 20:06:34 +00:00
AnActualEmerald d213bf787f
implement file loading
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-16 14:17:27 -04:00
AnActualEmerald e4770a8894
start file saving work
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-15 03:22:43 -04:00
AnActualEmerald f2486208c7
add icons to interface
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-15 02:06:23 -04:00
AnActualEmerald 10f0ac0576
remove extra printlns
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-14 02:14:06 -04:00
AnActualEmerald 1739faf1f7
Update deps
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-14 02:09:46 -04:00
AnActualEmerald ea076f468f
Add icons 2022-09-14 02:09:10 -04:00
AnActualEmerald f21509c896
add second bar for mouth-close threshold
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-13 10:00:32 -04:00
AnActualEmerald d02ca038a9
handle window resizing
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-13 01:13:51 -04:00
Emerald d9e79f5e07 add handle to setpoint
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-13 02:30:08 +00:00
Emerald 99dc3b3e61 improve audio monitor accuracy
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-13 02:03:09 +00:00
Emerald bc70d51411 Update 'README.md'
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-12 19:56:54 +00:00
AnActualEmerald 4b94c7799f
make pipeline only run on main branch
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-09-11 22:09:01 -04:00
39 changed files with 4080 additions and 3811 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/
package-lock.json
dist/
bundle/
target/

3
.helix/languages.toml Normal file
View File

@ -0,0 +1,3 @@
[[language]]
name = "svelte"
auto-format = true

2
.tool-versions Normal file
View File

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

View File

@ -1,26 +0,0 @@
branches: main
pipeline:
setup:
image: ivangabriele/tauri:bullseye-node18
when:
event: [tag]
commands:
- npm i -D @tauri-apps/cli
build:
image: ivangabriele/tauri:bullseye-node18
when:
event: [tag]
commands:
- apt install libasound2-dev
- npm ci
- npm run tauri build
upload:
image: alpine/curl
when:
event: [tag]
secrets: [gitea_key]
commands:
- curl --user Emerald:$GITEA_KEY --upload-file target/release/bundle/deb/*.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/*.AppImage https://gitea.greenboi.me/api/packages/Emerald/generic/cathode/${CI_COMMIT_TAG##v}/cathode-tube.AppImage

12
.woodpecker/build.yml Normal file
View File

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

34
.woodpecker/publish.yml Normal file
View File

@ -0,0 +1,34 @@
when:
- event: [ tag, manual ]
branch: main
- event: deployment
environment: production
steps:
build:
image: forge.greenboi.me/emerald/cathode-build:latest
pull: true
commands:
- yarn install --frozen-lockfile
- yarn tauri build
- cd src-tauri && cargo generate-rpm
upload:
image: alpine/curl
secrets: [gitea_key]
when:
- evaluate: 'CI_COMMIT_TAG matches "^v[[:digit:]]+[.][[:digit:]]+[.][[:digit:]]+.* "'
commands:
- 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

15
CHANGELOG.md Normal file
View File

@ -0,0 +1,15 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.2.0] - 2023-10-12
### Features
- Use canvas to render png
### Miscellaneous Tasks
- Update woodpecker pipeline syntax
<!-- generated by git-cliff -->

9
Dockerfile.build Normal file
View File

@ -0,0 +1,9 @@
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 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"]

View File

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

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

16
dist/index.html vendored
View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Svelte + TS</title>
<script type="module" crossorigin src="/assets/index.4fc3067b.js"></script>
<link rel="stylesheet" href="/assets/index.425121d3.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

1
dist/svelte.svg vendored
View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

6
dist/tauri.svg vendored
View File

@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

1
dist/vite.svg vendored
View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

61
flake.lock Normal file
View File

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714763106,
"narHash": "sha256-DrDHo74uTycfpAF+/qxZAMlP/Cpe04BVioJb6fdI0YY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e9be42459999a253a9f92559b1f5b72e1b44c13d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

41
flake.nix Normal file
View File

@ -0,0 +1,41 @@
{
description = "A very basic flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {inherit system;};
buildDeps = with pkgs; [
webkitgtk
gtk3
cairo
gdk-pixbuf
glib
dbus
openssl_3
librsvg
libsoup
];
in {
formatter = pkgs.alejandra;
devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [rustc nodejs pkg-config];
buildInputs = buildDeps;
packages = with pkgs; [cargo cargo-tauri yarn just];
shellHook = ''
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath buildDeps}:$LD_LIBRARY_PATH
export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS
'';
};
});
}

57
justfile Normal file
View File

@ -0,0 +1,57 @@
set export
alias d := debug
dev:
-cargo tauri dev
debug:
RUST_LOG=debug yarn tauri dev
log RUST_LOG:
yarn tauri dev
build:
yarn tauri build
cd src-tauri && cargo generate-rpm
install:
@cargo tauri build -b none
@echo Copying binary to /usr/bin/cathode...
@sudo cp src-tauri/target/release/cathode /usr/bin/cathode
@echo Installing desktop file...
@sudo cp src-tauri/cathode-tube.desktop /usr/share/applications/cathode-tube.desktop
@sudo cp src-tauri/application-cathode.xml /usr/share/mime/packages/application-cathode.xml
@echo Installing icons...
@sudo cp src-tauri/icons/128x128.png /usr/share/icons/hicolor/128x128/apps/cathode-tube.png
@sudo cp src-tauri/icons/32x32.png /usr/share/icons/hicolor/32x32/apps/cathode-tube.png
@sudo cp src-tauri/icons/128x128@2x.png /usr/share/icons/hicolor/256x256/apps/cathode-tube.png
@sudo update-desktop-database
uninstall:
@echo Removing cathode...
@sudo rm -f /usr/bin/cathode
@sudo rm -f /usr/share/applications/cathode-tube.desktop
@sudo rm -f /usr/share/mime/packages/application-cathode.xml
@sudo rm -f /usr/share/icons/hicolor/128x128/apps/cathode-tube.png
@sudo rm -f /usr/share/icons/hicolor/32x32/apps/cathode-tube.png
@sudo rm -f /usr/share/icons/hicolor/256x256/apps/cathode-tube.png
@sudo update-desktop-database
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}}"

2678
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.0.5",
"@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",

2379
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,30 @@
[package]
name = "cathode-tube"
version = "0.0.1"
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"
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.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.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"
[package.metadata.generate-rpm.requires]
filesystem = ">= 3"
gtk3 = ">= 3"
webkit2gtk3 = ">= 2"
[workspace]
[profile.release]
@ -16,21 +34,24 @@ lto = true
codegen-units = 1
[build-dependencies]
tauri-build = { version = "1.0.0", features = [] }
tauri-build = { version = "1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0", features = ["cli", "dialog-all", "fs-create-dir", "fs-read-dir", "fs-read-file", "fs-write-file", "macos-private-api", "window-minimize", "window-set-max-size", "window-set-min-size", "window-unminimize"] }
cpal = { version = "0.14.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.63"
anyhow = "1.0.66"
log = "0.4.17"
env_logger = "0.9.0"
env_logger = "0.9.3"
rand = "0.8.5"
tokio = { version = "1.21.0", features = ["full"] }
image = "0.24.3"
base64-url = "1.4.13"
tokio = { version = "1.21.2", features = ["full"] }
image = "0.24.4"
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,8 +1,8 @@
[Desktop Entry]
Type=Application
Icon=cathode-tube
Icon=cathode
Name=Cathode
Exec=cathode-tube %U
Exec=cathode %U
Terminal=false
Hidden=false
Categories=Graphics; Video

View File

@ -12,7 +12,12 @@ use cpal::InputCallbackInfo;
use log::debug;
use tauri::Window;
pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mutex<f32>>) {
pub fn monitor(
window: Window,
threshold: Arc<Mutex<f32>>,
level: Arc<Mutex<f32>>,
sens: Arc<Mutex<f32>>,
) -> ! {
let device = initialize().expect("Unable to init audio");
debug!("Using device {}", device.name().unwrap());
let config = device.default_input_config().unwrap();
@ -20,7 +25,10 @@ pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mute
.build_input_stream(
&config.config(),
move |data: &[f32], _: &InputCallbackInfo| {
if data.iter().any(|e| e.abs() >= *threshold.lock().unwrap()) {
if data
.iter()
.any(|e| (e.abs() * *sens.lock().unwrap()) >= *threshold.lock().unwrap())
{
window.emit("mouth-open", "").unwrap();
} else {
window.emit("mouth-close", "").unwrap();
@ -28,10 +36,9 @@ pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mute
*level.lock().unwrap() = data
.iter()
.map(|e| e.abs())
.map(|e| e.abs() * *sens.lock().unwrap())
.max_by(|a, b| a.total_cmp(&b))
.unwrap()
.clone();
.unwrap();
},
move |err| {
println!("Audio error: {:?}", err);
@ -41,14 +48,24 @@ pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mute
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(60));
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"))
}
#[tauri::command]
pub fn get_devices() -> Vec<String> {
let host = cpal::default_host();
host.input_devices()
.unwrap()
.filter_map(|e| e.name().ok())
.collect()
}

91
src-tauri/src/config.rs Normal file
View File

@ -0,0 +1,91 @@
use std::{fmt::Display, path::PathBuf};
use anyhow::Result;
use figment::{
providers::{Env, Format, Toml},
Figment,
};
use log::{debug, error, trace, warn};
use serde::{Deserialize, Serialize};
use tauri::api::path::config_dir;
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Config {
#[serde(default)]
pub background_color: BGColor,
#[serde(default = "default_interval")]
pub blink_interval: u64,
#[serde(default = "default_sens")]
pub mic_sens: f32,
}
#[inline(always)]
fn default_interval() -> u64 {
1500
}
#[inline(always)]
fn default_sens() -> f32 {
1.0
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BGColor {
Transparent,
Green,
Blue,
Pink,
Custom(String),
}
impl Default for BGColor {
fn default() -> Self {
Self::Transparent
}
}
impl Display for BGColor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Custom(c) => {
write!(f, "{}", c)
}
_ => {
write!(f, "{}", self.to_string().to_lowercase())
}
}
}
}
#[inline]
pub fn get_config_dir() -> PathBuf {
config_dir()
.expect("Unable to get config directory")
.join("cathode")
}
pub fn load_config() -> Config {
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()
})
}
pub fn save_config(config: &Config) -> Result<()> {
use std::fs;
let raw = toml::to_string_pretty(config)?;
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,16 +1,17 @@
use std::collections::HashMap;
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::home_dir;
const OBJ_URL: &'static str = "data:image/png;base64,";
use tauri::api::path::{cache_dir, home_dir, picture_dir};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(crate) struct WebRay {
@ -20,18 +21,28 @@ pub(crate) struct WebRay {
#[tauri::command]
pub(crate) async fn open_image() -> Option<String> {
debug!("Opening iamge dialog...");
let path = FileDialogBuilder::new()
.add_filter("Images", &["png", "jpg"])
.set_directory(home_dir()?)
.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
}
@ -44,11 +55,11 @@ pub(crate) async fn open_ray() -> Option<WebRay> {
.set_directory(home_dir()?)
.pick_file()?;
load_ray(path).await
load_ray(path)
}
pub(crate) async fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
let ray = Ray::load(path).ok()?;
pub(crate) fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
let ray = Ray::load(path.as_ref()).ok()?;
let mut frames = [String::new(), String::new(), String::new(), String::new()];
let mut meta = HashMap::new();
@ -60,9 +71,9 @@ pub(crate) async 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;
}
}
@ -72,6 +83,10 @@ pub(crate) async fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
}
}
if let Err(e) = cache_loaded_ray(path.as_ref()) {
error!("Error caching loaded ray: {e}");
}
Some(WebRay { frames, meta })
}
@ -87,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

@ -4,63 +4,142 @@
)]
use std::{
path::Path,
path::{Path, PathBuf},
sync::{Arc, Mutex},
thread::sleep,
time::Duration,
};
use audio::monitor;
use log::{trace, warn};
use fs::WebRay;
use log::{debug, error, trace, warn};
use serde_json::Value;
use tauri::{Manager, State};
use tauri::{
api::path::{cache_dir, config_dir},
Manager, State, WindowEvent,
};
use crate::config::Config;
mod audio;
mod config;
mod fs;
const MIC_THRESHOLD: f32 = 0.5f32;
struct MicThreshold(Arc<Mutex<f32>>);
struct AudioLevel(Arc<Mutex<f32>>);
struct BlinkInterval(Arc<Mutex<u64>>);
struct CurrentConfig(Arc<Mutex<Config>>);
struct RayToLoad(Arc<Mutex<Option<PathBuf>>>);
struct MicSense(Arc<Mutex<f32>>);
fn main() {
env_logger::init();
let threshold = Arc::new(Mutex::new(MIC_THRESHOLD));
let level = Arc::new(Mutex::new(0.));
let blink_interval = Arc::new(Mutex::new(1500));
let ray = Arc::new(Mutex::new(None));
let sens = Arc::new(Mutex::new(1.0));
if let Some(d) = cache_dir() {
use std::fs;
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());
}
}
debug!("Loading existing config");
let config = config::load_config();
let current_conf = Arc::new(Mutex::new(config));
let config = current_conf.clone();
let c2 = current_conf.clone();
tauri::Builder::default()
.manage(MicThreshold(threshold.clone()))
.manage(AudioLevel(level.clone()))
.setup(|app| {
.manage(BlinkInterval(blink_interval.clone()))
.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);
}
}
_ => {}
})
.setup(move |app| {
let window = app.get_window("main").unwrap();
tauri::async_runtime::spawn(async move {
monitor(window, threshold, level).await;
monitor(window, threshold, level, sens);
});
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() {
trace!("Blinking");
if let Some(e) = window.emit("blink", "").err() {
if let Some(e) = window.emit_all("blink", "").err() {
warn!("Failed to emit blink event: {}", e);
}
}
tokio::time::sleep(Duration::from_millis(3000)).await;
let blink = blink_interval.lock().unwrap();
sleep(Duration::from_millis(*blink));
}
});
if let Ok(matches) = app.get_cli_matches() {
if let Some(arg) = matches.args.get("file") {
if let Value::String(path) = arg.value.clone() {
let window = app.get_window("main").unwrap();
let path = Path::new(&path).canonicalize().unwrap();
tauri::async_runtime::spawn(async move {
if let Some(ray) = fs::load_ray(Path::new(&path)).await {
window.emit("load-ray", ray).unwrap();
let window = app.get_window("main").unwrap();
tauri::async_runtime::spawn(async move {
use notify::{event::AccessKind, Event, EventKind, RecursiveMode, Result, Watcher};
if let Some(path) = config_dir() {
let mut watcher =
notify::recommended_watcher(move |res: Result<Event>| match res {
Ok(event) => {
if let EventKind::Access(AccessKind::Close(_)) = event.kind {
*config.lock().unwrap() = config::load_config();
window.emit_all("reload-config", "").unwrap();
}
}
});
Err(e) => error!("error watching filesystem {:?}", e),
})
.unwrap();
watcher
.watch(
&path.join("cathode").join("config.toml"),
RecursiveMode::NonRecursive,
)
.unwrap();
loop {
sleep(Duration::from_millis(5000));
}
}
});
match app.get_cli_matches() {
Ok(matches) => {
trace!("matches OK");
if let Some(arg) = matches.args.get("file") {
trace!("CLI arg value: {:?}", arg);
if let Value::String(path) = arg.value.clone() {
*ray.lock().unwrap() = Some(PathBuf::from(path));
}
}
}
Err(e) => eprintln!("{}", e),
}
Ok(())
@ -69,18 +148,38 @@ fn main() {
log,
set_mic_threshold,
get_mic_threshold,
set_mic_sens,
get_mic_sens,
get_audio_level,
get_blink_interval,
set_blink_interval,
get_config,
set_config,
save_current_config,
get_ray_to_load,
fs::open_image,
fs::save_ray,
fs::open_ray,
audio::get_devices,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[tauri::command]
fn get_ray_to_load(ray: State<'_, RayToLoad>) -> Option<WebRay> {
let ray = { (*ray.0.lock().unwrap()).clone() };
if let Some(r) = ray {
let ray = fs::load_ray(r);
ray
} else {
None
}
}
#[tauri::command]
fn log(msg: String) {
println!("{}", msg);
debug!("frontend: {}", msg);
}
#[tauri::command]
@ -93,7 +192,42 @@ fn get_mic_threshold(current: State<'_, MicThreshold>) -> f32 {
*current.0.lock().unwrap()
}
#[tauri::command]
fn set_mic_sens(sens: f32, current: State<'_, MicSense>) {
*current.0.lock().unwrap() = sens;
}
#[tauri::command]
fn get_mic_sens(current: State<'_, MicSense>) -> f32 {
*current.0.lock().unwrap()
}
#[tauri::command]
fn get_audio_level(level: State<'_, AudioLevel>) -> f32 {
*level.0.lock().unwrap()
}
#[tauri::command]
fn get_blink_interval(current: State<'_, BlinkInterval>) -> u64 {
*current.0.lock().unwrap()
}
#[tauri::command]
fn set_blink_interval(value: u64, current: State<'_, BlinkInterval>) {
*current.0.lock().unwrap() = value;
}
#[tauri::command]
fn get_config(current: State<'_, CurrentConfig>) -> Config {
current.0.lock().unwrap().clone()
}
#[tauri::command]
fn set_config(config: Config, current: State<'_, CurrentConfig>) {
*current.0.lock().unwrap() = config
}
#[tauri::command]
fn save_current_config(current: State<'_, CurrentConfig>) -> Result<(), String> {
config::save_config(&*current.0.lock().unwrap()).map_err(|e| format!("{}", e))
}

View File

@ -6,17 +6,19 @@
"distDir": "../dist"
},
"package": {
"productName": "cathode-tube",
"version": "0.0.1"
"productName": "cathode",
"version": "0.2.0"
},
"tauri": {
"cli": {
"description": "A small app for PNG-tubing",
"args": [{
"name": "file",
"index": 1,
"takesValue": true
}]
"args": [
{
"name": "file",
"index": 1,
"takesValue": true
}
]
},
"macOSPrivateApi": true,
"allowlist": {
@ -24,17 +26,29 @@
"all": true
},
"fs": {
"scope": ["$PUBLIC/*", "$CONFIG/*"],
"scope": [
"$PUBLIC/*",
"$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": {
@ -42,10 +56,10 @@
"category": "DeveloperTool",
"copyright": "",
"deb": {
"files" : {
"/usr/share/applications/cathode-tube.desktop": "cathode-tube.desktop",
"/usr/share/mime/packages/application-cathode.xml": "application-cathode.xml",
"/usr/share/icons/hicolor/256x256/mimetypes/aplication-cathode.png": "icons/application-cathode-256.png"
"files": {
"/usr/share/applications/cathode-tube.desktop": "cathode-tube.desktop",
"/usr/share/mime/packages/application-cathode.xml": "application-cathode.xml",
"/usr/share/icons/hicolor/256x256/mimetypes/aplication-cathode.png": "icons/application-cathode-256.png"
},
"depends": []
},
@ -75,7 +89,7 @@
}
},
"security": {
"csp": null
"csp": "default-src 'self'; img-src 'self'; asset: https://asset.localhost"
},
"updater": {
"active": false
@ -92,4 +106,4 @@
}
]
}
}
}

View File

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

View File

@ -1,33 +1,55 @@
<script lang="ts" context="module">
const hints = [
"Eyes open | Mouth closed",
"Eyes open | Mouth open",
"Eyes closed | Mouth closed",
"Eyes closed | Mouth open",
];
let loading = [false, false, false, false];
</script>
<script lang="ts">
import { frames } from "../store";
import { invoke } from "@tauri-apps/api";
import { fs, invoke } from "@tauri-apps/api";
import { fade } from "svelte/transition";
import Context from "../components/context.svelte";
import Context from "./context.svelte";
import LoadingSVG from "/src/loading.svg";
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] = null;
};
//TODO: load frame from ray
</script>
<div class="box">
<div
<button
class="preview"
on:click={openImage}
on:contextmenu={openImage}
on:click={(e) => {
if (e.shiftKey) {
clearImage();
} else {
openImage();
}
}}
on:mouseenter={() =>
(menuTimeout = setTimeout(() => (showMenu = true), 200))}
on:mouseleave={() => {
@ -37,43 +59,81 @@
showMenu = false;
}}
>
{#if src}
{#if loading[index]}
<img class="loading" src={LoadingSVG} alt="Loading" />
{:else if src}
<img {src} alt="Frame {{ index }}" />
{/if}
</div>
</button>
{#if showMenu}
<div transition:fade={{ duration: 50 }} class="context">
<Context>
<p>Context Menu</p>
<p>{hints[index]}</p>
</Context>
</div>
{/if}
</div>
<style lang="scss">
@-webkit-keyframes rotating {
from {
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading {
-webkit-animation: rotating 2s linear infinite;
-moz-animation: rotating 2s linear infinite;
-ms-animation: rotating 2s linear infinite;
-o-animation: rotating 2s linear infinite;
animation: rotating 2s linear infinite;
}
$bg: rgba(150, 150, 150, 0.5);
.preview {
&:hover {
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;
align-content: center;
justify-content: center;
.context {
align-self: center;
z-index: 900;
position: absolute;
left: 105%;
width: max-content;
}
}

View File

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

View File

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

View File

@ -0,0 +1,126 @@
<script lang="ts">
import { config } from "../store";
import { createEventDispatcher } from "svelte";
import { invoke } from "@tauri-apps/api";
const dispatch = createEventDispatcher();
let org = $config;
$: {
invoke("set_mic_sens", { sens: $config.mic_sens }).catch(
async (e) => await invoke("log", { msg: JSON.stringify(e) })
);
}
const onClose = () => {
if (org != $config) {
invoke("set_blink_interval", { value: $config.blink_interval }).catch();
org = $config;
}
dispatch("close");
};
</script>
<div class="settings">
<button class="close" on:click={onClose}>X</button>
<div class="setting">
<label for="bg-color">Background color:</label>
<select bind:value={$config.background_color} id="bg-color">
<option value="transparent">Transparent</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="red">Red</option>
<option value="pink">Pink</option>
</select>
</div>
<div class="setting">
<label for="blink">Blink interval: </label>
<input
value={$config.blink_interval}
on:input={(e) => {
$config.blink_interval = e.currentTarget.valueAsNumber;
}}
id="blink"
type="number"
min="0"
/>
</div>
<div class="setting">
<label for="sens">Mic sensitivity: </label>
<input
type="range"
id="sens"
step="0.01"
min="0.01"
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">
.settings {
position: absolute;
transform: translateX(-50%) translateY(-50%);
top: 50%;
left: 50%;
z-index: 1000;
width: 90%;
height: 90%;
display: flex;
flex-direction: column;
gap: 5vh;
background-color: rgba(63, 176, 252, 0.9);
border-radius: 15px;
border: solid black 2px;
padding: 20px;
label {
font-weight: bold;
}
.close {
&:focus {
border: solid white 2px;
}
position: absolute;
right: 15px;
top: 15px;
font-size: 20px;
cursor: pointer;
width: 2rem;
height: 2rem;
background-color: black;
color: white;
border: solid black 1px;
text-align: center;
vertical-align: center;
border-radius: 5px;
}
}
.setting {
display: flex;
align-content: center;
justify-content: space-between;
width: 50%;
}
#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>

56
src/loading.svg Normal file
View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 204.481 204.481" style="enable-background:new 0 0 204.481 204.481;" xml:space="preserve">
<g>
<path d="M162.116,38.31c0.163-0.215,0.315-0.438,0.454-0.67c0.033-0.055,0.068-0.109,0.1-0.164
c0.156-0.276,0.297-0.561,0.419-0.857c0.014-0.034,0.024-0.069,0.038-0.104c0.102-0.26,0.188-0.528,0.261-0.801
c0.019-0.069,0.037-0.137,0.053-0.207c0.068-0.288,0.124-0.581,0.157-0.881c0.002-0.017,0.006-0.034,0.008-0.052
c0.028-0.262,0.043-0.527,0.043-0.796V7.5c0-4.142-3.358-7.5-7.5-7.5H48.332c-4.142,0-7.5,3.358-7.5,7.5v26.279
c0,0.269,0.016,0.534,0.043,0.796c0.002,0.017,0.006,0.034,0.008,0.052c0.034,0.3,0.089,0.593,0.157,0.881
c0.016,0.069,0.035,0.138,0.053,0.207c0.073,0.273,0.159,0.541,0.261,0.801c0.013,0.034,0.024,0.069,0.038,0.104
c0.121,0.296,0.262,0.581,0.419,0.857c0.032,0.056,0.067,0.109,0.1,0.164c0.14,0.232,0.291,0.455,0.454,0.67
c0.027,0.035,0.047,0.074,0.074,0.109l50.255,63.821l-50.255,63.821c-0.028,0.035-0.047,0.074-0.074,0.109
c-0.163,0.215-0.315,0.438-0.454,0.67c-0.033,0.055-0.068,0.109-0.1,0.164c-0.156,0.276-0.297,0.561-0.419,0.857
c-0.014,0.034-0.024,0.069-0.038,0.104c-0.102,0.26-0.188,0.528-0.261,0.801c-0.019,0.069-0.037,0.137-0.053,0.207
c-0.068,0.288-0.124,0.581-0.157,0.881c-0.002,0.017-0.006,0.034-0.008,0.052c-0.028,0.262-0.043,0.527-0.043,0.796v26.279
c0,4.142,3.358,7.5,7.5,7.5h107.817c4.142,0,7.5-3.358,7.5-7.5v-26.279c0-0.269-0.016-0.534-0.043-0.796
c-0.002-0.017-0.006-0.034-0.008-0.052c-0.034-0.3-0.089-0.593-0.157-0.881c-0.016-0.069-0.035-0.138-0.053-0.207
c-0.073-0.273-0.159-0.541-0.261-0.801c-0.013-0.034-0.024-0.069-0.038-0.104c-0.121-0.296-0.262-0.581-0.419-0.857
c-0.032-0.056-0.067-0.109-0.1-0.164c-0.14-0.232-0.291-0.455-0.454-0.67c-0.027-0.035-0.047-0.074-0.074-0.109l-50.255-63.821
l50.255-63.821C162.07,38.385,162.089,38.346,162.116,38.31z M148.649,15v11.279H55.832V15H148.649z M55.832,189.481v-11.279
h92.817v11.279H55.832z M140.698,163.202H63.784l38.457-48.838L140.698,163.202z M102.241,90.118L63.784,41.279h76.914
L102.241,90.118z"/>
</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>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

@ -6,3 +6,6 @@
left: 0px;
}
* {
user-select: none;
}

View File

@ -1,36 +1,39 @@
<script lang="ts" context="module">
import { writable, type Writable } from "svelte/store";
import { writable } from "svelte/store";
const transparent = writable(false);
const threshold: Writable<number> = writable(0.5);
const settings_open = writable(false);
const threshold = writable(0.5);
const level = writable(0);
type WebRay = {
frames: Array<string>;
meta: {
threshold: string | undefined;
closeThreshold: string | undefined;
};
};
</script>
<script lang="ts">
import { appWindow, PhysicalSize } from "@tauri-apps/api/window";
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
import FramePreview from "../components/FramePreview.svelte";
import Tuber from "../components/tube.svelte";
import { invoke } from "@tauri-apps/api/tauri";
import Bar from "../components/bar.svelte";
import { tick } from "svelte";
import { frames } from "../store";
import { quintInOut } from "svelte/easing";
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;
let closeThreshold = 75;
$: {
$transparent = $transparent && !$settings_open;
}
$: {
invoke("set_mic_threshold", { threshold: $threshold as number })
.then()
@ -39,9 +42,30 @@
});
}
const openRay = (ray: WebRay) => {
let focusUnlisten: UnlistenFn;
$: {
invoke("set_mic_threshold", { threshold: $threshold as number })
.then()
.catch(async (e) => {
await invoke("log", { msg: `Error setting mic threshold: ${e}` });
});
}
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);
@ -50,17 +74,14 @@
closeThreshold = parseFloat(ray.meta.closeThreshold);
}
};
onMount(async () => {
await appWindow.center();
await appWindow.setMinSize(new PhysicalSize(720, 600));
await appWindow.onFocusChanged(({ payload: focused }) => {
focusUnlisten = await appWindow.onFocusChanged(({ payload: focused }) => {
$transparent = !focused;
});
await appWindow.listen("load-ray", (e) => {
openRay(e.payload as WebRay);
});
monitorTimer = setInterval(async () => {
if (!$transparent) {
$level = await invoke("get_audio_level");
@ -69,38 +90,49 @@
}, 40);
$threshold = await invoke("get_mic_threshold");
const ray = await invoke("get_ray_to_load");
if (ray) {
openRay(ray as WebRay);
}
});
onDestroy(() => {
onDestroy(async () => {
if (monitorTimer) {
clearInterval(monitorTimer);
monitorTimer = undefined;
}
if (focusUnlisten) focusUnlisten();
await invoke("save_current_config");
});
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>
@ -111,6 +143,13 @@
bind:threshold={closeThreshold}
/>
{#if !$transparent}
{#if $settings_open}
<Settings
on:close={() => {
$settings_open = false;
}}
/>
{/if}
<div
transition:fly={{
duration: 200,
@ -165,8 +204,18 @@
}}
class="buttons"
>
<div on:click={saveRay}>Save</div>
<div on:click={loadRay}>Load</div>
<button
on:click={() => {
openRay(new WebRay());
}}>New</button
>
<button on:click={saveRay}>Save</button>
<button on:click={loadRay}>Load</button>
<button
on:click={() => {
$settings_open = true;
}}>Settings</button
>
</div>
{/if}
</div>
@ -183,13 +232,12 @@
align-items: center;
justify-items: center;
gap: 5vh;
div {
button {
color: white;
padding: 10px;
border-radius: 5px;
background-color: rgba(0.5, 0.5, 0.5, 0.5);
border: solid black 2px;
user-select: none;
cursor: pointer;
&:hover {
background-color: rgba(0.9, 0.9, 0.9, 0.9);
@ -219,23 +267,23 @@
@keyframes fade-out {
0% {
background-color: inherit;
background-color: var(--active-color);
}
100% {
background-color: transparent;
background-color: var(--inactive-color);
}
}
@keyframes fade-in {
0% {
background-color: transparent;
background-color: var(--inactive-color);
}
100% {
background-color: inherit;
background-color: var(--active-color);
}
}
.container {
background-color: inherit;
background-color: var(--active-color);
animation-name: fade-in;
animation-duration: 0.2s;
}
@ -243,6 +291,6 @@
.container.transparent {
animation-name: fade-out;
animation-duration: 0.2s;
background-color: transparent;
background-color: var(--inactive-color);
}
</style>

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

1286
yarn.lock Normal file

File diff suppressed because it is too large Load Diff