refactor: rework frontend to use sveltekit and skeleton (#8)
ci/woodpecker/push/build Pipeline failed
Details
Co-authored-by: Emerald <emerald@emeraldgreen.dev> Reviewed-on: #8 Co-authored-by: Emerald <emerald_actual@proton.me> Co-committed-by: Emerald <emerald_actual@proton.me>
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -0,0 +1,30 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
|
@ -1,3 +1,14 @@
|
|||
node_modules/
|
||||
dist/
|
||||
bundle/
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
target
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
{
|
||||
"prettier.documentSelectors": [
|
||||
"**/*.svelte"
|
||||
],
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"accent",
|
||||
"active",
|
||||
"aspectRatio",
|
||||
"background",
|
||||
"badge",
|
||||
"bgBackdrop",
|
||||
"bgDark",
|
||||
"bgDrawer",
|
||||
"bgLight",
|
||||
"blur",
|
||||
"border",
|
||||
"button",
|
||||
"buttonAction",
|
||||
"buttonBack",
|
||||
"buttonClasses",
|
||||
"buttonComplete",
|
||||
"buttonDismiss",
|
||||
"buttonNeutral",
|
||||
"buttonNext",
|
||||
"buttonPositive",
|
||||
"buttonTextCancel",
|
||||
"buttonTextConfirm",
|
||||
"buttonTextFirst",
|
||||
"buttonTextLast",
|
||||
"buttonTextNext",
|
||||
"buttonTextPrevious",
|
||||
"buttonTextSubmit",
|
||||
"caretClosed",
|
||||
"caretOpen",
|
||||
"chips",
|
||||
"color",
|
||||
"controlSeparator",
|
||||
"controlVariant",
|
||||
"cursor",
|
||||
"display",
|
||||
"element",
|
||||
"fill",
|
||||
"fillDark",
|
||||
"fillLight",
|
||||
"flex",
|
||||
"gap",
|
||||
"gridColumns",
|
||||
"height",
|
||||
"hover",
|
||||
"inactive",
|
||||
"indent",
|
||||
"justify",
|
||||
"meter",
|
||||
"padding",
|
||||
"position",
|
||||
"regionAnchor",
|
||||
"regionBackdrop",
|
||||
"regionBody",
|
||||
"regionCaption",
|
||||
"regionCaret",
|
||||
"regionCell",
|
||||
"regionChildren",
|
||||
"regionChipList",
|
||||
"regionChipWrapper",
|
||||
"regionCone",
|
||||
"regionContent",
|
||||
"regionControl",
|
||||
"regionDefault",
|
||||
"regionDrawer",
|
||||
"regionFoot",
|
||||
"regionFootCell",
|
||||
"regionFooter",
|
||||
"regionHead",
|
||||
"regionHeadCell",
|
||||
"regionHeader",
|
||||
"regionIcon",
|
||||
"regionInput",
|
||||
"regionInterface",
|
||||
"regionInterfaceText",
|
||||
"regionLabel",
|
||||
"regionLead",
|
||||
"regionLegend",
|
||||
"regionList",
|
||||
"regionListItem",
|
||||
"regionNavigation",
|
||||
"regionPage",
|
||||
"regionPanel",
|
||||
"regionRowHeadline",
|
||||
"regionRowMain",
|
||||
"regionSummary",
|
||||
"regionSymbol",
|
||||
"regionTab",
|
||||
"regionTrail",
|
||||
"ring",
|
||||
"rounded",
|
||||
"select",
|
||||
"shadow",
|
||||
"slotDefault",
|
||||
"slotFooter",
|
||||
"slotHeader",
|
||||
"slotLead",
|
||||
"slotMessage",
|
||||
"slotMeta",
|
||||
"slotPageContent",
|
||||
"slotPageFooter",
|
||||
"slotPageHeader",
|
||||
"slotSidebarLeft",
|
||||
"slotSidebarRight",
|
||||
"slotTrail",
|
||||
"spacing",
|
||||
"text",
|
||||
"track",
|
||||
"width",
|
||||
"zIndex"
|
||||
]
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
when:
|
||||
- event: [ tag, manual ]
|
||||
- event: [tag, manual]
|
||||
branch: main
|
||||
- event: deployment
|
||||
environment: production
|
||||
|
||||
|
||||
steps:
|
||||
build:
|
||||
image: forge.greenboi.me/emerald/cathode-build:latest
|
||||
|
@ -29,8 +28,7 @@ steps:
|
|||
settings:
|
||||
server: https://ci.greenboi.me
|
||||
repositories:
|
||||
- emerald/cathode_dot_tube
|
||||
- emerald/cathode_dot_tube@main
|
||||
deploy: produciton
|
||||
token:
|
||||
from_secret: woodpecker_token
|
||||
|
||||
|
|
60
README.md
|
@ -1,40 +1,38 @@
|
|||
# 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.
|
||||
# create-svelte
|
||||
|
||||
Built with Tauri and Svelte.
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Installation
|
||||
## Creating a project
|
||||
|
||||
### 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.
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
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.
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
### 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
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
#### Just
|
||||
If you have the [just](https://github.com/casey/just) command runner installed, as well as the other prerequisites, then you can run
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
just install
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
Which will build the project and install it to `/usr/bin`, along with the `.desktop` file and icons
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
|
|
27
justfile
|
@ -3,19 +3,17 @@ set export
|
|||
alias d := debug
|
||||
|
||||
dev:
|
||||
-cargo tauri dev
|
||||
-yarn tauri dev
|
||||
|
||||
debug:
|
||||
RUST_LOG=debug cargo tauri dev
|
||||
RUST_LOG=debug yarn tauri dev
|
||||
|
||||
log RUST_LOG:
|
||||
cargo tauri dev
|
||||
yarn tauri dev
|
||||
|
||||
build:
|
||||
cargo tauri build
|
||||
yarn tauri build
|
||||
cd src-tauri && cargo generate-rpm
|
||||
cp -r src-tauri/target/release/bundle bundle
|
||||
cp -r src-tauri/target/generate-rpm bundle
|
||||
|
||||
install:
|
||||
@cargo tauri build -b none
|
||||
|
@ -40,15 +38,20 @@ uninstall:
|
|||
@sudo rm -f /usr/share/icons/hicolor/256x256/apps/cathode-tube.png
|
||||
@sudo update-desktop-database
|
||||
|
||||
release tag:
|
||||
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
|
||||
git cliff -o CHANGELOG.md --tag v$v
|
||||
git commit -am "chore(release): prepare for ${v}" -S
|
||||
git tag -s -a "v$v" -m "$(git cliff -u --strip all --tag v$v)"
|
||||
echo "Ready to push release (git push && git push --tag v$v)"
|
||||
|
||||
echo "Updated versions to {{tag}}"
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "cathode-tube",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@neodrag/svelte": "^1.2.3",
|
||||
"@tauri-apps/api": "^1.5.0",
|
||||
"animejs": "^3.2.1",
|
||||
"image-js": "^0.35.4",
|
||||
"omggif": "^1.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"@tauri-apps/cli": "^1.5.2",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/animejs": "^3.1.8",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/omggif": "^1.0.3",
|
||||
"sass": "^1.54.8",
|
||||
"svelte": "^3.49.0",
|
||||
"svelte-check": "^2.8.0",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.2"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 654 B After Width: | Height: | Size: 654 B |
|
@ -0,0 +1,10 @@
|
|||
import sveltePreprocess from "svelte-preprocess";
|
||||
|
||||
export default {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: sveltePreprocess(),
|
||||
css: css => {
|
||||
css.write('public/bundle.css');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
|
||||
// Vite optons tailored for Tauri developemnt and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// tauri expects a fixed port, fail if that port is not available
|
||||
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
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: ["es2021", "chrome100", "safari13"],
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG,
|
||||
},
|
||||
});
|
75
package.json
|
@ -1,35 +1,62 @@
|
|||
{
|
||||
"name": "cathode-tube",
|
||||
"name": "cathode",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@neodrag/svelte": "^1.2.3",
|
||||
"@tauri-apps/api": "^1.5.0",
|
||||
"animejs": "^3.2.1",
|
||||
"image-js": "^0.35.4",
|
||||
"omggif": "^1.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"@tauri-apps/cli": "^1.5.2",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/animejs": "^3.1.8",
|
||||
"@types/node": "^18.7.10",
|
||||
"@skeletonlabs/skeleton": "2.3.0",
|
||||
"@skeletonlabs/tw-plugin": "0.2.2",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@tailwindcss/forms": "0.5.6",
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"@tauri-apps/cli": "^1.5.4",
|
||||
"@types/animejs": "^3.1.9",
|
||||
"@types/node": "20.8.6",
|
||||
"@types/omggif": "^1.0.3",
|
||||
"sass": "^1.54.8",
|
||||
"svelte": "^3.49.0",
|
||||
"svelte-check": "^2.8.0",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.2"
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vite-pwa/sveltekit": "^0.2.7",
|
||||
"autoprefixer": "10.4.16",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"kolorist": "^1.8.0",
|
||||
"postcss": "8.4.31",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"sass": "^1.69.4",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2",
|
||||
"vite-plugin-pwa": "^0.16.5",
|
||||
"vite-plugin-tailwind-purgecss": "^0.1.3",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vite-plugin-wasm": "^3.2.2",
|
||||
"workbox-build": "^7.0.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@cathode/cathode-ray": "^0.1.0",
|
||||
"@floating-ui/dom": "1.5.3",
|
||||
"@neodrag/svelte": "^2.0.3",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"animejs": "^3.2.1",
|
||||
"image-js": "^0.35.5",
|
||||
"omggif": "^1.0.10"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -190,7 +190,7 @@ version = "0.68.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"lazy_static",
|
||||
|
@ -218,9 +218,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
|
@ -348,7 +348,7 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "cathode-tube"
|
||||
name = "cathode"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
|
@ -781,10 +781,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
|
||||
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -1030,9 +1031,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.27"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
|
||||
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
|
@ -1594,16 +1595,16 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.57"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
|
||||
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows 0.48.0",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1994,9 +1995,9 @@ checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
|
|||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.10"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
|
||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
|
@ -2407,9 +2408,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
|||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.5.1"
|
||||
version = "6.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
|
||||
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
|
@ -2454,13 +2455,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.8"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
|
||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"redox_syscall 0.4.1",
|
||||
"smallvec",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
@ -2676,6 +2677,12 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
|
@ -2932,6 +2939,15 @@ dependencies = [
|
|||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
|
@ -2945,14 +2961,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.0"
|
||||
version = "1.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87"
|
||||
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.1",
|
||||
"regex-syntax 0.8.0",
|
||||
"regex-automata 0.4.3",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2966,13 +2982,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b"
|
||||
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.0",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2983,9 +2999,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.0"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
|
@ -3043,11 +3059,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.18"
|
||||
version = "0.38.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c"
|
||||
checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
|
@ -3139,18 +3155,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.188"
|
||||
version = "1.0.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.188"
|
||||
version = "1.0.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.69",
|
||||
"quote 1.0.33",
|
||||
|
@ -3190,9 +3206,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237"
|
||||
checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"chrono",
|
||||
|
@ -3207,9 +3223,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c"
|
||||
checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2 1.0.69",
|
||||
|
@ -3622,9 +3638,9 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
|
|||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.5.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0238c5063bf9613054149a1b6bce4935922e532b7d8211f36989a490a79806be"
|
||||
checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
@ -3894,12 +3910,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe"
|
||||
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa 1.0.9",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
|
@ -4035,11 +4052,10 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.37"
|
||||
version = "0.1.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
||||
checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
|
@ -4047,9 +4063,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.26"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.69",
|
||||
"quote 1.0.33",
|
||||
|
@ -4058,9 +4074,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.31"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
|
@ -4172,9 +4188,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.1"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
]
|
||||
|
@ -4489,6 +4505,15 @@ dependencies = [
|
|||
"windows-tokens",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.51.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.39.0"
|
||||
|
@ -4705,9 +4730,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.16"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907"
|
||||
checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -4837,12 +4862,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.8+zstd.1.5.5"
|
||||
version = "2.0.9+zstd.1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
|
||||
checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "cathode-tube"
|
||||
name = "cathode"
|
||||
version = "0.2.0" # managed by release.sh
|
||||
description = "A small PNGTubing app"
|
||||
authors = ["AnActualEmerald"]
|
||||
|
@ -10,10 +10,12 @@ edition = "2021"
|
|||
[package.metadata.generate-rpm]
|
||||
assets = [
|
||||
{source= "target/release/cathode", dest= "/usr/bin/cathode", mode= "755"},
|
||||
{source="cathode-tube.desktop", dest="/usr/share/applications/cathode-tube.desktop", mode="0644"},
|
||||
{source="cathode-tube.desktop", dest="/usr/share/applications/cathode.desktop", mode="0644"},
|
||||
{source="application-cathode.xml", dest="/usr/share/mime/packages/application-cathode.xml", mode="0644"},
|
||||
{source="icons/128x128.png", dest="/usr/share/icons/hicolor/128x128/apps/cathode-tube.png", mode="0644"},
|
||||
{source="icons/128x128@2x.png", dest="/usr/share/icons/hicolor/256x256@2/apps/cathode-tube.png", mode="0644"},
|
||||
{source="icons/128x128.png", dest="/usr/share/icons/hicolor/128x128/apps/cathode.png", mode="0644"},
|
||||
{source="icons/128x128@2x.png", dest="/usr/share/icons/hicolor/256x256/apps/cathode.png", mode="0644"},
|
||||
{source="icons/application-cathode-128.png", dest="/usr/share/icons/hicolor/128x128/mimetypes/application-cathode.png", mode="0644"},
|
||||
{source="icons/application-cathode-256.png", dest="/usr/share/icons/hicolor/256x256/mimetypes/application-cathode.png", mode="0644"},
|
||||
]
|
||||
auto-req = "no"
|
||||
|
||||
|
@ -23,8 +25,6 @@ gtk3 = ">= 3"
|
|||
webkit2gtk3 = ">= 2"
|
||||
|
||||
|
||||
[workspace]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "s"
|
||||
|
@ -37,7 +37,7 @@ tauri-build = { version = "1", features = [] }
|
|||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1", features = [ "protocol-asset", "window-center", "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"] }
|
||||
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.66"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Icon=cathode-tube
|
||||
Icon=cathode
|
||||
Name=Cathode
|
||||
Exec=cathode %U
|
||||
Terminal=false
|
||||
|
|
|
@ -25,20 +25,32 @@ pub fn monitor(
|
|||
.build_input_stream(
|
||||
&config.config(),
|
||||
move |data: &[f32], _: &InputCallbackInfo| {
|
||||
if data
|
||||
let Some(new_level) = data
|
||||
.iter()
|
||||
.any(|e| (e.abs() * *sens.lock().unwrap()) >= *threshold.lock().unwrap())
|
||||
.map(|v| {
|
||||
let val = v.abs();
|
||||
if val < 0.0001 {
|
||||
0.0
|
||||
} else {
|
||||
val * *sens.lock().expect("Unable to lock mic sens mutex")
|
||||
}
|
||||
})
|
||||
.reduce(|prev, curr| prev + curr)
|
||||
.map(|v| v / data.len() as f32)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if new_level
|
||||
> *threshold
|
||||
.lock()
|
||||
.expect("Unable to lock mic threshold mutex")
|
||||
{
|
||||
window.emit("mouth-open", "").unwrap();
|
||||
} else {
|
||||
window.emit("mouth-close", "").unwrap();
|
||||
}
|
||||
|
||||
*level.lock().unwrap() = data
|
||||
.iter()
|
||||
.map(|e| e.abs() * *sens.lock().unwrap())
|
||||
.max_by(|a, b| a.total_cmp(&b))
|
||||
.unwrap();
|
||||
*level.lock().unwrap() = new_level;
|
||||
},
|
||||
move |err| {
|
||||
println!("Audio error: {:?}", err);
|
||||
|
@ -55,7 +67,6 @@ pub fn monitor(
|
|||
}
|
||||
|
||||
fn initialize() -> Result<Device> {
|
||||
debug!("Hey there");
|
||||
let host = cpal::default_host();
|
||||
host.default_input_device()
|
||||
.ok_or_else(|| anyhow!("No default output device"))
|
||||
|
|
|
@ -5,7 +5,7 @@ use figment::{
|
|||
providers::{Env, Format, Toml},
|
||||
Figment,
|
||||
};
|
||||
use log::{debug, error, trace, warn};
|
||||
use log::{debug, error, trace};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::api::path::config_dir;
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ use std::path::Path;
|
|||
use anyhow::Result;
|
||||
use base64::prelude::*;
|
||||
use image::codecs::gif::{GifDecoder, GifEncoder, Repeat};
|
||||
use image::imageops::resize;
|
||||
use image::{guess_format, AnimationDecoder, Frame, ImageDecoder, ImageFormat};
|
||||
use image::{guess_format, AnimationDecoder, ImageFormat};
|
||||
use log::{debug, error};
|
||||
use ray_format::Ray;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devPath": "http://localhost:1420",
|
||||
"distDir": "../dist"
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": {
|
||||
"productName": "cathode",
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="description" content="A small PNGTubing app" />
|
||||
<title>Cathode</title>
|
||||
<meta name="og:site_name" contnent="Cathode" />
|
||||
<meta name="og:title" contnent="Cathode" />
|
||||
<meta name="og:type" content="website" />
|
||||
<meta name="og:description" content="A small PNGTubing app" />
|
||||
<meta name="og:url" content="https://app.cathode.tube" />
|
||||
<meta name="og:image" content="%sveltekit.assets%/icon.png" />
|
||||
<meta name="og:image:width" content="512" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" data-theme="hamlindigo" class="overflow-hidden">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,12 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
|
||||
.dark body {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
.meta-button {
|
||||
@apply btn btn-md variant-soft-surface border-token border-surface-400-500-token;
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
<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 { fs, invoke } from "@tauri-apps/api";
|
||||
import { fade } from "svelte/transition";
|
||||
import Context from "./context.svelte";
|
||||
import LoadingSVG from "/src/loading.svg";
|
||||
import Image from "image-js";
|
||||
import { Gif } from "../gifler";
|
||||
export let index: number;
|
||||
|
||||
let menuTimeout: NodeJS.Timeout | null = null;
|
||||
let showMenu = false;
|
||||
let src: string | null;
|
||||
|
||||
$: {
|
||||
const frame = $frames[index];
|
||||
if (!frame) src = null;
|
||||
else {
|
||||
if (frame?.kind === "still") src = frame.value.toDataURL();
|
||||
else {
|
||||
const b = new Blob([new Uint8Array(frame.data)]);
|
||||
src = URL.createObjectURL(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const openImage = async () => {
|
||||
loading[index] = true;
|
||||
const path = (await invoke("open_image")) as {
|
||||
kind: "png" | "gif";
|
||||
data: string;
|
||||
};
|
||||
|
||||
if (path) {
|
||||
const data = await fs.readBinaryFile(path.data);
|
||||
if (path.kind == "png")
|
||||
$frames[index] = {
|
||||
kind: "still",
|
||||
value: await Image.load(data),
|
||||
};
|
||||
else {
|
||||
$frames[index] = {
|
||||
kind: "GIF",
|
||||
value: new Gif(new Promise((res) => res(data))),
|
||||
data: data,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.error("Error loading image");
|
||||
}
|
||||
loading[index] = false;
|
||||
};
|
||||
const clearImage = () => {
|
||||
$frames[index] = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="box">
|
||||
<button
|
||||
class="preview"
|
||||
on:click={(e) => {
|
||||
if (e.shiftKey) {
|
||||
clearImage();
|
||||
} else {
|
||||
openImage();
|
||||
}
|
||||
}}
|
||||
on:mouseenter={() =>
|
||||
(menuTimeout = setTimeout(() => (showMenu = true), 200))}
|
||||
on:mouseleave={() => {
|
||||
if (menuTimeout) {
|
||||
clearTimeout(menuTimeout);
|
||||
}
|
||||
showMenu = false;
|
||||
}}
|
||||
>
|
||||
{#if loading[index]}
|
||||
<img class="loading" src={LoadingSVG} alt="Loading" />
|
||||
{:else if src}
|
||||
<img {src} alt="Frame {{ index }}" />
|
||||
{/if}
|
||||
</button>
|
||||
{#if showMenu}
|
||||
<div transition:fade={{ duration: 50 }} class="context">
|
||||
<Context>
|
||||
<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-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
border: 2px solid black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $bg;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 15vh;
|
||||
height: 15vh;
|
||||
user-select: none;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
.context {
|
||||
z-index: 900;
|
||||
position: absolute;
|
||||
left: 105%;
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 75%;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
|
@ -1,126 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { draggable } from "@neodrag/svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { tweened } from "svelte/motion";
|
||||
import { sineIn, sineOut } from "svelte/easing";
|
||||
// import { invoke } from "@tauri-apps/api";
|
||||
export let progress = 0;
|
||||
|
||||
export let withSetpoint = false;
|
||||
export let setpoint = 0;
|
||||
export let onSetpointChange = (_: number) => {};
|
||||
|
||||
let bar: HTMLDivElement;
|
||||
let point: HTMLDivElement;
|
||||
let pos = { x: 0, y: 0 };
|
||||
const tweenedProgress = tweened(0, { duration: 100, easing: sineOut });
|
||||
|
||||
onMount(async () => {
|
||||
let rect = bar.getBoundingClientRect();
|
||||
let prect = point.getBoundingClientRect();
|
||||
pos.y = -(setpoint / 100) * rect.height + prect.height;
|
||||
});
|
||||
|
||||
$: if (bar) {
|
||||
let pxProgress = (progress / 100) * bar.getBoundingClientRect().height;
|
||||
if (pxProgress > $tweenedProgress) {
|
||||
tweenedProgress
|
||||
.set(pxProgress, {
|
||||
duration: 30,
|
||||
easing: sineOut,
|
||||
})
|
||||
.then();
|
||||
} else {
|
||||
$tweenedProgress = pxProgress;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:resize={async () => {
|
||||
// await invoke("log", {
|
||||
// msg: `Resized`,
|
||||
// });
|
||||
|
||||
pos = { x: 0, y: 0 };
|
||||
let rect = bar.getBoundingClientRect();
|
||||
let prect = point.getBoundingClientRect();
|
||||
pos.y = -(setpoint / 100) * rect.height + prect.height;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="box">
|
||||
<div bind:this={bar} class="bar-container">
|
||||
{#if withSetpoint}
|
||||
<div
|
||||
bind:this={point}
|
||||
class="setpoint"
|
||||
use:draggable={{
|
||||
onDragStart: () => (pos = undefined),
|
||||
handle: ".handle",
|
||||
position: pos,
|
||||
axis: "y",
|
||||
bounds: "parent",
|
||||
onDragEnd: (e) => {
|
||||
let rect = bar.getBoundingClientRect();
|
||||
let y = (e.domRect.y - rect.y + e.domRect.height / 2) / rect.height;
|
||||
onSetpointChange(1.0 - y);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div class="handle" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="bar" style="height: {$tweenedProgress}px;" />
|
||||
</div>
|
||||
<slot class="icon" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1vh;
|
||||
:global(img) {
|
||||
width: var(--bar-width, 5vh);
|
||||
}
|
||||
}
|
||||
|
||||
.setpoint {
|
||||
position: absolute;
|
||||
background-color: black;
|
||||
width: var(--bar-width, 5vh);
|
||||
height: 0.5vh;
|
||||
.handle {
|
||||
position: absolute;
|
||||
border-top: 2vh solid transparent;
|
||||
border-bottom: 2vh solid transparent;
|
||||
border-left: 2vh solid var(--handle-color, forestgreen);
|
||||
left: -2.2vh;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
border-radius: var(--bar-radius, 0px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
background-color: white;
|
||||
border: 2px solid black;
|
||||
height: 80vh;
|
||||
width: var(--bar-width, 5vh);
|
||||
}
|
||||
|
||||
.bar {
|
||||
border-radius: inherit;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
background-color: var(--bar-color, blue);
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
</style>
|
|
@ -1,16 +0,0 @@
|
|||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<div class="context">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.context {
|
||||
color: white;
|
||||
background-color: rgba(75, 75, 75, 10);
|
||||
padding: 1vh;
|
||||
border-radius: 10px;
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
<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>
|
|
@ -1,179 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { frames, type FrameData } from "../store";
|
||||
import { onMount } from "svelte";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import anime from "animejs";
|
||||
import type { Animator } from "../gifler";
|
||||
|
||||
export let buf = 0;
|
||||
export let open = false;
|
||||
export let threshold = 50;
|
||||
let closed = false;
|
||||
let blink = false;
|
||||
$: bitMaps = [null, null, null, null];
|
||||
$: currFrame = 0;
|
||||
|
||||
$: src = null;
|
||||
|
||||
$: {
|
||||
if (src?.mode === "gif" && src.data.running) src.data.stop();
|
||||
src = bitMaps[currFrame] as
|
||||
| { mode: "png"; data: ImageBitmap }
|
||||
| { mode: "gif"; data: Animator };
|
||||
if (src?.mode === "gif") src.data.reset().start();
|
||||
}
|
||||
|
||||
let pos = { x: 0, y: 0 };
|
||||
|
||||
$: {
|
||||
if (buf < threshold) {
|
||||
open = false;
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (closed) {
|
||||
currFrame = 0;
|
||||
} else if (open) {
|
||||
currFrame = 1;
|
||||
}
|
||||
|
||||
if (blink && closed) {
|
||||
currFrame = $frames[2] ? 2 : 0;
|
||||
} else if (blink && open) {
|
||||
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<FrameData | null>) => {
|
||||
return await Promise.all(
|
||||
f.map(async (v) => {
|
||||
if (!v) return null;
|
||||
return v.kind === "still"
|
||||
? {
|
||||
mode: "png",
|
||||
data: await createImageBitmap(await v.value.toBlob()),
|
||||
}
|
||||
: {
|
||||
mode: "gif",
|
||||
data: (
|
||||
await v.value.frames(
|
||||
"#png",
|
||||
(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
frame: {
|
||||
buffer: CanvasImageSource;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
) => {
|
||||
ctx.drawImage(
|
||||
frame.buffer,
|
||||
canvas.width / 2 - pos.x - frame.width / 2,
|
||||
canvas.height / 2 - pos.y - frame.height / 2
|
||||
);
|
||||
},
|
||||
false
|
||||
)
|
||||
).stop(),
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
bitMaps = await createBitmaps($frames);
|
||||
console.log("bitMaps: ", bitMaps);
|
||||
frames.subscribe(async (f) => {
|
||||
bitMaps.forEach((v) => (v && v.mode === "gif" ? v.data.stop() : null));
|
||||
bitMaps = await createBitmaps(f);
|
||||
});
|
||||
|
||||
await appWindow.listen("mouth-open", () => {
|
||||
buf = 100;
|
||||
open = true;
|
||||
closed = false;
|
||||
});
|
||||
|
||||
await appWindow.listen("mouth-close", () => {
|
||||
buf = buf - 5;
|
||||
buf = buf < 0 ? 0 : buf;
|
||||
});
|
||||
|
||||
await appWindow.listen("blink", () => {
|
||||
blink = true;
|
||||
setTimeout(() => (blink = false), 100);
|
||||
});
|
||||
|
||||
await appWindow.onResized(updateCanvasSize);
|
||||
|
||||
updateCanvasSize();
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (src.mode === "png") {
|
||||
const img = src.data;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(
|
||||
img,
|
||||
canvas.width / 2 - pos.x - img.width / 2,
|
||||
canvas.height / 2 - pos.y - img.height / 2
|
||||
);
|
||||
} else if (src.mode === "gif" && !src.data.running()) {
|
||||
src.data.start();
|
||||
}
|
||||
} catch {}
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
requestAnimationFrame(update);
|
||||
});
|
||||
|
||||
const updateCanvasSize = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
</script>
|
||||
|
||||
<canvas bind:this={canvas} id="png" />
|
||||
|
||||
<style lang="scss">
|
||||
canvas {
|
||||
position: absolute;
|
||||
translate: -50% -50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,81 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import { mode } from './store';
|
||||
|
||||
export const micLevel = writable(0);
|
||||
export const micThreshold = writable(0);
|
||||
|
||||
export const initAudio = async (): Promise<NodeJS.Timeout | undefined> => {
|
||||
if (mode == 'tauri') {
|
||||
const { invoke } = await import('@tauri-apps/api');
|
||||
|
||||
|
||||
micThreshold.subscribe(async (threshold) => {
|
||||
try {
|
||||
await invoke('set_mic_threshold', { threshold });
|
||||
} catch (e) {
|
||||
await invoke('log', { msg: `Error setting mic threshold: ${e}` });
|
||||
}
|
||||
});
|
||||
|
||||
return setInterval(async () => {
|
||||
const newLevel = (await invoke('get_audio_level')) as number;
|
||||
micLevel.set(newLevel * 100);
|
||||
}, 40);
|
||||
} else {
|
||||
await webAudio();
|
||||
}
|
||||
};
|
||||
|
||||
let audioCtx: AudioContext | undefined = undefined;
|
||||
const webAudio = async () => {
|
||||
if (audioCtx) audioCtx.close();
|
||||
audioCtx = new AudioContext();
|
||||
if (navigator.mediaDevices) {
|
||||
try {
|
||||
const device = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mic = audioCtx.createMediaStreamSource(device);
|
||||
|
||||
|
||||
await audioCtx.audioWorklet.addModule("audio.worker.js")
|
||||
const worklet = new AudioWorkletNode(audioCtx, "audio-trigger");
|
||||
|
||||
mic.connect(worklet);
|
||||
|
||||
micThreshold.subscribe((val) => {
|
||||
worklet.port.postMessage(val);
|
||||
})
|
||||
|
||||
worklet.port.onmessage = (e) => {
|
||||
switch (typeof e.data) {
|
||||
case "number":
|
||||
micLevel.set(e.data);
|
||||
break;
|
||||
case "string":
|
||||
window?.dispatchEvent(new Event(e.data));
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await audioCtx.resume();
|
||||
|
||||
|
||||
|
||||
// const buffer = new Uint8Array(analyzer.frequencyBinCount);
|
||||
// const monitor = () => {
|
||||
// analyzer.getByteFrequencyData(buffer);
|
||||
|
||||
// }
|
||||
|
||||
// return setInterval(monitor);
|
||||
|
||||
} catch (e) {
|
||||
// console.error("Failed to get microphone: ", e);
|
||||
throw e;
|
||||
//TODO: toast here
|
||||
}
|
||||
} else {
|
||||
console.error("This browser doesn't support audio access?");
|
||||
//TODO: toast here
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { version } from '$app/environment';
|
||||
import { mode } from '$lib/store';
|
||||
</script>
|
||||
|
||||
<section class="grid grid-rows-[auto_1fr_auto] gap-2 h-full">
|
||||
<header>
|
||||
<h1 class="h1"><strong>Cathode {mode === 'web' ? 'Web' : 'Desktop'}</strong></h1>
|
||||
<p>Version v{version}</p>
|
||||
</header>
|
||||
<main>
|
||||
<p>
|
||||
Powered by <a href="https://kit.svelte.dev" class="anchor" target="_blank">SvelteKit</a>{mode === 'tauri'
|
||||
? ', '
|
||||
: ' and '}
|
||||
<a href="https://skeleton.dev" class="anchor" target="_blank">Skeleton</a>
|
||||
{#if mode === 'tauri'}
|
||||
<span>, and <a href="https://tauri.app" class="anchor" target="_blank">Tauri</a></span>
|
||||
{/if}
|
||||
</p>
|
||||
<p>
|
||||
Source code is available <a href="https://codeberg.org/anactualemerald/cathode" class="anchor" target="_blank">here</a>
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p><strong>Cathode</strong> ©2023 Emerald</p>
|
||||
<p>This program comes with ABSOLUTELY NO WARRANTY.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; For details, see the <a href="https://codeberg.org/AnActualEmerald/cathode/src/branch/main/LICENSE" class="anchor">license</a>.</p>
|
||||
|
||||
</footer>
|
||||
</section>
|
|
@ -0,0 +1,112 @@
|
|||
<script lang="ts">
|
||||
import { draggable } from '@neodrag/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { tweened } from 'svelte/motion';
|
||||
import { sineOut } from 'svelte/easing';
|
||||
// import { invoke } from "@tauri-apps/api";
|
||||
export let value = 0;
|
||||
export let withSetpoint = false;
|
||||
export let setpoint = 0;
|
||||
export let onSetpointChange = (_: number) => {};
|
||||
export let barColor: string;
|
||||
|
||||
const handleXPos = 0;
|
||||
|
||||
let bar: HTMLDivElement;
|
||||
let point: HTMLDivElement;
|
||||
let pos = { x: handleXPos, y: 0 };
|
||||
const tweenedProgress = tweened(0, { duration: 100, easing: sineOut });
|
||||
|
||||
onMount(async () => {
|
||||
let rect = bar.getBoundingClientRect();
|
||||
let prect = point.getBoundingClientRect();
|
||||
pos.y = -(setpoint / 100) * rect.height + prect.height;
|
||||
});
|
||||
|
||||
$: if (bar) {
|
||||
let pxProgress = (value / 100) * bar.getBoundingClientRect().height;
|
||||
if (pxProgress > $tweenedProgress) {
|
||||
tweenedProgress
|
||||
.set(pxProgress, {
|
||||
duration: 30,
|
||||
easing: sineOut
|
||||
})
|
||||
.then();
|
||||
} else {
|
||||
$tweenedProgress = pxProgress;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:resize={async () => {
|
||||
// await invoke("log", {
|
||||
// msg: `Resized`,
|
||||
// });
|
||||
|
||||
pos = { x: handleXPos, y: 0 };
|
||||
let rect = bar.getBoundingClientRect();
|
||||
let prect = point.getBoundingClientRect();
|
||||
pos.y = -(setpoint / 100) * rect.height + prect.height;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="h-full flex flex-col gap-4">
|
||||
<div
|
||||
id="bar-container"
|
||||
bind:this={bar}
|
||||
class="w-10 flex flex-col justify-end rounded-container-token bg-surface-500 border-token border-surface-500-400-token h-5/6"
|
||||
>
|
||||
{#if withSetpoint}
|
||||
<div
|
||||
bind:this={point}
|
||||
class="setpoint bg-slate-900 w-9 rounded-token"
|
||||
use:draggable={{
|
||||
handle: '.handle',
|
||||
position: pos,
|
||||
axis: 'y',
|
||||
bounds: 'parent'
|
||||
}}
|
||||
on:neodrag:end={(e) => {
|
||||
const rect = bar.getBoundingClientRect();
|
||||
const pointRect = e.detail.currentNode.getBoundingClientRect();
|
||||
const y = (pointRect.y - rect.y + pointRect.height / 2) / rect.height;
|
||||
onSetpointChange(1.0 - y);
|
||||
}}
|
||||
>
|
||||
<div class="handle rounded" />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
id="bar"
|
||||
class="bar rounded-token {barColor ? barColor : 'bg-primary-500'}"
|
||||
style="height: {$tweenedProgress}px;"
|
||||
/>
|
||||
</div>
|
||||
<div class="aspect-square w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.setpoint {
|
||||
position: absolute;
|
||||
height: 0.5vh;
|
||||
.handle {
|
||||
position: absolute;
|
||||
border-top: 1rem solid transparent;
|
||||
border-bottom: 1rem solid transparent;
|
||||
border-left: 1.5rem solid var(--handle-color, forestgreen);
|
||||
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-1.7rem);
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
user-select: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,147 @@
|
|||
<script lang="ts" context="module">
|
||||
const hints = [
|
||||
'Eyes open | Mouth closed',
|
||||
'Eyes open | Mouth open',
|
||||
'Eyes closed | Mouth closed',
|
||||
'Eyes closed | Mouth open'
|
||||
];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { frames, mode } from '../store';
|
||||
|
||||
import { ProgressRadial } from '@skeletonlabs/skeleton';
|
||||
import { debug, info } from '$lib/logging';
|
||||
import { loadGif, loadImage } from '$lib/io';
|
||||
import { keepFocused } from '$lib/state';
|
||||
export let index: number;
|
||||
|
||||
let src: string | null;
|
||||
|
||||
let files: FileList;
|
||||
|
||||
$: loading = false;
|
||||
|
||||
// prevent the component from being unmounted when the file picker opens
|
||||
$: $keepFocused = loading;
|
||||
|
||||
$: {
|
||||
const frame = $frames[index];
|
||||
if (!frame) src = null;
|
||||
else {
|
||||
debug(frame);
|
||||
if (frame?.kind === 'still') src = frame.value.toDataURL();
|
||||
else {
|
||||
const b = new Blob([new Uint8Array(frame.data)]);
|
||||
src = URL.createObjectURL(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const openImage = async () => {
|
||||
if (mode === 'web') {
|
||||
fileInput?.click();
|
||||
loading = true;
|
||||
}
|
||||
// loading[index] = true;
|
||||
// const path = (await invoke('open_image')) as {
|
||||
// kind: 'png' | 'gif';
|
||||
// data: string;
|
||||
// };
|
||||
|
||||
// if (path) {
|
||||
// const data = await fs.readBinaryFile(path.data);
|
||||
// if (path.kind == 'png')
|
||||
// $frames[index] = {
|
||||
// kind: 'still',
|
||||
// value: await Image.load(data)
|
||||
// };
|
||||
// else {
|
||||
// $frames[index] = {
|
||||
// kind: 'GIF',
|
||||
// value: new Gif(new Promise((res) => res(data))),
|
||||
// data: data
|
||||
// };
|
||||
// }
|
||||
// } else {
|
||||
// console.error('Error loading image');
|
||||
// }
|
||||
// loading[index] = false;
|
||||
};
|
||||
|
||||
const finishLoad = async () => {
|
||||
info('Loading image in web mode');
|
||||
loading = true;
|
||||
const file = files.item(0);
|
||||
info('Got file:', file);
|
||||
if (file) {
|
||||
switch (file.type) {
|
||||
case 'image/gif':
|
||||
const gif = await loadGif(file);
|
||||
$frames[index] = gif
|
||||
? {
|
||||
kind: 'GIF',
|
||||
value: gif,
|
||||
data: new Uint8Array(await file.arrayBuffer())
|
||||
}
|
||||
: null;
|
||||
break;
|
||||
default:
|
||||
const img = await loadImage(file);
|
||||
$frames[index] = img
|
||||
? {
|
||||
kind: 'still',
|
||||
value: img
|
||||
}
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
fileInput.value = "";
|
||||
loading = false;
|
||||
};
|
||||
|
||||
const clearImage = () => {
|
||||
$frames[index] = null;
|
||||
loading = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if(loading && fileInput?.value.length === 0) loading = false;
|
||||
};
|
||||
|
||||
let fileInput: HTMLInputElement;
|
||||
</script>
|
||||
|
||||
<svelte:window on:focus={handleCancel} />
|
||||
|
||||
{#if mode === 'web'}
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
type="file"
|
||||
bind:files={files}
|
||||
on:change={finishLoad}
|
||||
on:input={() => debug("This is the input event")}
|
||||
class="hidden"
|
||||
accept="image/png, image/jpeg, image/tiff, image/gif"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="btn btn-xl p-4 w-24 md:w-32 variant-soft-primary border-token border-primary-400-500-token aspect-square cursor-pointer z-50"
|
||||
aria-label="Preview of frame {index + 1}"
|
||||
|
||||
on:click={(e) => {
|
||||
if (e.shiftKey) {
|
||||
clearImage();
|
||||
} else {
|
||||
openImage();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if loading}
|
||||
<ProgressRadial />
|
||||
{:else if src}
|
||||
<img {src} alt="Frame {{ index }}" class="w-full" />
|
||||
{/if}
|
||||
</button>
|
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import { loadRay } from '$lib/io';
|
||||
import { debug } from '$lib/logging';
|
||||
import { keepFocused } from '$lib/state';
|
||||
import { mode } from '$lib/store';
|
||||
import { setLoaded } from '$lib/utils';
|
||||
|
||||
let fileLoader: HTMLInputElement;
|
||||
let files: FileList;
|
||||
|
||||
$: loading = false;
|
||||
$: $keepFocused = loading;
|
||||
|
||||
const handleLoad = () => {
|
||||
if (mode === 'web') {
|
||||
fileLoader.click();
|
||||
loading = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebLoad = () => {
|
||||
const file = files.item(0);
|
||||
if (file) finishLoad(file);
|
||||
|
||||
fileLoader.value = '';
|
||||
};
|
||||
|
||||
const finishLoad = async (data: File | Blob) => {
|
||||
loadRay(data);
|
||||
setLoaded(await data.arrayBuffer());
|
||||
loading = false
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if mode === 'web'}
|
||||
<input type="file" class="hidden" bind:this={fileLoader} bind:files on:change={handleWebLoad} accept="application/ray,.ray" />
|
||||
{/if}
|
||||
|
||||
<button class="meta-button" on:click={handleLoad}>Load</button>
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { mode } from '$lib/store';
|
||||
import { setLoaded } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
import { packRay } from '$lib/io';
|
||||
|
||||
let downloader: HTMLAnchorElement | undefined;
|
||||
|
||||
onMount(() => {
|
||||
if (!downloader) downloader = document.createElement('a') as HTMLAnchorElement;
|
||||
});
|
||||
|
||||
const saveCurrent = async () => {
|
||||
|
||||
const data = await packRay();
|
||||
|
||||
if(mode === "web"){
|
||||
const url = URL.createObjectURL(new Blob([data]));
|
||||
if(!downloader) return;
|
||||
downloader.href = url;
|
||||
downloader.download = 'cathode.ray';
|
||||
downloader.click();
|
||||
}
|
||||
|
||||
setLoaded(data);
|
||||
};
|
||||
</script>
|
||||
|
||||
<button class="meta-button" on:click={saveCurrent}> Save </button>
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { getModalStore, type ModalSettings } from "@skeletonlabs/skeleton";
|
||||
import SettingsModal from "./SettingsModal.svelte";
|
||||
|
||||
const modal = getModalStore();
|
||||
|
||||
const onClick = () => {
|
||||
const settings: ModalSettings = {
|
||||
type: "component",
|
||||
component: {
|
||||
ref: SettingsModal,
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
modal.trigger(settings);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="meta-button" on:click={onClick}>Settings</button>
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { Tab, TabGroup } from '@skeletonlabs/skeleton';
|
||||
import SettingsPane from './SettingsPane.svelte';
|
||||
import AboutPane from './AboutPane.svelte';
|
||||
|
||||
let currTab = 0;
|
||||
</script>
|
||||
|
||||
<TabGroup class="h-[50vh] w-modal card p-4 flex flex-col" regionPanel="flex-1 flex-col">
|
||||
<Tab bind:group={currTab} value={0} name="settings">
|
||||
<strong>Settings</strong>
|
||||
</Tab>
|
||||
<Tab bind:group={currTab} value={1} name="about">
|
||||
<strong>About</strong>
|
||||
</Tab>
|
||||
<svelte:fragment slot="panel">
|
||||
{#if currTab === 0}
|
||||
<SettingsPane />
|
||||
{:else if currTab === 1}
|
||||
<AboutPane />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TabGroup>
|
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import { config, mode, type BGColor } from '$lib/store';
|
||||
import { slide } from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
<section id="settings" class="w-1/2 flex flex-col gap-4">
|
||||
<label>
|
||||
Mic Sens
|
||||
<div class="grid grid-cols-[0.75fr_0.25fr] gap-2">
|
||||
<input
|
||||
type="range"
|
||||
name="MicSens"
|
||||
id="mic-sens"
|
||||
bind:value={$config.mic_sens}
|
||||
class="range"
|
||||
max={5.0}
|
||||
step={0.1}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
name="MicSens"
|
||||
id="mic-sens"
|
||||
bind:value={$config.mic_sens}
|
||||
class="input"
|
||||
max={5.0}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Minimum Blink Interval (milliseconds)
|
||||
<input
|
||||
type="number"
|
||||
name="MicSens"
|
||||
id="mic-sens"
|
||||
bind:value={$config.blink_interval}
|
||||
class="input"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Background Color
|
||||
<select class="select" bind:value={$config.background_color.color}>
|
||||
<option value="bg-transparent" disabled={mode === 'web'}>Transparent</option>
|
||||
<option value="bg-green-500">Green</option>
|
||||
<option value="bg-blue-500">Blue</option>
|
||||
<option value="bg-pink-500">Pink</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
{#if $config.background_color.color === 'custom'}
|
||||
<div class="grid grid-cols-[auto_1fr] gap-2 my-4" transition:slide>
|
||||
<input class="input" type="color" bind:value={$config.background_color.custom} />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
bind:value={$config.background_color.custom}
|
||||
readonly
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
</section>
|
|
@ -0,0 +1,165 @@
|
|||
<script lang="ts">
|
||||
import { frames, type FrameData } from '../store';
|
||||
import { onMount } from 'svelte';
|
||||
import anime from 'animejs';
|
||||
import type { Animator } from '$lib/gifler';
|
||||
import { active, blink } from '$lib/state';
|
||||
import { debug, info } from '$lib/logging';
|
||||
|
||||
type MaybeSrc = { mode: 'png'; data: ImageBitmap } | { mode: 'gif'; data: Animator } | null;
|
||||
|
||||
$: bitMaps = [null, null, null, null] as MaybeSrc[];
|
||||
$: currFrame = 0;
|
||||
|
||||
$: src = null as MaybeSrc;
|
||||
|
||||
$: {
|
||||
if (src?.mode === 'gif' && src.data.running()) src.data.stop();
|
||||
src = bitMaps[currFrame] as MaybeSrc;
|
||||
debug('canvas source', src);
|
||||
if (src?.mode === 'gif') src.data.reset().start();
|
||||
}
|
||||
|
||||
let pos = { x: 0, y: 0 };
|
||||
|
||||
$: {
|
||||
if (!$active) {
|
||||
currFrame = 0;
|
||||
} else {
|
||||
currFrame = 1;
|
||||
}
|
||||
|
||||
if ($blink && !$active) {
|
||||
currFrame = $frames[2] ? 2 : 0;
|
||||
} else if ($blink && $active) {
|
||||
currFrame = $frames[3] ? 3 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($active) {
|
||||
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<FrameData | null>): Promise<MaybeSrc[]> => {
|
||||
return await Promise.all(
|
||||
f.map(async (v) => {
|
||||
debug('creating bitmap for', v);
|
||||
if (!v) return null;
|
||||
if (v?.kind === 'still') {
|
||||
info('Creating bitmap for still image');
|
||||
|
||||
return {
|
||||
mode: 'png',
|
||||
data: await createImageBitmap(await v?.value.toBlob())
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
mode: 'gif',
|
||||
data: (
|
||||
await v?.value.frames(
|
||||
'#png',
|
||||
(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
frame: {
|
||||
buffer: CanvasImageSource;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
) => {
|
||||
ctx.drawImage(
|
||||
frame.buffer,
|
||||
canvas.width / 2 - pos.x - frame.width / 2,
|
||||
canvas.height / 2 - pos.y - frame.height / 2
|
||||
);
|
||||
},
|
||||
false
|
||||
)
|
||||
).stop()
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
bitMaps = await createBitmaps($frames);
|
||||
console.log('bitMaps: ', bitMaps);
|
||||
frames.subscribe(async (f) => {
|
||||
debug('Updating image bitmaps');
|
||||
// make sure any loaded gifs are stopped or they'll keep drawing themselves
|
||||
bitMaps.forEach((v) => (v && v.mode === 'gif' ? v.data.stop() : null));
|
||||
bitMaps = await createBitmaps(f);
|
||||
});
|
||||
|
||||
updateCanvasSize();
|
||||
|
||||
window?.addEventListener('resize', updateCanvasSize);
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (src?.mode === 'png') {
|
||||
const img = src?.data;
|
||||
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx?.drawImage(
|
||||
img,
|
||||
canvas.width / 2 - pos.x - img.width / 2,
|
||||
canvas.height / 2 - pos.y - img.height / 2
|
||||
);
|
||||
} else if (src?.mode === 'gif' && !src.data.running()) {
|
||||
src.data.start();
|
||||
} else {
|
||||
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
} catch (e) {
|
||||
debug('Rendering error', e);
|
||||
}
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
requestAnimationFrame(update);
|
||||
});
|
||||
|
||||
const updateCanvasSize = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
info(`Set canvas size to ${canvas.width}, ${canvas.height}`);
|
||||
};
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
</script>
|
||||
|
||||
<canvas bind:this={canvas} id="png" />
|
||||
|
||||
<style lang="scss">
|
||||
canvas {
|
||||
position: absolute;
|
||||
translate: -50% -50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,145 @@
|
|||
import { Image } from 'image-js';
|
||||
import { loadedRay, mode, frames } from '$lib/store';
|
||||
import gifler, { Gif } from './gifler';
|
||||
import { Expression, load_ray, save_ray, UnpackedRay } from '@cathode/cathode-ray';
|
||||
import { base64ToArrayBuffer, getBase64 } from '$lib/utils';
|
||||
import { info, debug } from './logging';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export const loadLast = async () => {
|
||||
if (mode === 'web' && loadedRay) {
|
||||
const data = get(loadedRay);
|
||||
if (data) await loadRay(new Blob([base64ToArrayBuffer(data)]));
|
||||
} else if (mode === 'tauri') {
|
||||
//TODO
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loadFrame = async (raw: string): Promise<Gif | Image | null> => {
|
||||
const blob = new Blob([base64ToArrayBuffer(raw)]);
|
||||
try {
|
||||
return await loadImage(blob);
|
||||
} catch {
|
||||
info('Error loading still image, attempting gif');
|
||||
try {
|
||||
return await loadGif(blob);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loadImage = async (data: File | Blob): Promise<Image | null> => {
|
||||
if (mode === 'web') {
|
||||
return new Promise(async (resolve) => {
|
||||
const raw = await data.arrayBuffer();
|
||||
if (!raw) return null;
|
||||
let img = await Image.load(raw);
|
||||
if (img.width > 600 || img.height > 400) {
|
||||
img = img.resize({
|
||||
width: 600
|
||||
});
|
||||
}
|
||||
resolve(img);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loadGif = async (data: File | Blob): Promise<Gif | null> => {
|
||||
if (mode === 'web') {
|
||||
return new Promise(async (resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const raw = reader.result as string | null;
|
||||
if (!raw) return resolve(null);
|
||||
let img = gifler(raw);
|
||||
resolve(img);
|
||||
};
|
||||
reader.readAsDataURL(data);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const packRay = async () => {
|
||||
// this feels inefficient but we're basically just mapping the current expression to
|
||||
// wasm memory
|
||||
const ray = new UnpackedRay();
|
||||
const expr = new Expression();
|
||||
for (const [i, frame] of get(frames).entries()) {
|
||||
if (!frame) {
|
||||
continue;
|
||||
}
|
||||
const base64 = await getBase64(frame);
|
||||
expr.set_frame(i, base64);
|
||||
}
|
||||
|
||||
ray.add_expr(expr);
|
||||
|
||||
const data = save_ray(ray);
|
||||
ray.free();
|
||||
return data;
|
||||
}
|
||||
|
||||
export const getRay = async (data: File | Blob): Promise<UnpackedRay | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const raw = reader.result as ArrayBuffer | null;
|
||||
if (!raw) return resolve(null);
|
||||
const ray = load_ray(new Uint8Array(raw));
|
||||
resolve(ray ?? null);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(data);
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRay = async (data: File | Blob) => {
|
||||
const ray = await getRay(data);
|
||||
|
||||
const expr = ray?.get_expr(0);
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const raw = expr?.get_frame(i);
|
||||
if (!raw) {
|
||||
frames.update((v) => {
|
||||
v[i] = null;
|
||||
return v;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const image = await loadFrame(raw);
|
||||
|
||||
if (image instanceof Image) {
|
||||
frames.update((v) => {
|
||||
v[i] = {
|
||||
kind: 'still',
|
||||
value: image
|
||||
};
|
||||
return v;
|
||||
});
|
||||
} else if (image instanceof Gif) {
|
||||
frames.update((v) => {
|
||||
v[i] = {
|
||||
kind: 'GIF',
|
||||
value: image,
|
||||
data: new Uint8Array(base64ToArrayBuffer(raw))
|
||||
};
|
||||
return v;
|
||||
});
|
||||
} else {
|
||||
frames.update((v) => {
|
||||
v[i] = null;
|
||||
return v;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ray?.free();
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
import { mode } from "$lib/store"
|
||||
|
||||
export const info = (...args: any[]) => {
|
||||
if(mode === "web")
|
||||
console.log("%c[INFO]", "color: lightgreen", ":", ...args);
|
||||
}
|
||||
|
||||
export const debug = (...args: any[]) => {
|
||||
if(mode === "web")
|
||||
console.log("%c[DEBUG]", "color: yellow", ":", ...args);
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import { derived, writable } from 'svelte/store';
|
||||
import { mode } from '$lib/store';
|
||||
|
||||
export const activationLevel = writable(0);
|
||||
export const closeThreshold = writable(0);
|
||||
export const active = derived(
|
||||
[activationLevel, closeThreshold],
|
||||
([$level, $threshold]) => $level > $threshold
|
||||
);
|
||||
export const blink = writable(false);
|
||||
|
||||
export const keepFocused = writable(false);
|
||||
export const wantsTransparent = writable(false);
|
||||
|
||||
export const transparent = derived(
|
||||
[keepFocused, wantsTransparent],
|
||||
([$keepFocused, $wantsTransparent]) => !$keepFocused && $wantsTransparent
|
||||
);
|
||||
|
||||
export const initState = async () => {
|
||||
if (mode == 'tauri') {
|
||||
const { appWindow, PhysicalSize } = await import('@tauri-apps/api/window');
|
||||
|
||||
await appWindow.center();
|
||||
await appWindow.setMinSize(new PhysicalSize(720, 600));
|
||||
await appWindow.onFocusChanged(({ payload: focused }) => {
|
||||
wantsTransparent.set(!focused);
|
||||
});
|
||||
|
||||
await appWindow.listen('mouth-open', () => {
|
||||
activationLevel.set(100);
|
||||
});
|
||||
|
||||
await appWindow.listen('mouth-close', () => {
|
||||
activationLevel.update((buf) => {
|
||||
buf = buf - 5;
|
||||
return buf < 0 ? 0 : buf;
|
||||
});
|
||||
});
|
||||
|
||||
await appWindow.listen('blink', () => {
|
||||
blink.set(true);
|
||||
setTimeout(() => blink.set(false), 100);
|
||||
});
|
||||
} else {
|
||||
window?.addEventListener('mouth-open', () => {
|
||||
activationLevel.set(100);
|
||||
});
|
||||
|
||||
window?.addEventListener('mouth-close', () => {
|
||||
activationLevel.update((buf) => {
|
||||
buf = buf - 5;
|
||||
return buf < 0 ? 0 : buf;
|
||||
});
|
||||
});
|
||||
|
||||
window?.addEventListener('blink', () => {
|
||||
blink.set(true);
|
||||
setTimeout(() => blink.set(false), 100);
|
||||
});
|
||||
|
||||
window?.addEventListener('blur', () => wantsTransparent.set(true));
|
||||
|
||||
window?.addEventListener('focus', () => wantsTransparent.set(false));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
import { writable, type Writable } from 'svelte/store';
|
||||
import type { Image } from 'image-js';
|
||||
import type { Gif } from '$lib/gifler';
|
||||
import { info } from '$lib/logging';
|
||||
import { TAURI_MODE } from '$env/static/public';
|
||||
import { localStorageStore } from '@skeletonlabs/skeleton';
|
||||
import { loadLast } from './io';
|
||||
|
||||
export const mode = TAURI_MODE === 'tauri' ? 'tauri' : 'web';
|
||||
|
||||
export type BGColor = {
|
||||
color: 'bg-transparent' | 'bg-blue-500' | 'bg-green-500' | 'bg-pink-500' | 'custom';
|
||||
custom?: string;
|
||||
};
|
||||
export type Meta = {
|
||||
threshold: string | null;
|
||||
closeThreshold: string | null;
|
||||
};
|
||||
|
||||
export class WebRay {
|
||||
meta: Meta;
|
||||
frames: Array<Frame | null>;
|
||||
public constructor(
|
||||
frames: Array<Frame | null> = [null, null, null, null],
|
||||
meta: Meta = { threshold: null, closeThreshold: null }
|
||||
) {
|
||||
this.frames = frames;
|
||||
this.meta = meta;
|
||||
}
|
||||
}
|
||||
|
||||
export type Frame = {
|
||||
kind: 'png' | 'gif';
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type FrameData =
|
||||
| {
|
||||
kind: 'still';
|
||||
value: Image;
|
||||
}
|
||||
| {
|
||||
kind: 'GIF';
|
||||
value: Gif;
|
||||
data: ArrayLike<number>;
|
||||
};
|
||||
|
||||
export class Config {
|
||||
background_color: BGColor;
|
||||
blink_interval: number;
|
||||
mic_sens: number;
|
||||
|
||||
constructor() {
|
||||
this.background_color = { color: mode === 'tauri' ? 'bg-transparent' : 'bg-green-500' };
|
||||
this.blink_interval = 1500;
|
||||
this.mic_sens = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
export let frames: Writable<Array<FrameData | null>> = writable([null, null, null, null]);
|
||||
export let config: Writable<Config> =
|
||||
mode === 'web' ? localStorageStore('cathode-config', new Config()) : writable(new Config());
|
||||
export let loadedRay: Writable<string | null> | null = mode === 'web' ? localStorageStore('cathode-loaded-ray', null) : null;
|
||||
|
||||
info(`Running app in ${mode} mode`);
|
|
@ -0,0 +1,27 @@
|
|||
import { loadedRay, type FrameData } from './store';
|
||||
|
||||
export function bytesToBase64(bytes: Uint8Array): string {
|
||||
const binString = String.fromCodePoint(...bytes);
|
||||
return btoa(binString);
|
||||
}
|
||||
|
||||
export function base64ToArrayBuffer(base64: string) {
|
||||
var binaryString = atob(base64);
|
||||
var bytes = new Uint8Array(binaryString.length);
|
||||
for (var i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
export const getBase64 = async (obj: FrameData) => {
|
||||
if (obj.kind === 'still') {
|
||||
return await obj.value.toBase64();
|
||||
} else {
|
||||
return bytesToBase64(new Uint8Array(obj.data));
|
||||
}
|
||||
};
|
||||
|
||||
export const setLoaded = (data: ArrayBufferLike) => {
|
||||
loadedRay?.set(bytesToBase64(new Uint8Array(data)));
|
||||
};
|
|
@ -1,56 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,222 @@
|
|||
<script lang="ts">
|
||||
import FramePreview from '$lib/components/FramePreview.svelte';
|
||||
import { quintInOut } from 'svelte/easing';
|
||||
import { initAudio, micLevel, micThreshold } from '$lib/audio';
|
||||
import '../app.postcss';
|
||||
|
||||
// Floating UI for Popups
|
||||
import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom';
|
||||
import {
|
||||
Modal,
|
||||
Toast,
|
||||
initializeStores,
|
||||
storePopup,
|
||||
type ModalSettings,
|
||||
getToastStore,
|
||||
getModalStore
|
||||
} from '@skeletonlabs/skeleton';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Bar from '$lib/components/Bar.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { active, initState, closeThreshold, activationLevel, transparent } from '$lib/state';
|
||||
import SaveButton from '$lib/components/SaveButton.svelte';
|
||||
import LoadButton from '$lib/components/LoadButton.svelte';
|
||||
import { config, frames } from '$lib/store';
|
||||
import SettingsButton from '$lib/components/SettingsButton.svelte';
|
||||
import { debug } from '$lib/logging';
|
||||
import { loadLast } from '$lib/io';
|
||||
|
||||
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
|
||||
|
||||
|
||||
initializeStores();
|
||||
|
||||
const modal = getModalStore();
|
||||
const toast = getToastStore();
|
||||
|
||||
// import { pwaInfo } from 'virtual:pwa-info';
|
||||
|
||||
// $: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : '' ;
|
||||
|
||||
let deinitAudio: NodeJS.Timeout | undefined = undefined;
|
||||
onMount(async () => {
|
||||
try {
|
||||
deinitAudio = await initAudio();
|
||||
} catch (e) {
|
||||
toast.trigger({ message: 'Error starting audio worker', background: 'variant-ghost-error' });
|
||||
}
|
||||
await initState();
|
||||
await loadLast();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (deinitAudio) clearInterval(deinitAudio);
|
||||
});
|
||||
|
||||
const clearFrames = () => {
|
||||
const settings: ModalSettings = {
|
||||
type: 'confirm',
|
||||
title: 'Create new Ray?',
|
||||
body: 'This will clear the currently loaded Ray',
|
||||
response(ok) {
|
||||
if (ok) frames.update(() => [null, null, null, null]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!$frames.every((v) => !v)) modal.trigger(settings);
|
||||
};
|
||||
|
||||
$: solidColor = 'bg-surface-900';
|
||||
// $config.background_color.color === 'bg-transparent'
|
||||
// ? 'bg-surface-900'
|
||||
// : $config.background_color.color === "custom" ? undefined : $config.background_color.color;
|
||||
$: transparentColor =
|
||||
$config.background_color.color === 'custom' ? undefined : $config.background_color.color;
|
||||
$: bgColor = $transparent ? transparentColor : solidColor;
|
||||
</script>
|
||||
|
||||
<!-- <svelte:head>
|
||||
{@html webManifestLink}
|
||||
</svelte:head> -->
|
||||
|
||||
<Toast />
|
||||
<Modal />
|
||||
|
||||
<div
|
||||
class="{bgColor} w-screen h-screen"
|
||||
style:background-color={$config.background_color.color === 'custom' && $transparent
|
||||
? $config.background_color.custom
|
||||
: undefined}
|
||||
id="container"
|
||||
>
|
||||
<slot />
|
||||
{#if !$transparent}
|
||||
<div
|
||||
transition:fly={{
|
||||
duration: 200,
|
||||
y: -100,
|
||||
opacity: 100,
|
||||
easing: quintInOut
|
||||
}}
|
||||
class="flex w-full justify-center absolute top-4 gap-4"
|
||||
>
|
||||
<button class="meta-button" on:click={clearFrames}>New</button>
|
||||
<SaveButton />
|
||||
<LoadButton />
|
||||
<SettingsButton />
|
||||
</div>
|
||||
<div
|
||||
transition:fly={{
|
||||
duration: 200,
|
||||
x: -200,
|
||||
opacity: 100,
|
||||
easing: quintInOut
|
||||
}}
|
||||
class="flex flex-col justify-around h-full mx-10"
|
||||
>
|
||||
{#each [0, 1, 2, 3] as i}
|
||||
<FramePreview index={i} />
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
transition:fly={{
|
||||
duration: 200,
|
||||
x: 200,
|
||||
opacity: 100,
|
||||
easing: quintInOut
|
||||
}}
|
||||
class="audio flex absolute right-6 gap-10 top-1/2 translate-y-[-50%] h-5/6"
|
||||
>
|
||||
{#key $micThreshold}
|
||||
<Bar
|
||||
value={$micLevel}
|
||||
setpoint={$micThreshold * 100}
|
||||
withSetpoint
|
||||
onSetpointChange={async (e) => {
|
||||
$micThreshold = e;
|
||||
console.log(e);
|
||||
}}
|
||||
barColor={$active ? 'bg-success-500' : 'bg-blue-500'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class:stroke-success-500={$active}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"
|
||||
/>
|
||||
</svg>
|
||||
</Bar>
|
||||
{/key}
|
||||
{#key $closeThreshold}
|
||||
<Bar
|
||||
withSetpoint
|
||||
value={$activationLevel}
|
||||
setpoint={$closeThreshold}
|
||||
onSetpointChange={(y) => ($closeThreshold = y * 100)}
|
||||
barColor={'bg-blue-500'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
||||
/>
|
||||
</svg>
|
||||
</Bar>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!--
|
||||
<style lang="scss">
|
||||
// @keyframes fade-out {
|
||||
// 0% {
|
||||
// background-color: var(--active-color);
|
||||
// }
|
||||
// 100% {
|
||||
// background-color: var(--inactive-color);
|
||||
// }
|
||||
// }
|
||||
|
||||
// @keyframes fade-in {
|
||||
// 0% {
|
||||
// background-color: var(--inactive-color);
|
||||
// }
|
||||
// 100% {
|
||||
// background-color: var(--active-color);
|
||||
// }
|
||||
// }
|
||||
// .container {
|
||||
// animation-name: fade-in;
|
||||
// animation-duration: 0.2s;
|
||||
// }
|
||||
|
||||
// .container.transparent {
|
||||
// animation-name: fade-out;
|
||||
// animation-duration: 0.2s;
|
||||
// }
|
||||
|
||||
.frames {
|
||||
align-items: left;
|
||||
position: absolute;
|
||||
top: 5vh;
|
||||
left: 20px;
|
||||
bottom: 5vh;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style> -->
|
|
@ -0,0 +1,2 @@
|
|||
export const prerender = true;
|
||||
export const ssr = false;
|
|
@ -0,0 +1,6 @@
|
|||
<script lang="ts">
|
||||
import Tuber from "$lib/components/tube.svelte";
|
||||
|
||||
</script>
|
||||
|
||||
<Tuber />
|
|
@ -0,0 +1,64 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference no-default-lib="true"/>
|
||||
/// <reference lib="esnext" />
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import {build, files, version} from "$service-worker";
|
||||
|
||||
const CACHE = `cache-${version}`;
|
||||
|
||||
const ASSETS = [
|
||||
...build,
|
||||
...files,
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
async function cacheFiles() {
|
||||
const c = await caches.open(CACHE);
|
||||
await c.addAll(ASSETS);
|
||||
}
|
||||
|
||||
event.waitUntil(cacheFiles());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
// Remove previous cached data from disk
|
||||
async function deleteOldCaches() {
|
||||
for (const key of await caches.keys()) {
|
||||
if (key !== CACHE) await caches.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
event.waitUntil(deleteOldCaches());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// ignore POST requests etc
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
async function respond() {
|
||||
const url = new URL(event.request.url);
|
||||
const cache = await caches.open(CACHE);
|
||||
|
||||
// `build`/`files` can always be served from the cache
|
||||
if (ASSETS.includes(url.pathname)) {
|
||||
return cache.match(url.pathname);
|
||||
}
|
||||
|
||||
// for everything else, try the network first, but
|
||||
// fall back to the cache if we're offline
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (response.status === 200) {
|
||||
cache.put(event.request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch {
|
||||
return cache.match(event.request);
|
||||
}
|
||||
}
|
||||
|
||||
event.respondWith(respond());
|
||||
});
|
54
src/store.ts
|
@ -1,54 +0,0 @@
|
|||
import {writable} from "svelte/store";
|
||||
import type { Image } from "image-js";
|
||||
import type { Gif } from "./gifler";
|
||||
export type BGColor ="transparent" | "blue" | "green" | "pink" | {custom: string} ;
|
||||
|
||||
export type Meta = {
|
||||
threshold: string | null;
|
||||
closeThreshold: string | null;
|
||||
};
|
||||
|
||||
export class WebRay {
|
||||
meta: Meta;
|
||||
frames: Array<Frame | null>;
|
||||
public constructor(
|
||||
frames: Array<Frame | null> = [null, null, null, null],
|
||||
meta: Meta = { threshold: null, closeThreshold: null }
|
||||
) {
|
||||
this.frames = frames;
|
||||
this.meta = meta;
|
||||
}
|
||||
}
|
||||
|
||||
export type Frame = {
|
||||
kind: "png" | "gif",
|
||||
data: string
|
||||
}
|
||||
|
||||
export type FrameData = {
|
||||
kind: "still",
|
||||
value: Image,
|
||||
} | {
|
||||
kind: "GIF",
|
||||
value: Gif
|
||||
data: ArrayLike<number>
|
||||
}
|
||||
|
||||
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<FrameData | null>>([null, null, null, null]);
|
||||
export let config = writable(new Config());
|
|
@ -1,4 +0,0 @@
|
|||
export function bytesToBase64(bytes: Uint8Array): string {
|
||||
const binString = String.fromCodePoint(...bytes);
|
||||
return btoa(binString);
|
||||
}
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,37 @@
|
|||
// @ts-nocheck
|
||||
|
||||
export class AudioTrigger extends AudioWorkletProcessor {
|
||||
threshold
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.port.onmessage = (e) => {
|
||||
this.threshold = e.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
process(inputs) {
|
||||
const channels = inputs[0];
|
||||
let count = 0;
|
||||
let avg = 0;
|
||||
for (const chan of channels) {
|
||||
avg += (chan.map(v => Math.abs(v)).reduce((prev, cur) => prev + cur) / chan.length);
|
||||
count += chan.length
|
||||
}
|
||||
|
||||
avg /= count;
|
||||
avg *= 100 * 100;
|
||||
|
||||
if (avg > (this.threshold * 100) - 1) {
|
||||
this.port.postMessage('mouth-open')
|
||||
} else {
|
||||
this.port.postMessage('mouth-close');
|
||||
}
|
||||
|
||||
|
||||
this.port.postMessage(avg);
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor("audio-trigger", AudioTrigger);
|
After Width: | Height: | Size: 562 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z" />
|
||||
</svg>
|
After Width: | Height: | Size: 334 B |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z" />
|
||||
</svg>
|
After Width: | Height: | Size: 462 B |
|
@ -0,0 +1,2 @@
|
|||
User-Agent: *
|
||||
Allow: /
|
|
@ -1,10 +1,33 @@
|
|||
import sveltePreprocess from "svelte-preprocess";
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: sveltePreprocess(),
|
||||
css: css => {
|
||||
css.write('public/bundle.css');
|
||||
}
|
||||
const file = fileURLToPath(new URL('package.json', import.meta.url));
|
||||
const json = readFileSync(file, 'utf8');
|
||||
const pkg = JSON.parse(json);
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte'],
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: [ vitePreprocess()],
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter({
|
||||
fallback: 'index.html'
|
||||
}),
|
||||
env: {
|
||||
publicPrefix: 'TAURI_'
|
||||
},
|
||||
version: {
|
||||
name: pkg.version
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
export default config;
|
|
@ -0,0 +1,27 @@
|
|||
import { join } from 'path'
|
||||
import type { Config } from 'tailwindcss'
|
||||
import forms from '@tailwindcss/forms';
|
||||
import typography from '@tailwindcss/typography';
|
||||
import { skeleton } from '@skeletonlabs/tw-plugin'
|
||||
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: ['./src/**/*.{html,js,svelte,ts}', join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
forms,
|
||||
typography,
|
||||
skeleton({
|
||||
themes: {
|
||||
preset: [
|
||||
{
|
||||
name: 'hamlindigo',
|
||||
enhancements: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
} satisfies Config;
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"vite-plugin-pwa/client",
|
||||
"vite-plugin-pwa/info"
|
||||
]
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
|
|
|
@ -1,28 +1,12 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { purgeCss } from 'vite-plugin-tailwind-purgecss';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import wasm from 'vite-plugin-wasm';
|
||||
import topLevelAwait from 'vite-plugin-top-level-await';
|
||||
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
plugins: [wasm(), topLevelAwait(), sveltekit(), purgeCss()],
|
||||
|
||||
// Vite optons tailored for Tauri developemnt and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// tauri expects a fixed port, fail if that port is not available
|
||||
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
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: ["es2021", "chrome100", "safari13"],
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG,
|
||||
},
|
||||
});
|
||||
|
|