Compare commits
59 Commits
v0.0.1-bet
...
main
Author | SHA1 | Date |
---|---|---|
emerald | aa72b4a7cf | |
emerald | b4ed39965f | |
Emerald | 47b33853f3 | |
Emerald | 6ddb397c22 | |
Emerald | b1329697bf | |
Emerald | a7a897495a | |
Emerald | 0c7c57a0fd | |
Emerald | 038c20600d | |
Emerald | 7cc2aa022a | |
Emerald | c12b2af269 | |
Emerald | 594f321c57 | |
Emerald | 7dd67cd8b5 | |
Emerald | 5f51d0b1d6 | |
emerald | 1a27172cc9 | |
emerald | e3fdda3f0f | |
emerald | c3be26e5fa | |
emerald | 1711e57f5d | |
emerald | efc513a249 | |
emerald | db171e0549 | |
emerald | 2ca5380e85 | |
emerald | a28bf6b63f | |
emerald | 4b2b0c14ec | |
emerald | 23a700a4d8 | |
emerald | 562030bed3 | |
emerald | df884108e2 | |
emerald | 56f7e1f11b | |
emerald | b0d82ea050 | |
emerald | a5fcfde5af | |
emerald | 8654fe5070 | |
emerald | ce7f3fd010 | |
emerald | 9b105a1f88 | |
emerald | e08394d68a | |
emerald | acb76b4441 | |
emerald | 1703f92a3d | |
emerald | 09a08554d4 | |
emerald | 080f8d1edc | |
emerald | d532f76d01 | |
Emerald | d7912d12e7 | |
Emerald | 49fcf738fe | |
Emerald | 4d88245e69 | |
AnActualEmerald | 6be45162e3 | |
AnActualEmerald | ef1211731f | |
AnActualEmerald | 20af8b8164 | |
AnActualEmerald | e08e508eda | |
AnActualEmerald | 7e991bd5e6 | |
Emerald | c42ee4933e | |
Emerald | 251aa384ad | |
AnActualEmerald | d213bf787f | |
AnActualEmerald | e4770a8894 | |
AnActualEmerald | f2486208c7 | |
AnActualEmerald | 10f0ac0576 | |
AnActualEmerald | 1739faf1f7 | |
AnActualEmerald | ea076f468f | |
AnActualEmerald | f21509c896 | |
AnActualEmerald | d02ca038a9 | |
Emerald | d9e79f5e07 | |
Emerald | 99dc3b3e61 | |
Emerald | bc70d51411 | |
AnActualEmerald | 4b94c7799f |
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
package-lock.json
|
||||
dist/
|
||||
bundle/
|
||||
target/
|
|
@ -0,0 +1,3 @@
|
|||
[[language]]
|
||||
name = "svelte"
|
||||
auto-format = true
|
|
@ -0,0 +1,2 @@
|
|||
node 20
|
||||
yarn latest
|
|
@ -1,28 +0,0 @@
|
|||
pipeline:
|
||||
setup:
|
||||
image: ivangabriele/tauri:bullseye-node18
|
||||
when:
|
||||
event: [push, pull_request, tag]
|
||||
branch: main
|
||||
commands:
|
||||
- npm i -D @tauri-apps/cli
|
||||
build:
|
||||
image: ivangabriele/tauri:bullseye-node18
|
||||
when:
|
||||
event: [push, pull_request, tag]
|
||||
branch: main
|
||||
commands:
|
||||
- apt install libasound2-dev
|
||||
- npm ci
|
||||
- npm run tauri build
|
||||
upload:
|
||||
image: alpine/curl
|
||||
when:
|
||||
event: [tag]
|
||||
branch: main
|
||||
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
|
||||
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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 -->
|
|
@ -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"]
|
42
README.md
42
README.md
|
@ -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
|
|
@ -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
|
|
@ -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 +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 |
|
@ -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 +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 |
|
@ -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
|
||||
}
|
|
@ -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
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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}}"
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cathode-tube",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
@ -12,12 +12,15 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@neodrag/svelte": "^1.2.3",
|
||||
"@tauri-apps/api": "^1.0.2"
|
||||
"@tauri-apps/api": "^1.5.0",
|
||||
"animejs": "^3.2.1",
|
||||
"image-js": "^0.35.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"@tauri-apps/cli": "^1.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",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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 |
38
src/store.ts
38
src/store.ts
|
@ -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());
|
||||
|
|
|
@ -6,3 +6,6 @@
|
|||
left: 0px;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -12,6 +12,7 @@ export default defineConfig({
|
|||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
cors: true
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
|
|
Loading…
Reference in New Issue