Compare commits
359 Commits
46d2969cfb
...
v4.11.163
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4d4c73c2b | ||
| 73fbc1c77a | |||
|
|
3dc732521f | ||
| b76aa3fe4a | |||
|
|
c0d1e87b8d | ||
| 04d4026e78 | |||
|
|
cfbc59d6a9 | ||
| dfe42fa942 | |||
|
|
6a8397425e | ||
| 641ec26142 | |||
|
|
1eaf246806 | ||
| 0e97e4d3b6 | |||
|
|
1636a8d0c6 | ||
| ac236750aa | |||
|
|
7e747e6c36 | ||
| 1b2cae5b12 | |||
|
|
76921c4e79 | ||
| 688efff7c9 | |||
|
|
9bc377b62a | ||
| 836ccce7a3 | |||
|
|
34c8e6e38c | ||
| e69bda3aa6 | |||
|
|
5d55e5d826 | ||
| b5902b9fc3 | |||
|
|
f60cf0bd3c | ||
| 83dec0ef54 | |||
|
|
40a76f02cd | ||
| 3def3cc456 | |||
|
|
30bd67d43a | ||
| ffeccee680 | |||
|
|
facbeee024 | ||
| 09102c7903 | |||
|
|
fa33ec7ac8 | ||
| c44f5cf14a | |||
|
|
27793d7c4f | ||
| 32b2902ef7 | |||
|
|
5cdb318854 | ||
| a28f05bb88 | |||
|
|
cecfe73b2e | ||
| b458c7918c | |||
|
|
d8660f4822 | ||
| dad83773a6 | |||
|
|
d2d35de46b | ||
| c752ec0b38 | |||
|
|
28752eb56e | ||
| 4f243a0fcf | |||
|
|
9bce616f26 | ||
| f16ab73c9a | |||
|
|
c29a15e073 | ||
| f5fe61e3eb | |||
|
|
a1a752b0fb | ||
| 7769250875 | |||
|
|
b3af9f3185 | ||
| 7906326790 | |||
|
|
3b4babb9d7 | ||
| 4b284ec747 | |||
| e1b4cfb9c8 | |||
|
|
c0266903ec | ||
| 81964d6f14 | |||
|
|
47e025cf55 | ||
| b400070ad0 | |||
| 529694ce66 | |||
|
|
791c3ab4df | ||
| de8d9db795 | |||
|
|
ffaf9ff66a | ||
| f0891ba4cc | |||
| 627b117119 | |||
|
|
b6c6659a5a | ||
| d7a73afebc | |||
| e469d51ba4 | |||
|
|
6ba03277e5 | ||
| fdb2dca794 | |||
|
|
8797bb5a22 | ||
| 4d154fd4f3 | |||
| 42e8fb71ef | |||
|
|
ff2c284984 | ||
| 75be84e472 | |||
| a3c91b306a | |||
|
|
20edc293bb | ||
| 6803eac8fb | |||
| fa5592f1e4 | |||
|
|
78601be6f4 | ||
| c940bdc78c | |||
|
|
9471fe2f5d | ||
| 57f7fe7086 | |||
| a2364f2622 | |||
|
|
df2220581d | ||
| 198eb0fe04 | |||
| fff114abef | |||
|
|
7013f1c22d | ||
| 1dbc7d6cc4 | |||
| f93f109d4a | |||
|
|
f707aa98f5 | ||
| 8a0be09d5d | |||
| a1b86f00ba | |||
|
|
c9f569a2b2 | ||
| 7a736b3adf | |||
| b6ecd16148 | |||
|
|
30d6b8dc74 | ||
| b6bcd8d05b | |||
|
|
0b5fe5f04c | ||
| c629c07bf9 | |||
|
|
fcd7d3e7ea | ||
| db994a5d43 | |||
| fea9ebd501 | |||
|
|
9238d80adb | ||
| a28d3c26b5 | |||
|
|
161030927f | ||
| c431c1d547 | |||
| 739c863601 | |||
|
|
5b6c213101 | ||
| 5acf981bd4 | |||
|
|
14b27717a6 | ||
| bd3e1d5b9c | |||
| a668544c6e | |||
|
|
dd8f4eb47d | ||
| 54cb11e55b | |||
| dec48f6c1d | |||
|
|
8f776f4f7c | ||
| c6aff43467 | |||
| 9f95ef1ecd | |||
|
|
aafda0d437 | ||
| f97a8d4929 | |||
|
|
a8c742376f | ||
| 938ab53d4b | |||
|
|
62352412d3 | ||
| fd33b3f6b7 | |||
| 3c0b4cf108 | |||
|
|
4eb48726ae | ||
| ba17993001 | |||
| cf39f0acd9 | |||
|
|
bce2cbec94 | ||
| f401b5ea3d | |||
| 4a5e3a1691 | |||
|
|
dc50f74971 | ||
| a198eb6896 | |||
| dc9b12dd26 | |||
|
|
6ea0e7a4fd | ||
| 51b13e0fcd | |||
|
|
1d7a48c57e | ||
| 0e34ca2063 | |||
|
|
6248d5843a | ||
| 5af79606c2 | |||
| 3cc31d8e05 | |||
|
|
0f7dd5f55c | ||
| de4f6ebbc9 | |||
|
|
fd25143489 | ||
| 5265674a58 | |||
|
|
39376e0003 | ||
| cf1f7fcec8 | |||
|
|
153e55705a | ||
| 77cdd66027 | |||
| 80580391cb | |||
|
|
6f0988f873 | ||
| 99358803d2 | |||
| 6a347d977f | |||
|
|
c03a4d3f41 | ||
| 50818f1d76 | |||
|
|
0f8c9b83f4 | ||
| 76cae46005 | |||
| ca22449d54 | |||
|
|
dda1041268 | ||
| b4f8f13213 | |||
| 9ad9971a03 | |||
| c38dc7bbf6 | |||
|
|
aaed71e37b | ||
| ed85051480 | |||
| d0524c2bc7 | |||
|
|
8a0f8f5422 | ||
| 8a95d9e67d | |||
| b8a126fd69 | |||
|
|
6b18e5c8ee | ||
| 27c2ce021d | |||
|
|
de6620245c | ||
| 21e895efff | |||
| e9487c3043 | |||
|
|
686749ac55 | ||
| dd790d3336 | |||
|
|
c8ceeccc8c | ||
| a592766e0a | |||
| 7acf8ea877 | |||
|
|
2a87133d14 | ||
| c960bc5410 | |||
| fde412fafa | |||
|
|
476d0c71fc | ||
| df6fa0fa42 | |||
|
|
246521b7b3 | ||
| 8ccc3ecb78 | |||
|
|
19ce4b43ca | ||
| 8ea4c58a5b | |||
| 465fa6b57b | |||
|
|
12f0d7f8d3 | ||
| f11078bb5c | |||
| 4e6835c741 | |||
|
|
4584367041 | ||
| bf71bd9726 | |||
| 8cfc96688e | |||
|
|
eedd040203 | ||
| 8396834b08 | |||
| cb136002b9 | |||
|
|
9a6dc126db | ||
| c9352327eb | |||
| 9985b0931c | |||
|
|
5faf0056d7 | ||
| cf6bedf58a | |||
| 03ecdbb54f | |||
|
|
1222651d17 | ||
| 1f34f726bf | |||
| 5b1bd1cffa | |||
| 050352fd2b | |||
|
|
422b680b18 | ||
| 6510522b6b | |||
| 48a189ed6a | |||
|
|
a72758d9ef | ||
| 7ee178f6e9 | |||
| 50885a9da4 | |||
|
|
5d39692e41 | ||
| de87a55582 | |||
| a3c2510830 | |||
|
|
6b9a57e243 | ||
| 4fa0ce0d4a | |||
| e90b8d43cb | |||
|
|
379a957a40 | ||
| fc18b0b902 | |||
| a4429fe9c2 | |||
|
|
fc4229cea7 | ||
| 69fcfd12f3 | |||
| 843b00166e | |||
|
|
4dbc8c93d1 | ||
| a196d2706f | |||
| 643c8a7291 | |||
|
|
e503829d3a | ||
| 78fce9b5b0 | |||
| 42f21c274f | |||
|
|
d39d006296 | ||
| 8e8b5a732c | |||
| edd2c457ed | |||
|
|
097f45d8ca | ||
| 53422c8718 | |||
| 89ecc91acd | |||
|
|
abd22d4875 | ||
| ccb922fbe0 | |||
| b80128f029 | |||
|
|
c737edfa43 | ||
| 735511fad0 | |||
|
|
d650e34a8f | ||
| 1cb8fd6e49 | |||
|
|
7857e42c5a | ||
| 7e667a1708 | |||
| 42a01c357f | |||
|
|
8bd238c793 | ||
| 7b28d2fcba | |||
| 362ca62296 | |||
|
|
d0bfb72581 | ||
| c5d011958c | |||
|
|
4b7e9245b6 | ||
| d906a3bc73 | |||
| f287257a27 | |||
| 5931d14cae | |||
|
|
2a98dc1f25 | ||
| 46f72b94fa | |||
|
|
0bcaedda73 | ||
| 2dfef751c0 | |||
| bf786742a5 | |||
|
|
567980feae | ||
| 90a0dad0ec | |||
|
|
abc5ad66be | ||
| 18dae047e9 | |||
| 3849f52852 | |||
|
|
79547c2884 | ||
| 4b5cc29266 | |||
| b54a003a96 | |||
|
|
7563926d8f | ||
| 1fea022306 | |||
| 9bfa6e3040 | |||
|
|
be822877a9 | ||
| ff13ae817e | |||
|
|
a59287669b | ||
| 09cfe7dc84 | |||
|
|
823fd8593b | ||
| 62e53b05ff | |||
|
|
aaa9bae16b | ||
| 07feddd44b | |||
|
|
b71268a38f | ||
| 3904eec992 | |||
|
|
5c6d69ba51 | ||
| 6cc20baad6 | |||
| ce0c8277ec | |||
|
|
9a3e24f55c | ||
| d03712a430 | |||
|
|
e7d240e2d2 | ||
| 9ec515e351 | |||
| 4be9d2bc21 | |||
|
|
1666724c91 | ||
| 5d2e91befa | |||
|
|
03a12320a1 | ||
| 94fcc02138 | |||
| 855c3c45ac | |||
|
|
280582548c | ||
| 6a2dceaeac | |||
| 8deec2e7f8 | |||
|
|
3fe7eaf482 | ||
| 4a2352edd4 | |||
| a87c475c1a | |||
|
|
a575e073e3 | ||
| 571427aa09 | |||
| 1a04f9b503 | |||
| 637d1ab3c4 | |||
|
|
d17df23b29 | ||
| 7675365a82 | |||
| 40691222a1 | |||
|
|
e37ea9c7ad | ||
| 750976ad71 | |||
| 65e3cad890 | |||
|
|
435d83c4cc | ||
| 7af732f24c | |||
| c3f78ab76e | |||
|
|
75af85a300 | ||
| c0a1a7266d | |||
| 3da53b6ded | |||
|
|
914a8a2ba4 | ||
| 9bde8b088b | |||
| 58a1545774 | |||
|
|
4cfd30ef79 | ||
| 45aa4c8a1b | |||
|
|
01ebebdb18 | ||
| 2490ec7de5 | |||
|
|
0d8eba16c6 | ||
| fb1c4622bd | |||
| 56d155448e | |||
|
|
227079ed9d | ||
| 7cb3349da9 | |||
| 22378c001e | |||
|
|
5d074aa17e | ||
| 386cfc7710 | |||
| 1c87bbc6a3 | |||
|
|
8eb0cd9e1c | ||
| b757e1f553 | |||
| 497923ecb9 | |||
|
|
faad0cca71 | ||
| e016eb4d5f | |||
| 9cf7113dbe | |||
|
|
3ba8df423c | ||
| edcd995b88 | |||
| 2e0bf52a40 | |||
|
|
17ad5f7f51 | ||
| a1afb05683 | |||
| 90ee0aff90 | |||
|
|
209d337640 | ||
| a2e71269e1 | |||
| 3493508b8a | |||
|
|
9fc058954f | ||
| 687b42872c | |||
|
|
9f090145fe | ||
| 8b43d7546e | |||
| 66b9dde909 | |||
|
|
70d66fdf1d | ||
| 54bedf0fcd | |||
| 2ef6e225f4 |
@@ -11,10 +11,9 @@ on:
|
||||
jobs:
|
||||
increment-version:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
outputs:
|
||||
version: ${{ steps.bump.outputs.VERSION }}
|
||||
should_build: ${{ steps.bump.outputs.SHOULD_BUILD }}
|
||||
version: ${{ steps.finalize.outputs.VERSION }}
|
||||
should_build: ${{ steps.decide.outputs.SHOULD_BUILD }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -22,9 +21,35 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Decide version behavior
|
||||
id: decide
|
||||
run: |
|
||||
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||
COMMIT_MSG="${{ github.event.head_commit.message || '' }}"
|
||||
|
||||
SHOULD_BUMP=false
|
||||
SHOULD_BUILD=true
|
||||
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ] && ! echo "$COMMIT_MSG" | grep -qi '\[skip ci\]'; then
|
||||
SHOULD_BUMP=true
|
||||
fi
|
||||
|
||||
if echo "$COMMIT_MSG" | grep -qi '\[skip ci\]'; then
|
||||
SHOULD_BUILD=false
|
||||
fi
|
||||
|
||||
echo "CURRENT_VERSION=$CURRENT" >> $GITHUB_OUTPUT
|
||||
echo "SHOULD_BUMP=$SHOULD_BUMP" >> $GITHUB_OUTPUT
|
||||
echo "SHOULD_BUILD=$SHOULD_BUILD" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Current version: $CURRENT"
|
||||
echo "Should bump: $SHOULD_BUMP"
|
||||
echo "Should build: $SHOULD_BUILD"
|
||||
|
||||
- name: Bump patch version
|
||||
id: bump
|
||||
if: ${{ steps.decide.outputs.SHOULD_BUMP == 'true' }}
|
||||
run: |
|
||||
# Get current version from package.json
|
||||
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||
@@ -45,15 +70,40 @@ jobs:
|
||||
grep '"version"' package.json | head -1
|
||||
|
||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "SHOULD_BUILD=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and push version bump
|
||||
if: ${{ steps.decide.outputs.SHOULD_BUMP == 'true' }}
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git add package.json
|
||||
git commit -m "chore: bump version to ${{ steps.bump.outputs.VERSION }} [skip ci]"
|
||||
git push
|
||||
for attempt in 1 2 3; do
|
||||
git fetch origin main
|
||||
git rebase origin/main || {
|
||||
git rebase --abort || true
|
||||
echo "Failed to rebase version bump onto latest main"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if git push origin HEAD:main; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Push rejected on attempt ${attempt}, retrying against latest main"
|
||||
done
|
||||
|
||||
echo "Failed to push version bump after 3 attempts"
|
||||
exit 1
|
||||
|
||||
- name: Finalize version output
|
||||
id: finalize
|
||||
run: |
|
||||
if [ -n "${{ steps.bump.outputs.VERSION }}" ]; then
|
||||
echo "VERSION=${{ steps.bump.outputs.VERSION }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "VERSION=${{ steps.decide.outputs.CURRENT_VERSION }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-windows:
|
||||
runs-on: windows
|
||||
@@ -70,7 +120,19 @@ jobs:
|
||||
- name: Checkout submodules manually
|
||||
shell: powershell
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
git submodule sync --recursive
|
||||
|
||||
if (git submodule update --init --recursive) {
|
||||
Write-Host "Submodules checked out at pinned commits."
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Warning "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
|
||||
git submodule deinit -f --all
|
||||
git submodule update --init --recursive --remote
|
||||
|
||||
Write-Host "Resolved submodule commit:"
|
||||
git -C cinny rev-parse HEAD
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
@@ -83,11 +145,11 @@ jobs:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies (root)
|
||||
run: npm install --prefer-offline
|
||||
run: npm ci --prefer-offline
|
||||
|
||||
- name: Install dependencies (cinny)
|
||||
working-directory: ./cinny
|
||||
run: npm install --prefer-offline
|
||||
run: npm ci --prefer-offline
|
||||
|
||||
- name: Clean previous builds
|
||||
shell: powershell
|
||||
@@ -96,42 +158,26 @@ jobs:
|
||||
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
|
||||
|
||||
- name: Build Electron app
|
||||
run: npm run build:win
|
||||
run: npm run build:local -- --win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
|
||||
- name: Upload Squirrel Setup (x64)
|
||||
- name: Upload NSIS Installer (x64)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Paarrot-Setup-x64.exe
|
||||
path: dist-electron/squirrel-windows/Paarrot-*-win-x64.exe
|
||||
path: dist-electron/Paarrot-*-win-x64.exe
|
||||
|
||||
- name: Upload latest.yml
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: latest.yml
|
||||
path: dist-electron/latest.yml
|
||||
|
||||
- name: List build output
|
||||
shell: powershell
|
||||
run: |
|
||||
Get-ChildItem -Path dist-electron -Recurse | Select-Object FullName
|
||||
|
||||
- name: Copy RELEASES file for upload
|
||||
shell: powershell
|
||||
run: |
|
||||
if (Test-Path "dist-electron/squirrel-windows/RELEASES") {
|
||||
Copy-Item "dist-electron/squirrel-windows/RELEASES" -Destination "dist-electron/RELEASES"
|
||||
} else {
|
||||
Write-Error "RELEASES file not found for x64"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Upload Squirrel RELEASES
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: RELEASES
|
||||
path: dist-electron/RELEASES
|
||||
|
||||
- name: Upload Squirrel nupkg (x64)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Paarrot-x64.nupkg
|
||||
path: dist-electron/squirrel-windows/*.nupkg
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -142,8 +188,25 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
submodules: false
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Checkout submodules manually
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git submodule sync --recursive
|
||||
|
||||
if git submodule update --init --recursive; then
|
||||
echo "Submodules checked out at pinned commits."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
|
||||
git submodule deinit -f --all
|
||||
git submodule update --init --recursive --remote
|
||||
|
||||
echo "Resolved submodule commit:"
|
||||
git -C cinny rev-parse HEAD
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
@@ -166,15 +229,21 @@ jobs:
|
||||
rm -rf cinny/dist dist-electron
|
||||
|
||||
- name: Build Electron app
|
||||
run: npm run build:linux
|
||||
run: npm run build:local -- --linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Paarrot-Linux-x64.AppImage
|
||||
path: dist-electron/Paarrot-*.AppImage
|
||||
|
||||
- name: Upload latest-linux.yml
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: latest-linux.yml
|
||||
path: dist-electron/latest-linux.yml
|
||||
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -232,7 +301,7 @@ jobs:
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release-files
|
||||
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.nupkg" -o -name "RELEASES*" \) -exec cp {} release-files/ \;
|
||||
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.yml" \) -exec cp {} release-files/ \;
|
||||
ls -la release-files/
|
||||
|
||||
- name: Create or Update Release
|
||||
@@ -244,6 +313,28 @@ jobs:
|
||||
API_BASE="http://synbox.ruv.wtf:8418/api/v1"
|
||||
REPO="litruv/cinny-desktop"
|
||||
|
||||
# Delete old releases (keep only current)
|
||||
echo "Cleaning up old releases..."
|
||||
OLD_RELEASES=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases" | jq -r '.[] | select(.tag_name != "'${TAG_NAME}'") | .id')
|
||||
|
||||
for OLD_ID in $OLD_RELEASES; do
|
||||
echo "Deleting old release ${OLD_ID}..."
|
||||
OLD_TAG=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
|
||||
|
||||
# Delete release
|
||||
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}"
|
||||
|
||||
# Delete tag
|
||||
if [ -n "$OLD_TAG" ]; then
|
||||
echo "Deleting old tag ${OLD_TAG}..."
|
||||
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/tags/${OLD_TAG}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if release exists
|
||||
RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
|
||||
@@ -283,3 +374,67 @@ jobs:
|
||||
done
|
||||
|
||||
echo "Release complete!"
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
TAG_NAME="v${VERSION}"
|
||||
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
||||
|
||||
# Delete old GitHub releases (keep only current)
|
||||
echo "Cleaning up old GitHub releases..."
|
||||
OLD_RELEASES=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r '.[] | select(.tag_name != "'${TAG_NAME}'") | .id')
|
||||
|
||||
for OLD_ID in $OLD_RELEASES; do
|
||||
echo "Deleting old GitHub release ${OLD_ID}..."
|
||||
OLD_TAG=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
|
||||
|
||||
# Delete release
|
||||
curl -s -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}"
|
||||
|
||||
# Delete tag
|
||||
if [ -n "$OLD_TAG" ]; then
|
||||
echo "Deleting old GitHub tag ${OLD_TAG}..."
|
||||
curl -s -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/git/refs/tags/${OLD_TAG}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if release exists on GitHub
|
||||
RELEASE_ID=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Creating new GitHub release for ${TAG_NAME}..."
|
||||
RELEASE_ID=$(curl -s -X POST \
|
||||
-H "Authorization: token ${GH_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r '.id')
|
||||
echo "Created GitHub release with ID: ${RELEASE_ID}"
|
||||
else
|
||||
echo "GitHub release exists with ID: ${RELEASE_ID}"
|
||||
fi
|
||||
|
||||
# Upload assets to GitHub
|
||||
echo "Uploading assets to GitHub..."
|
||||
UPLOAD_URL=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/${RELEASE_ID}" | jq -r '.upload_url' | sed 's/{?name,label}//')
|
||||
|
||||
for FILE in release-files/*; do
|
||||
FILENAME=$(basename "$FILE")
|
||||
echo "Uploading ${FILENAME} to GitHub..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GH_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${FILE}" \
|
||||
"${UPLOAD_URL}?name=${FILENAME}"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "GitHub release complete!"
|
||||
|
||||
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
||||
github: ajbura
|
||||
liberapay: ajbura
|
||||
open_collective: cinny
|
||||
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Report a bug
|
||||
labels: 'type: bug'
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## First of all
|
||||
1. Please search for [existing issues](https://github.com/cinnyapp/cinny-desktop/issues?q=is%3Aissue) about this problem first.
|
||||
2. Make sure Cinny is up to date.
|
||||
3. Make sure it's an issue with Cinny and not something else you are using.
|
||||
4. Remember to be friendly.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear description of what the bug is. Include screenshots if applicable.
|
||||
placeholder: Bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Go to ...
|
||||
2. Click on ...
|
||||
3. See error
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear description of what you expected to happen.
|
||||
|
||||
- type: textarea
|
||||
id: info
|
||||
attributes:
|
||||
label: Platform and versions
|
||||
description: "Provide OS, browser and Cinny version with your Homeserver."
|
||||
placeholder: |
|
||||
1. OS: [e.g. Windows 10, MacOS]
|
||||
2. Cinny version: [e.g. 1.8.1]
|
||||
3. Matrix homeserver: [e.g. matrix.org]
|
||||
4. Downloaded from: [e.g. GitHub, Flatpak]
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +0,0 @@
|
||||
contact_links:
|
||||
- name: 💬 Matrix Chat
|
||||
url: https://matrix.to/#/#cinny:matrix.org
|
||||
about: Ask questions and talk to other Cinny users and the maintainers
|
||||
34
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea
|
||||
labels: 'type: feature'
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Describe the problem
|
||||
description: A clear description of the problem this feature would solve
|
||||
placeholder: "I'm always frustrated when..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: "Describe the solution you'd like"
|
||||
description: A clear description of what change you would like
|
||||
placeholder: "I would like to..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: "Any alternative solutions you've considered"
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,22 +0,0 @@
|
||||
<!-- Please read https://github.com/cinnyapp/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
|
||||
|
||||
### Description
|
||||
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
|
||||
|
||||
Fixes #
|
||||
|
||||
#### Type of change
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
### Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
3
.github/SECURITY.md
vendored
@@ -1,3 +0,0 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
|
||||
30
.github/dependabot.yml
vendored
@@ -1,30 +0,0 @@
|
||||
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# - package-ecosystem: npm
|
||||
# directory: /
|
||||
# schedule:
|
||||
# interval: weekly
|
||||
# day: "tuesday"
|
||||
# time: "01:00"
|
||||
# timezone: "Asia/Kolkata"
|
||||
# open-pull-requests-limit: 15
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
# - package-ecosystem: cargo
|
||||
# directory: /src-tauri/
|
||||
# schedule:
|
||||
# interval: weekly
|
||||
# day: "tuesday"
|
||||
# time: "01:00"
|
||||
# timezone: "Asia/Kolkata"
|
||||
# open-pull-requests-limit: 5
|
||||
21
.github/renovate.json
vendored
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":dependencyDashboardApproval"
|
||||
],
|
||||
"labels": [
|
||||
"Dependencies"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": [
|
||||
"lockFileMaintenance"
|
||||
]
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
},
|
||||
"dependencyDashboard": true
|
||||
}
|
||||
22
.github/workflows/archive.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: "Upload zip-archive"
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
zip-archive:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Create zip including submodules
|
||||
run: |
|
||||
cd ..
|
||||
zip ${{ github.event.repository.name }}/${{ github.event.repository.name }}-${{ github.ref_name }}.zip ${{ github.event.repository.name }} -r
|
||||
- name: Upload zip to release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
with:
|
||||
files: |
|
||||
${{ github.event.repository.name }}-${{ github.ref_name }}.zip
|
||||
36
.github/workflows/cla.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: 'CLA Assistant'
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened, closed, synchronize]
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'CLA Assistant'
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.6.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
|
||||
with:
|
||||
path-to-signatures: 'signatures.json'
|
||||
path-to-document: 'https://github.com/cinnyapp/cla/blob/main/cla.md' # e.g. a CLA or a DCO document
|
||||
# branch should not be protected
|
||||
branch: 'main'
|
||||
allowlist: ajbura,bot*
|
||||
|
||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||
remote-organization-name: cinnyapp
|
||||
remote-repository-name: cla
|
||||
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
|
||||
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
|
||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
26
.github/workflows/lockfile.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: NPM Lockfile Changes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package-lock.json'
|
||||
|
||||
jobs:
|
||||
lockfile_changes:
|
||||
runs-on: ubuntu-latest
|
||||
# Permission overwrite is required for Dependabot PRs, see "Common issues" below.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: NPM Lockfile Changes
|
||||
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Optional inputs, can be deleted safely if you are happy with default values.
|
||||
collapsibleThreshold: 25
|
||||
failOnDowngrade: false
|
||||
path: package-lock.json
|
||||
updateComment: true
|
||||
40
.github/workflows/test.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: "Build pull request"
|
||||
on:
|
||||
#pull_request:
|
||||
#types: ['opened', 'synchronize']
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, ubuntu-20.04, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 20.12.2
|
||||
cache: 'npm'
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1.0.7
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||
- name: Install cinny dependencies
|
||||
run: cd cinny && npm ci
|
||||
- name: Install tauri dependencies
|
||||
run: npm ci
|
||||
- name: Build desktop app with Tauri
|
||||
uses: tauri-apps/tauri-action@v0.5.14
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
1
.gitignore
vendored
@@ -4,3 +4,4 @@ node_modules
|
||||
devAssets
|
||||
|
||||
.DS_Store
|
||||
dist-electron/
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"liveServer.settings.port": 5501
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
# Paarrot API Quick Start
|
||||
|
||||
A local HTTP API server for controlling Paarrot from external devices like Stream Deck, scripts, and automation tools.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
The API server starts automatically when you run Paarrot. It listens on `http://127.0.0.1:33384`.
|
||||
|
||||
### Test the API
|
||||
|
||||
**Option 1: Using Postman**
|
||||
|
||||
Import the Postman collection for easy testing:
|
||||
1. Open Postman
|
||||
2. Click **Import** → **File**
|
||||
3. Select `paarrot-api.postman_collection.json`
|
||||
4. All endpoints will be ready to use!
|
||||
|
||||
The collection is automatically updated on git push.
|
||||
|
||||
**Option 2: Using the test script**
|
||||
```bash
|
||||
node test-api.js
|
||||
```
|
||||
|
||||
**Option 3: Using curl**
|
||||
```bash
|
||||
./test-api.sh
|
||||
```
|
||||
|
||||
**Option 4: Manual curl commands**
|
||||
```bash
|
||||
# Health check
|
||||
curl http://127.0.0.1:33384/health
|
||||
|
||||
# Toggle mute
|
||||
curl -X POST http://127.0.0.1:33384/mute/toggle
|
||||
|
||||
# Send message
|
||||
curl -X POST http://127.0.0.1:33384/message/current \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Hello from API!"}'
|
||||
```
|
||||
|
||||
## 📝 Quick Reference
|
||||
|
||||
### Common Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/health` | GET | Check if API is running |
|
||||
| `/status` | GET | Get app status (mute, deafen, etc.) |
|
||||
| `/mute/toggle` | POST | Toggle microphone mute |
|
||||
| `/deafen/toggle` | POST | Toggle deafen |
|
||||
| `/channels` | GET | Get list of rooms |
|
||||
| `/channel` | POST | Switch to a room |
|
||||
| `/message/current` | POST | Send message to current room |
|
||||
|
||||
## 🎮 Stream Deck Integration
|
||||
|
||||
1. Install the "API Ninja" or "HTTP Request" plugin
|
||||
2. Create buttons with these settings:
|
||||
|
||||
**Mute Toggle Button:**
|
||||
- URL: `http://127.0.0.1:33384/mute/toggle`
|
||||
- Method: POST
|
||||
|
||||
**Quick Message Button:**
|
||||
- URL: `http://127.0.0.1:33384/message/current`
|
||||
- Method: POST
|
||||
- Body: `{"message": "BRB!"}`
|
||||
|
||||
## 🔧 Integration with Paarrot
|
||||
|
||||
To enable full functionality, you need to implement the action handlers in the Cinny frontend:
|
||||
|
||||
1. Import the API handler in your app initialization:
|
||||
```typescript
|
||||
import { initPaarrotAPI } from './app/paarrot-api';
|
||||
|
||||
// After Matrix client is initialized
|
||||
initPaarrotAPI(matrixClient);
|
||||
```
|
||||
|
||||
2. Implement the TODO items in `cinny/src/app/paarrot-api.ts`:
|
||||
- Mute/unmute logic (WebRTC audio)
|
||||
- Deafen logic (WebRTC audio output)
|
||||
- Navigation to rooms (router integration)
|
||||
- Get current room from URL/state
|
||||
|
||||
## 📖 Full Documentation
|
||||
|
||||
See [API.md](API.md) for complete API documentation including:
|
||||
- All available endpoints
|
||||
- Request/response formats
|
||||
- Error handling
|
||||
- Code examples in multiple languages
|
||||
- Detailed integration guide
|
||||
|
||||
## 🛠️ Configuration
|
||||
|
||||
Edit `electron/api-server.js` to customize:
|
||||
- Port number (default: 33384)
|
||||
- Timeout duration (default: 10s)
|
||||
- CORS settings
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
**API not responding:**
|
||||
1. Check if Paarrot is running
|
||||
2. Look for "Paarrot API server listening" in console logs
|
||||
3. Verify port 33384 is not in use: `lsof -i :33384`
|
||||
|
||||
**Actions not working:**
|
||||
1. Check browser console for errors
|
||||
2. Ensure `initPaarrotAPI()` is called in your app
|
||||
3. Implement the TODO items in `paarrot-api.ts`
|
||||
|
||||
**Port in use:**
|
||||
Edit `electron/api-server.js` and change the port number in the constructor.
|
||||
|
||||
## 📦 Files
|
||||
|
||||
- `electron/api-server.js` - API server implementation
|
||||
- `cinny/src/app/paarrot-api.ts` - Client-side handler (needs implementation)
|
||||
- `test-api.js` - Node.js test script
|
||||
- `test-api.sh` - Bash test script
|
||||
- `API.md` - Full documentation
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
- API only listens on localhost (127.0.0.1)
|
||||
- Not accessible from network
|
||||
- No authentication required (local only)
|
||||
- CORS enabled for all origins (safe since localhost only)
|
||||
|
||||
## 💡 Examples
|
||||
|
||||
**Python Script:**
|
||||
```python
|
||||
import requests
|
||||
|
||||
def toggle_mute():
|
||||
r = requests.post('http://127.0.0.1:33384/mute/toggle')
|
||||
print(r.json())
|
||||
|
||||
toggle_mute()
|
||||
```
|
||||
|
||||
**JavaScript:**
|
||||
```javascript
|
||||
async function sendQuickMessage(msg) {
|
||||
const res = await fetch('http://127.0.0.1:33384/message/current', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: msg })
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. Run Paarrot: `npm run dev`
|
||||
2. Test the API: `node test-api.js`
|
||||
3. Integrate the handler: Import and call `initPaarrotAPI()`
|
||||
4. Implement the TODO items in `paarrot-api.ts`
|
||||
5. Create Stream Deck buttons or automation scripts!
|
||||
|
||||
---
|
||||
|
||||
For more details, see [API.md](API.md)
|
||||
537
API.md
@@ -1,537 +0,0 @@
|
||||
# Paarrot API Server Documentation
|
||||
|
||||
The Paarrot API server provides a local HTTP API for controlling the Paarrot desktop app from external applications like Stream Deck, keyboard macros, scripts, and other automation tools.
|
||||
|
||||
## Server Details
|
||||
|
||||
- **Base URL**: `http://127.0.0.1:33384`
|
||||
- **Protocol**: HTTP
|
||||
- **Content-Type**: `application/json`
|
||||
- **CORS**: Enabled for all origins
|
||||
|
||||
## Authentication
|
||||
|
||||
Currently, no authentication is required. The API server only listens on localhost (127.0.0.1) for security.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Health Check
|
||||
|
||||
Check if the API server is running.
|
||||
|
||||
**Endpoint**: `GET /health`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"app": "Paarrot API",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Status
|
||||
|
||||
Get the current status of the app (mute, deafen, current room, etc.).
|
||||
|
||||
**Endpoint**: `GET /status`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"muted": false,
|
||||
"deafened": false,
|
||||
"currentRoom": "!abc123:matrix.org",
|
||||
"connected": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Mute/Unmute
|
||||
|
||||
Set the mute status.
|
||||
|
||||
**Endpoint**: `POST /mute`
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"muted": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"muted": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Toggle Mute
|
||||
|
||||
Toggle the current mute status.
|
||||
|
||||
**Endpoint**: `POST /mute/toggle`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"muted": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Deafen/Undeafen
|
||||
|
||||
Set the deafen status (deafening also mutes).
|
||||
|
||||
**Endpoint**: `POST /deafen`
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"deafened": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"deafened": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Toggle Deafen
|
||||
|
||||
Toggle the current deafen status.
|
||||
|
||||
**Endpoint**: `POST /deafen/toggle`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"deafened": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Change Channel/Room
|
||||
|
||||
Switch to a different Matrix room.
|
||||
|
||||
**Endpoint**: `POST /channel`
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"roomId": "!abc123:matrix.org"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"roomId": "!abc123:matrix.org",
|
||||
"roomName": "General"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Channels/Rooms
|
||||
|
||||
Get a list of available rooms/channels.
|
||||
|
||||
**Endpoint**: `GET /channels`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"roomId": "!abc123:matrix.org",
|
||||
"name": "General",
|
||||
"server": "matrix.org",
|
||||
"isDirect": false,
|
||||
"avatar": null
|
||||
},
|
||||
{
|
||||
"roomId": "!def456:matrix.org",
|
||||
"name": "Random",
|
||||
"server": "matrix.org",
|
||||
"isDirect": false,
|
||||
"avatar": null
|
||||
},
|
||||
{
|
||||
"roomId": "!xyz789:example.com",
|
||||
"name": "General",
|
||||
"server": "example.com",
|
||||
"isDirect": false,
|
||||
"avatar": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `server` field helps distinguish between rooms with the same name on different servers.
|
||||
|
||||
---
|
||||
|
||||
### Send Message
|
||||
|
||||
Send a message to a specific room.
|
||||
|
||||
**Endpoint**: `POST /message`
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"roomId": "!abc123:matrix.org",
|
||||
"message": "Hello from the API!"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"eventId": "$abc123",
|
||||
"roomId": "!abc123:matrix.org"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Send Message to Current Room
|
||||
|
||||
Send a message to the currently active room.
|
||||
|
||||
**Endpoint**: `POST /message/current`
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"message": "Hello from the API!"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"eventId": "$abc123",
|
||||
"roomId": "!abc123:matrix.org"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Current Room
|
||||
|
||||
Get information about the currently active room.
|
||||
|
||||
**Endpoint**: `GET /room/current`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"roomId": "!abc123:matrix.org",
|
||||
"name": "General",
|
||||
"server": "matrix.org",
|
||||
"avatar": "mxc://matrix.org/abc123",
|
||||
"isDirect": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
All endpoints return error responses in the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message here"
|
||||
}
|
||||
```
|
||||
|
||||
Common HTTP status codes:
|
||||
- `200 OK` - Request successful
|
||||
- `400 Bad Request` - Missing or invalid parameters
|
||||
- `404 Not Found` - Endpoint not found
|
||||
- `500 Internal Server Error` - Server error
|
||||
|
||||
---
|
||||
|
||||
## Example Usage
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
# Toggle mute
|
||||
curl -X POST http://127.0.0.1:33384/mute/toggle
|
||||
|
||||
# Send message to current room
|
||||
curl -X POST http://127.0.0.1:33384/message/current \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Hello!"}'
|
||||
|
||||
# Change channel
|
||||
curl -X POST http://127.0.0.1:33384/channel \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"roomId": "!abc123:matrix.org"}'
|
||||
```
|
||||
|
||||
### JavaScript/Node.js
|
||||
|
||||
```javascript
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
// Toggle mute
|
||||
async function toggleMute() {
|
||||
const response = await fetch('http://127.0.0.1:33384/mute/toggle', {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Muted:', data.data.muted);
|
||||
}
|
||||
|
||||
// Send message
|
||||
async function sendMessage(roomId, message) {
|
||||
const response = await fetch('http://127.0.0.1:33384/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomId, message })
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Message sent:', data.data.eventId);
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Toggle mute
|
||||
def toggle_mute():
|
||||
response = requests.post('http://127.0.0.1:33384/mute/toggle')
|
||||
data = response.json()
|
||||
print('Muted:', data['data']['muted'])
|
||||
|
||||
# Send message
|
||||
def send_message(room_id, message):
|
||||
response = requests.post('http://127.0.0.1:33384/message', json={
|
||||
'roomId': room_id,
|
||||
'message': message
|
||||
})
|
||||
data = response.json()
|
||||
print('Message sent:', data['data']['eventId'])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stream Deck Integration
|
||||
|
||||
To integrate with Stream Deck:
|
||||
|
||||
1. Install the "API Ninja" or "HTTP Request" plugin for Stream Deck
|
||||
2. Create a button with the following configuration:
|
||||
- **URL**: `http://127.0.0.1:33384/mute/toggle`
|
||||
- **Method**: POST
|
||||
- **Headers**: `Content-Type: application/json`
|
||||
|
||||
Example configurations:
|
||||
|
||||
**Mute Toggle Button**:
|
||||
- URL: `http://127.0.0.1:33384/mute/toggle`
|
||||
- Method: POST
|
||||
|
||||
**Deafen Toggle Button**:
|
||||
- URL: `http://127.0.0.1:33384/deafen/toggle`
|
||||
- Method: POST
|
||||
|
||||
**Send Quick Message**:
|
||||
- URL: `http://127.0.0.1:33384/message/current`
|
||||
- Method: POST
|
||||
- Body: `{"message": "BRB!"}`
|
||||
|
||||
---
|
||||
|
||||
## Client-Side Implementation
|
||||
|
||||
To enable the API functionality in the Paarrot app, you need to implement the action handlers in your Matrix client code. The API server sends actions via IPC to the renderer process, which should handle them and respond.
|
||||
|
||||
### Example Handler (TypeScript/React)
|
||||
|
||||
Add this to your app initialization:
|
||||
|
||||
```typescript
|
||||
// Listen for API actions from the Electron API server
|
||||
if (window.electron?.api?.onAction) {
|
||||
window.electron.api.onAction(async (action: {
|
||||
action: string;
|
||||
params: any;
|
||||
responseChannel: string;
|
||||
}) => {
|
||||
try {
|
||||
let result;
|
||||
|
||||
switch (action.action) {
|
||||
case 'toggle-mute':
|
||||
// Toggle microphone mute
|
||||
result = await toggleMicrophone();
|
||||
break;
|
||||
|
||||
case 'set-mute':
|
||||
// Set microphone mute state
|
||||
result = await setMicrophone(action.params.muted);
|
||||
break;
|
||||
|
||||
case 'toggle-deafen':
|
||||
// Toggle deafen (mute speakers)
|
||||
result = await toggleDeafen();
|
||||
break;
|
||||
|
||||
case 'set-deafen':
|
||||
// Set deafen state
|
||||
result = await setDeafen(action.params.deafened);
|
||||
break;
|
||||
|
||||
case 'change-channel':
|
||||
// Navigate to a different room
|
||||
result = await changeRoom(action.params.roomId);
|
||||
break;
|
||||
|
||||
case 'get-channels':
|
||||
// Get list of rooms
|
||||
result = await getRoomList();
|
||||
break;
|
||||
|
||||
case 'send-message':
|
||||
// Send message to specific room
|
||||
result = await sendMessage(action.params.roomId, action.params.message);
|
||||
break;
|
||||
|
||||
case 'send-message-current':
|
||||
// Send message to current room
|
||||
result = await sendMessageToCurrent(action.params.message);
|
||||
break;
|
||||
|
||||
case 'get-current-room':
|
||||
// Get current room info
|
||||
result = await getCurrentRoomInfo();
|
||||
break;
|
||||
|
||||
case 'get-status':
|
||||
// Get current app status
|
||||
result = await getAppStatus();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action.action}`);
|
||||
}
|
||||
|
||||
// Send success response back
|
||||
window.electron.api.sendResponse(action.responseChannel, {
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
// Send error response back
|
||||
window.electron.api.sendResponse(action.responseChannel, {
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
The API server can be configured by modifying `electron/api-server.js`:
|
||||
|
||||
- **Default Port**: 33384 (can be changed in the constructor)
|
||||
- **Bind Address**: 127.0.0.1 (localhost only for security)
|
||||
- **Timeout**: 10 seconds for actions
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. The API server only listens on localhost (127.0.0.1) and is not accessible from the network
|
||||
2. No authentication is implemented - anyone with local access can control the app
|
||||
3. Consider adding API key authentication if needed for your use case
|
||||
4. CORS is enabled for all origins since it's localhost only
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**API server not starting**:
|
||||
- Check if port 33384 is already in use
|
||||
- Check console logs for errors
|
||||
- Verify Express is installed: `npm install express cors body-parser`
|
||||
|
||||
**Actions not working**:
|
||||
- Ensure the client-side handler is implemented
|
||||
- Check browser console for errors
|
||||
- Verify the action names match between API server and client
|
||||
|
||||
**Timeout errors**:
|
||||
- Actions must respond within 10 seconds
|
||||
- Check if the Matrix client is initialized
|
||||
- Verify the action handler is registered
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential features to add:
|
||||
- WebSocket support for real-time events
|
||||
- Authentication with API keys
|
||||
- Voice channel controls (join/leave)
|
||||
- User status updates
|
||||
- Notification controls
|
||||
- Screen sharing controls
|
||||
51
README.md
@@ -1,4 +1,4 @@
|
||||
# Paarrot
|
||||
# Paarrot
|
||||
|
||||
Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny.
|
||||
|
||||
@@ -22,10 +22,57 @@ To setup development locally run the following commands:
|
||||
* `cd cinny-desktop/cinny`
|
||||
* `npm ci`
|
||||
* `cd ..`
|
||||
* `npm ci`
|
||||
* `npm ci`
|
||||
|
||||
To build the app locally, run:
|
||||
* `npm run build`
|
||||
|
||||
To start local dev server, run:
|
||||
* `npm run dev`
|
||||
|
||||
## Plugin System
|
||||
|
||||
Paarrot includes a plugin system for extending and customising the app with JavaScript modules.
|
||||
|
||||
### Installing Plugins
|
||||
|
||||
1. Drop a plugin folder into your plugins directory:
|
||||
- **Windows**: `%APPDATA%\paarrot\plugins\<plugin-name>\`
|
||||
- **Linux**: `~/.config/Paarrot/plugins/<plugin-name>/`
|
||||
- **macOS**: `~/Library/Application Support/Paarrot/plugins/<plugin-name>/`
|
||||
2. Enable it in **Settings → Plugins → Installed**
|
||||
|
||||
### What Plugins Can Do
|
||||
|
||||
- Register custom slash commands
|
||||
- Intercept and modify messages
|
||||
- Inject buttons into 11 UI locations (nav lists, toolbars, headers, sidebar, menus)
|
||||
- Register custom themes
|
||||
- Hook into raw Matrix events
|
||||
- Run background tasks
|
||||
- Show system notifications
|
||||
|
||||
### Plugin Locations (UI Buttons)
|
||||
|
||||
Plugins can inject buttons as **nav list rows** or **icon buttons** across the app:
|
||||
|
||||
| Location | Style | Where |
|
||||
|---|---|---|
|
||||
| `channel-list` | Nav row | Space channel list |
|
||||
| `home-section` | Nav row | Home panel, above room list |
|
||||
| `direct-messages` | Nav row | DMs panel, below "Create Chat" |
|
||||
| `sidebar-actions` | Icon | Left sidebar — above Explore and above Search |
|
||||
| `text-composer-toolbar` | Icon | Message composer toolbar |
|
||||
| `composer-actions` | Icon | Beside the `+` attach button |
|
||||
| `room-header` | Icon | Room header bar |
|
||||
| `room-menu` | Icon | Room ⋮ dropdown |
|
||||
| `message-actions` | Icon | Message hover bar |
|
||||
| `user-menu` | Icon | Right-click on user avatar |
|
||||
| `search-notification-section` | Icon | Notifications page header |
|
||||
|
||||
### Documentation
|
||||
|
||||
- [Plugin System Overview](PLUGINS.md)
|
||||
- [Full Plugin API Reference](docs/PLUGIN_API.md)
|
||||
- [Button Registration API](docs/PLUGIN_BUTTON_API.md)
|
||||
- [Example Plugins](plugins/)
|
||||
|
||||
10
build-and-publish.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set GitHub token for publishing
|
||||
export GH_TOKEN="ghtoken"
|
||||
|
||||
# Run the build and publish
|
||||
npm run build
|
||||
|
||||
# Exit with the build's exit code
|
||||
exit $?
|
||||
2
cinny
19
config.json
@@ -9,30 +9,13 @@
|
||||
"xmr.se"
|
||||
],
|
||||
"allowCustomHomeservers": true,
|
||||
|
||||
"calling": {
|
||||
"livekitServiceUrl": "https://b.ruv.wtf/matrix-rtc/livekit/jwt"
|
||||
},
|
||||
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
"spaces": [
|
||||
"#cinny-space:matrix.org",
|
||||
"#community:matrix.org",
|
||||
"#space:envs.net",
|
||||
"#science-space:matrix.org",
|
||||
"#libregaming-games:tchncs.de",
|
||||
"#mathematics-on:matrix.org"
|
||||
],
|
||||
"rooms": [
|
||||
"#cinny:matrix.org",
|
||||
"#freesoftware:matrix.org",
|
||||
"#pcapdroid:matrix.org",
|
||||
"#gentoo:matrix.org",
|
||||
"#PrivSec.dev:arcticfoxes.net",
|
||||
"#disroot:aria-net.org"
|
||||
],
|
||||
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
||||
"servers": []
|
||||
},
|
||||
|
||||
"hashRouter": {
|
||||
|
||||
4
dev-app-update.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
provider: github
|
||||
owner: Paarrot
|
||||
repo: Paarrot-Desktop
|
||||
updaterCacheDirName: paarrot-updater
|
||||
BIN
dist-electron/win-unpacked/resources/app.asar
Normal file
@@ -10,6 +10,7 @@
|
||||
"files": [
|
||||
"electron/**/*",
|
||||
"cinny/dist/**/*",
|
||||
"dev-app-update.yml",
|
||||
"!**/*.map",
|
||||
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
|
||||
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
|
||||
@@ -28,6 +29,17 @@
|
||||
"from": "icons",
|
||||
"to": "icons",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "cinny/public/sound",
|
||||
"to": "sound",
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"protocols": [
|
||||
{
|
||||
"name": "Paarrot URL",
|
||||
"schemes": ["paarrot"]
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
@@ -45,6 +57,7 @@
|
||||
"Name": "Paarrot",
|
||||
"GenericName": "Matrix Client",
|
||||
"Comment": "A Matrix client built with Cinny",
|
||||
"MimeType": "x-scheme-handler/paarrot;",
|
||||
"Categories": "Network;InstantMessaging;",
|
||||
"Keywords": "matrix;chat;messaging;",
|
||||
"StartupWMClass": "paarrot"
|
||||
@@ -72,23 +85,30 @@
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "squirrel",
|
||||
"target": "nsis",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"icon": "icons/icon.ico",
|
||||
"publisherName": "Mates.Media",
|
||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
|
||||
},
|
||||
"squirrelWindows": {
|
||||
"name": "Paarrot",
|
||||
"loadingGif": "icons/icon.png",
|
||||
"iconUrl": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/raw/branch/main/icons/icon.ico",
|
||||
"remoteReleases": false,
|
||||
"useAppIdAsId": true
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": false,
|
||||
"deleteAppDataOnUninstall": false,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "Paarrot",
|
||||
"differentialPackage": false
|
||||
},
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases/download/v${version}"
|
||||
}
|
||||
"compression": "normal",
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "Paarrot",
|
||||
"repo": "Paarrot-Desktop",
|
||||
"releaseType": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1402
electron/main.js
@@ -18,7 +18,8 @@ const commandMap = {
|
||||
'check_for_updates': 'check-for-updates',
|
||||
'download_update': 'download-update',
|
||||
'install_update': 'install-update',
|
||||
'get_desktop_sources': 'get-desktop-sources'
|
||||
'get_desktop_sources': 'get-desktop-sources',
|
||||
'get_protocol_status': 'protocol:get-status'
|
||||
};
|
||||
|
||||
// Shared invoke function for legacy API compatibility
|
||||
@@ -93,7 +94,9 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
unmaximize: () => ipcRenderer.invoke('window:unmaximize'),
|
||||
close: () => ipcRenderer.invoke('window:close'),
|
||||
isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
|
||||
startDrag: () => ipcRenderer.invoke('window:start-drag')
|
||||
startDrag: () => ipcRenderer.invoke('window:start-drag'),
|
||||
focus: () => ipcRenderer.invoke('window:focus'),
|
||||
flashFrame: (flash = true) => ipcRenderer.invoke('window:flash-frame', flash)
|
||||
},
|
||||
|
||||
// Clipboard
|
||||
@@ -101,8 +104,12 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
readImage: () => ipcRenderer.invoke('read-clipboard-image'),
|
||||
writeText: (text) => ipcRenderer.invoke('write-clipboard-text', text)
|
||||
},
|
||||
|
||||
// External URLs
|
||||
// Audio
|
||||
audio: {
|
||||
playNotificationSound: (soundType = 'message') => ipcRenderer.invoke('play-notification-sound', soundType),
|
||||
getSoundBaseUrl: () => ipcRenderer.invoke('get-sound-base-url'),
|
||||
},
|
||||
// External URLs
|
||||
shell: {
|
||||
openExternal: (url) => ipcRenderer.invoke('open-external-url', url)
|
||||
},
|
||||
@@ -122,8 +129,8 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
// Auto-updater
|
||||
updater: {
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||
downloadAndInstallUpdate: () => ipcRenderer.invoke('download-and-install-update'),
|
||||
isMock: () => ipcRenderer.invoke('updater-is-mock'),
|
||||
onUpdateAvailable: (callback) => {
|
||||
ipcRenderer.on('update-available', (_, info) => callback(info));
|
||||
},
|
||||
@@ -132,7 +139,12 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
},
|
||||
onUpdateDownloaded: (callback) => {
|
||||
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
||||
}
|
||||
},
|
||||
onUpdateNotAvailable: (callback) => {
|
||||
ipcRenderer.on('update-not-available', (_, info) => callback(info));
|
||||
},
|
||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||
},
|
||||
|
||||
// Desktop capturer (for screen sharing)
|
||||
@@ -150,6 +162,31 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
sendResponse: (channel, result) => {
|
||||
ipcRenderer.invoke(channel, result);
|
||||
}
|
||||
},
|
||||
|
||||
// Desktop notifications
|
||||
notification: {
|
||||
show: (options) => ipcRenderer.invoke('show-notification', options),
|
||||
onNavigate: (callback) => {
|
||||
const handler = (_, data) => callback(data);
|
||||
ipcRenderer.on('notification:navigate', handler);
|
||||
return () => ipcRenderer.removeListener('notification:navigate', handler);
|
||||
}
|
||||
},
|
||||
|
||||
// Plugin management
|
||||
plugins: {
|
||||
getPath: () => ipcRenderer.invoke('plugin:get-path'),
|
||||
download: (pluginId, downloadUrl, name) => ipcRenderer.invoke('plugin:download', { pluginId, downloadUrl, name }),
|
||||
list: () => ipcRenderer.invoke('plugin:list'),
|
||||
uninstall: (pluginId) => ipcRenderer.invoke('plugin:uninstall', pluginId),
|
||||
readPluginCode: (pluginId) => ipcRenderer.invoke('plugin:read-code', pluginId)
|
||||
},
|
||||
|
||||
// Desktop protocol diagnostics
|
||||
protocol: {
|
||||
getStatus: () => ipcRenderer.invoke('protocol:get-status'),
|
||||
repair: () => ipcRenderer.invoke('protocol:repair')
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
161
example-plugin/index.js
Normal file
@@ -0,0 +1,161 @@
|
||||
// Example plugin for Paarrot - Demonstrates all plugin API features
|
||||
// This plugin showcases commands, message interceptors, settings, logging, and more
|
||||
|
||||
module.exports = {
|
||||
name: "Example Plugin",
|
||||
version: "2.0.0",
|
||||
|
||||
/**
|
||||
* Called when the plugin is loaded
|
||||
* @param {PluginContext} ctx - The plugin context with access to all APIs
|
||||
*/
|
||||
onLoad: async (ctx) => {
|
||||
ctx.log('🎉 Example plugin is loading...');
|
||||
|
||||
try {
|
||||
// 1. Register commands with args
|
||||
ctx.commands.register({
|
||||
name: "shrug",
|
||||
description: "Send a shrug emoji",
|
||||
run: () => {
|
||||
ctx.log('Shrug command executed');
|
||||
return "¯\\_(ツ)_/¯";
|
||||
}
|
||||
});
|
||||
|
||||
ctx.commands.register({
|
||||
name: "echo",
|
||||
args: ["text"],
|
||||
description: "Echo back your text",
|
||||
run: ({ text }) => {
|
||||
ctx.log('Echo command:', text);
|
||||
return text || "Nothing to echo!";
|
||||
}
|
||||
});
|
||||
|
||||
ctx.commands.register({
|
||||
name: "wave",
|
||||
description: "Wave at someone",
|
||||
args: ["name"],
|
||||
run: ({ name }) => {
|
||||
return `👋 Hey ${name || 'everyone'}!`;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Message interceptors
|
||||
ctx.messages.onBeforeSend((msg) => {
|
||||
// Auto-expand abbreviations
|
||||
if (msg.content === "brb") {
|
||||
msg.content = "be right back!";
|
||||
ctx.log('Expanded brb to full phrase');
|
||||
}
|
||||
if (msg.content === "omw") {
|
||||
msg.content = "on my way!";
|
||||
}
|
||||
});
|
||||
|
||||
ctx.messages.onReceive((msg) => {
|
||||
// Log all received messages (for debugging)
|
||||
ctx.log(`Received message in ${msg.roomId}:`, msg.content.substring(0, 50));
|
||||
});
|
||||
|
||||
// 3. Define settings
|
||||
ctx.settings.define({
|
||||
autoShrug: {
|
||||
type: "boolean",
|
||||
label: "Auto Shrug",
|
||||
description: "Automatically append ¯\\_(ツ)_/¯ to messages ending with '?'",
|
||||
default: false
|
||||
},
|
||||
theme: {
|
||||
type: "select",
|
||||
label: "Plugin Theme",
|
||||
description: "Choose your preferred theme",
|
||||
options: [
|
||||
{ value: "dark", label: "Dark" },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "purple", label: "Purple" }
|
||||
],
|
||||
default: "dark"
|
||||
},
|
||||
customPrefix: {
|
||||
type: "string",
|
||||
label: "Custom Prefix",
|
||||
description: "Prefix for plugin commands",
|
||||
default: "[Plugin]"
|
||||
}
|
||||
});
|
||||
|
||||
// Use settings
|
||||
const autoShrug = ctx.settings.get('autoShrug');
|
||||
if (autoShrug) {
|
||||
ctx.log('Auto shrug is enabled!');
|
||||
ctx.messages.onBeforeSend((msg) => {
|
||||
if (msg.content.endsWith('?')) {
|
||||
msg.content += ' ¯\\_(ツ)_/¯';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Listen to raw Matrix events
|
||||
ctx.matrix.on('Room.timeline', (event) => {
|
||||
if (event.getType() === 'm.room.message') {
|
||||
const content = event.getContent();
|
||||
if (content.body?.includes('hello')) {
|
||||
ctx.log('Someone said hello!');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Background tasks
|
||||
ctx.timers.setInterval(() => {
|
||||
ctx.log('Background task running... (every 30s)');
|
||||
// You could check for notifications, clean up data, etc.
|
||||
}, 30000);
|
||||
|
||||
// 6. Notifications
|
||||
ctx.notify({
|
||||
title: "Example Plugin",
|
||||
body: "Plugin loaded successfully!",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
// 7. Custom UI renderer (conceptual - would need UI integration)
|
||||
ctx.ui.registerRenderer("custom-message", (msg) => {
|
||||
ctx.log('Rendering custom message:', msg);
|
||||
// Return custom React component
|
||||
return null;
|
||||
});
|
||||
|
||||
// 8. Export API for other plugins
|
||||
this.exports = {
|
||||
greet: (name) => {
|
||||
return `Hello, ${name}!`;
|
||||
},
|
||||
version: "2.0.0"
|
||||
};
|
||||
|
||||
ctx.log('✅ Example plugin loaded successfully!');
|
||||
ctx.log('Registered commands: /shrug, /echo, /wave');
|
||||
ctx.log('Settings:', {
|
||||
autoShrug: ctx.settings.get('autoShrug'),
|
||||
theme: ctx.settings.get('theme'),
|
||||
prefix: ctx.settings.get('customPrefix')
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
ctx.error('Failed to load example plugin:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the plugin is unloaded or disabled
|
||||
*/
|
||||
onUnload: async () => {
|
||||
console.log('[ExamplePlugin] Plugin is unloading...');
|
||||
console.log('[ExamplePlugin] Cleaned up successfully!');
|
||||
},
|
||||
|
||||
// Exports for other plugins to use via ctx.require()
|
||||
exports: null // Will be set during onLoad
|
||||
};
|
||||
9
example-plugin/plugin-metadata.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Example Plugin",
|
||||
"version": "2.0.0",
|
||||
"description": "A comprehensive example plugin demonstrating all plugin API features including commands, interceptors, settings, and more",
|
||||
"author": "Paarrot Team",
|
||||
"homepage": "https://github.com/Paarrot/cinny-desktop",
|
||||
"thumbnail": "https://raw.githubusercontent.com/Paarrot/cinny-desktop/main/icons/icon.png",
|
||||
"tags": ["example", "demo", "utility", "commands", "interceptors"]
|
||||
}
|
||||
3944
package-lock.json
generated
48
package.json
@@ -1,23 +1,22 @@
|
||||
{
|
||||
"name": "paarrot",
|
||||
<<<<<<< HEAD
|
||||
"version": "4.11.24",
|
||||
"version": "4.11.163",
|
||||
"description": "Paarrot - A Matrix client based on Cinny",
|
||||
"homepage": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop",
|
||||
=======
|
||||
"version": "4.11.24",
|
||||
"description": "Yet another matrix client",
|
||||
"homepage": "https://github.com/ajbura/cinny",
|
||||
>>>>>>> bd544193be537882c7c69a153c38cc211293c741
|
||||
"homepage": "https://github.com/Paarrot/Paarrot-Desktop",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Paarrot/Paarrot-Desktop.git"
|
||||
},
|
||||
"main": "electron/main.js",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"",
|
||||
"dev:vite": "cd cinny && npm start",
|
||||
"dev:electron": "wait-on http://localhost:8080 && NODE_ENV=development electron .",
|
||||
"build": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish never",
|
||||
"dev:vite": "cross-env BROWSER=none sh -c 'cd cinny && npm start'",
|
||||
"dev:electron": "wait-on http://localhost:38347 && cross-env NODE_ENV=development electron .",
|
||||
"build": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish always",
|
||||
"build:local": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder",
|
||||
"build:linux": "npm run build -- --linux",
|
||||
"build:win": "npm run build -- --win",
|
||||
"postman:generate": "node scripts/generate-postman-collection.js"
|
||||
@@ -29,23 +28,28 @@
|
||||
},
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"body-parser": "2.2.2",
|
||||
"adm-zip": "^0.5.18",
|
||||
"body-parser": "2.3.0",
|
||||
"cors": "2.8.6",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-store": "^11.0.2",
|
||||
"electron-updater": "^6.8.9",
|
||||
"express": "5.2.1",
|
||||
"open": "11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "6.0.0",
|
||||
"@electron/rebuild": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"electron": "40.6.0",
|
||||
"electron-builder": "26.8.1",
|
||||
"@actions/github": "9.1.1",
|
||||
"@electron/rebuild": "4.2.0",
|
||||
"concurrently": "10.0.3",
|
||||
"cross-env": "10.1.0",
|
||||
"electron": "43.1.0",
|
||||
"electron-builder": "26.15.3",
|
||||
"node-fetch": "3.3.2",
|
||||
"png2icons": "2.0.1",
|
||||
"sharp": "0.34.5",
|
||||
"wait-on": "9.0.4"
|
||||
"sharp": "0.35.3",
|
||||
"wait-on": "9.0.10"
|
||||
},
|
||||
"allowScripts": {
|
||||
"electron-winstaller@5.4.0": true,
|
||||
"electron@43.1.0": true
|
||||
}
|
||||
}
|
||||
|
||||
104
scripts/migrate-icons.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const srcRoot = path.resolve(__dirname, '../cinny/src');
|
||||
const iconsModule = path.resolve(srcRoot, 'app/components/icons');
|
||||
|
||||
const ICON_SYMBOLS = new Set(['Icon', 'Icons', 'IconName', 'IconSrc']);
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath, files);
|
||||
} else if (/\.(tsx?)$/.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function getIconsImportPath(filePath) {
|
||||
const rel = path.relative(path.dirname(filePath), iconsModule).replace(/\\/g, '/');
|
||||
return rel.startsWith('.') ? rel : `./${rel}`;
|
||||
}
|
||||
|
||||
function splitFoldsImport(importClause) {
|
||||
const match = importClause.match(/^\{([^}]+)\}(?:\s+as\s+\w+)?$/);
|
||||
if (!match) return null;
|
||||
|
||||
const specifiers = match[1]
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
const [local, imported] = part.split(/\s+as\s+/).map((s) => s.trim());
|
||||
return { local: local ?? imported, imported: imported ?? local };
|
||||
});
|
||||
|
||||
const iconSpecs = specifiers.filter((s) => ICON_SYMBOLS.has(s.imported));
|
||||
const foldsSpecs = specifiers.filter((s) => !ICON_SYMBOLS.has(s.imported));
|
||||
|
||||
if (iconSpecs.length === 0) return null;
|
||||
|
||||
return { iconSpecs, foldsSpecs };
|
||||
}
|
||||
|
||||
function migrateFile(filePath) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
const importRegex = /import\s+(\{[^}]+\})\s+from\s+['"]folds['"];?/g;
|
||||
let changed = false;
|
||||
const iconImports = new Map();
|
||||
|
||||
content = content.replace(importRegex, (fullMatch, importClause) => {
|
||||
const split = splitFoldsImport(importClause);
|
||||
if (!split) return fullMatch;
|
||||
|
||||
changed = true;
|
||||
for (const spec of split.iconSpecs) {
|
||||
iconImports.set(spec.local, spec.imported);
|
||||
}
|
||||
|
||||
if (split.foldsSpecs.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const foldsImport = split.foldsSpecs.map((s) => (s.local === s.imported ? s.local : `${s.imported} as ${s.local}`)).join(', ');
|
||||
return `import { ${foldsImport} } from 'folds';`;
|
||||
});
|
||||
|
||||
if (!changed) return false;
|
||||
|
||||
const iconsPath = getIconsImportPath(filePath);
|
||||
const iconExportList = [...iconImports.entries()]
|
||||
.map(([local, imported]) => (local === imported ? imported : `${imported} as ${local}`))
|
||||
.join(', ');
|
||||
|
||||
const iconsImportLine = `import { ${iconExportList} } from '${iconsPath}';`;
|
||||
|
||||
const foldsImportMatch = content.match(/^import\s+\{[^}]+\}\s+from\s+['"]folds['"];?\s*$/m);
|
||||
if (foldsImportMatch) {
|
||||
const insertAt = content.indexOf(foldsImportMatch[0]) + foldsImportMatch[0].length;
|
||||
content = `${content.slice(0, insertAt)}\n${iconsImportLine}${content.slice(insertAt)}`;
|
||||
} else {
|
||||
content = `${iconsImportLine}\n${content}`;
|
||||
}
|
||||
|
||||
content = content.replace(/\n{3,}/g, '\n\n');
|
||||
fs.writeFileSync(filePath, content);
|
||||
return true;
|
||||
}
|
||||
|
||||
const files = walk(srcRoot);
|
||||
let migrated = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.includes(`${path.sep}components${path.sep}icons${path.sep}`)) continue;
|
||||
if (migrateFile(file)) {
|
||||
migrated += 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Migrated ${migrated} files.`);
|
||||
26
streamdeck-plugin/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Distribution files
|
||||
dist/
|
||||
*.streamDeckPlugin
|
||||
|
||||
# Node modules (if any dependencies are added)
|
||||
node_modules/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
207
streamdeck-plugin/QUICKSTART.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Quick Start Guide - Paarrot Stream Deck Plugin
|
||||
|
||||
## For Users
|
||||
|
||||
### Installation (3 simple steps)
|
||||
|
||||
1. **Download the plugin**
|
||||
- Get `com.paarrot.streamdeck-v1.0.0.streamDeckPlugin` from releases
|
||||
|
||||
2. **Install**
|
||||
- Double-click the `.streamDeckPlugin` file
|
||||
- Stream Deck software will open and install it automatically
|
||||
|
||||
3. **Add to your Stream Deck**
|
||||
- Open Stream Deck software
|
||||
- Find "Paarrot" in the actions list (under "Voice & Chat")
|
||||
- Drag actions to your buttons
|
||||
|
||||
### Quick Action Guide
|
||||
|
||||
| Action | What it does | Setup needed? |
|
||||
|--------|--------------|---------------|
|
||||
| Toggle Mute | Mute/unmute mic | No |
|
||||
| Toggle Deafen | Mute/unmute speakers | No |
|
||||
| Change Channel | Switch rooms | Yes - pick room |
|
||||
| Send Message | Send preset message | Yes - type message |
|
||||
| Get Status | Show mute/deafen state | No |
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
**Change Channel**:
|
||||
1. Drag to button
|
||||
2. Click button in Stream Deck to open settings
|
||||
3. Select channel from dropdown
|
||||
4. Done!
|
||||
|
||||
**Send Message**:
|
||||
1. Drag to button
|
||||
2. Click button in Stream Deck to open settings
|
||||
3. Type message (e.g., "BRB!")
|
||||
4. Done!
|
||||
|
||||
---
|
||||
|
||||
## For Developers
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (for build scripts)
|
||||
- Stream Deck software
|
||||
- Text editor
|
||||
|
||||
### Development Setup
|
||||
|
||||
```powershell
|
||||
# Clone or download the repo
|
||||
cd streamdeck-plugin
|
||||
|
||||
# Validate plugin structure
|
||||
node validate.js
|
||||
|
||||
# Install to Stream Deck (Windows)
|
||||
Copy-Item -Recurse -Force com.paarrot.streamdeck.sdPlugin "$env:APPDATA\Elgato\StreamDeck\Plugins\"
|
||||
|
||||
# Install to Stream Deck (macOS)
|
||||
cp -r com.paarrot.streamdeck.sdPlugin ~/Library/Application\ Support/com.elgato.StreamDeck/Plugins/
|
||||
|
||||
# Restart Stream Deck software
|
||||
```
|
||||
|
||||
### Making Changes
|
||||
|
||||
1. Edit files in `com.paarrot.streamdeck.sdPlugin/`
|
||||
2. Run validation: `node validate.js`
|
||||
3. Restart Stream Deck software
|
||||
4. Test your changes
|
||||
|
||||
### Building for Distribution
|
||||
|
||||
```powershell
|
||||
# Validate first
|
||||
node validate.js
|
||||
|
||||
# Build .streamDeckPlugin file
|
||||
node build.js
|
||||
|
||||
# Output will be in dist/ folder
|
||||
```
|
||||
|
||||
### File Guide
|
||||
|
||||
```
|
||||
com.paarrot.streamdeck.sdPlugin/
|
||||
├── manifest.json # Edit: plugin info, actions
|
||||
├── plugin/
|
||||
│ └── index.js # Edit: main logic, API calls
|
||||
├── propertyinspector/
|
||||
│ ├── change-channel.html # Edit: channel picker UI
|
||||
│ └── send-message.html # Edit: message input UI
|
||||
└── images/ # Edit: replace SVGs with your icons
|
||||
└── *.svg
|
||||
```
|
||||
|
||||
### Adding a New Action
|
||||
|
||||
1. **Add to manifest.json**:
|
||||
```json
|
||||
{
|
||||
"Icon": "images/my-action",
|
||||
"Name": "My Action",
|
||||
"States": [{ "Image": "images/my-action" }],
|
||||
"Tooltip": "Does something cool",
|
||||
"UUID": "com.paarrot.streamdeck.myaction"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add icon**: `images/my-action.svg`
|
||||
|
||||
3. **Add logic in plugin/index.js**:
|
||||
```javascript
|
||||
const ACTIONS = {
|
||||
MY_ACTION: 'com.paarrot.streamdeck.myaction'
|
||||
};
|
||||
|
||||
// In handleKeyDown:
|
||||
case ACTIONS.MY_ACTION:
|
||||
// Your code here
|
||||
break;
|
||||
```
|
||||
|
||||
4. **Test**: Restart Stream Deck software
|
||||
|
||||
### Testing
|
||||
|
||||
1. Enable Stream Deck console: `View > Open Console`
|
||||
2. Watch for errors when pressing buttons
|
||||
3. Test all actions with Paarrot running
|
||||
4. Test with Paarrot not running (should fail gracefully)
|
||||
|
||||
### API Reference
|
||||
|
||||
The plugin calls these Paarrot API endpoints:
|
||||
|
||||
```javascript
|
||||
// Status polling (every 1 second)
|
||||
GET http://127.0.0.1:33384/status
|
||||
|
||||
// Toggle actions
|
||||
POST http://127.0.0.1:33384/mute/toggle
|
||||
POST http://127.0.0.1:33384/deafen/toggle
|
||||
|
||||
// Configured actions
|
||||
POST http://127.0.0.1:33384/channel
|
||||
Body: { "roomId": "!abc:server.com" }
|
||||
|
||||
POST http://127.0.0.1:33384/message/current
|
||||
Body: { "message": "Hello!" }
|
||||
|
||||
// Property inspector
|
||||
GET http://127.0.0.1:33384/channels
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Plugin not loading**:
|
||||
- Check manifest.json syntax (use validator)
|
||||
- Ensure folder name is exactly `com.paarrot.streamdeck.sdPlugin`
|
||||
- Restart Stream Deck software
|
||||
|
||||
**Actions not working**:
|
||||
- Open console (View > Open Console)
|
||||
- Check for JavaScript errors
|
||||
- Verify Paarrot API is running
|
||||
|
||||
**Icons not showing**:
|
||||
- Ensure files exist at paths specified in manifest
|
||||
- Use .svg or .png format
|
||||
- Check file paths are relative to plugin root
|
||||
|
||||
### Resources
|
||||
|
||||
- [Stream Deck SDK Documentation](https://docs.elgato.com/sdk)
|
||||
- [Paarrot API Documentation](../API.md)
|
||||
- [Example Plugins](https://github.com/elgatosf/streamdeck-plugins)
|
||||
|
||||
### Publishing Checklist
|
||||
|
||||
- [ ] Update version in manifest.json
|
||||
- [ ] Run `node validate.js` (no errors)
|
||||
- [ ] Test all actions
|
||||
- [ ] Test with Paarrot off (graceful failure)
|
||||
- [ ] Update README.md version history
|
||||
- [ ] Run `node build.js`
|
||||
- [ ] Test .streamDeckPlugin file installs correctly
|
||||
- [ ] Create GitHub release with .streamDeckPlugin file
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
**Users**: See README.md troubleshooting section
|
||||
|
||||
**Developers**: Open an issue on GitHub with:
|
||||
- What you're trying to do
|
||||
- What's happening
|
||||
- Console errors
|
||||
- Plugin version
|
||||
263
streamdeck-plugin/README.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Paarrot Stream Deck Plugin
|
||||
|
||||
Control your Paarrot desktop app directly from your Elgato Stream Deck! Toggle mute, deafen, switch channels, and send quick messages with the press of a button.
|
||||
|
||||
## Features
|
||||
|
||||
- **Toggle Mute** - Mute/unmute your microphone with visual feedback
|
||||
- **Toggle Deafen** - Deafen/undeafen (mute speakers) with visual feedback
|
||||
- **Change Channel** - Switch between rooms/channels instantly
|
||||
- **Send Message** - Send predefined messages to the current room
|
||||
- **Get Status** - Display current mute/deafen status
|
||||
|
||||
## Requirements
|
||||
|
||||
- Elgato Stream Deck (any model)
|
||||
- Stream Deck software version 6.0 or later
|
||||
- Paarrot desktop app running with API server enabled
|
||||
- Windows 10+ or macOS 10.14+
|
||||
|
||||
## Installation
|
||||
|
||||
### Method 1: Install from Release (easiest)
|
||||
|
||||
1. Download the latest `.streamDeckPlugin` file from the [Releases](https://github.com/yourusername/paarrot-streamdeck/releases) page
|
||||
2. Double-click the file to install
|
||||
3. Stream Deck software will open and install the plugin automatically
|
||||
|
||||
### Method 2: Manual Installation
|
||||
|
||||
1. Download or clone this repository
|
||||
2. Copy the `com.paarrot.streamdeck.sdPlugin` folder to:
|
||||
- **Windows**: `%APPDATA%\Elgato\StreamDeck\Plugins\`
|
||||
- **macOS**: `~/Library/Application Support/com.elgato.StreamDeck/Plugins/`
|
||||
3. Restart the Stream Deck software
|
||||
|
||||
### Method 3: Developer Mode
|
||||
|
||||
1. Open Stream Deck software
|
||||
2. Open the Plugins view
|
||||
3. Enable "Developer Mode" in settings
|
||||
4. Drag the `com.paarrot.streamdeck.sdPlugin` folder into the Stream Deck window
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Ensure Paarrot API is Running
|
||||
|
||||
Make sure your Paarrot desktop app is running with the API server enabled on port 33384. You can test this by visiting:
|
||||
|
||||
```
|
||||
http://127.0.0.1:33384/health
|
||||
```
|
||||
|
||||
You should see:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"app": "Paarrot API",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add Actions to Stream Deck
|
||||
|
||||
1. Open Stream Deck software
|
||||
2. Find "Paarrot" in the actions list
|
||||
3. Drag actions onto your Stream Deck buttons
|
||||
4. Configure actions as needed (see below)
|
||||
|
||||
## Actions
|
||||
|
||||
### Toggle Mute
|
||||
|
||||
**What it does**: Toggles your microphone mute status
|
||||
|
||||
**Configuration**: No configuration needed
|
||||
|
||||
**Visual Feedback**:
|
||||
- Gray microphone icon = unmuted
|
||||
- Red X over microphone = muted
|
||||
|
||||
**Usage**: Press to toggle between muted and unmuted. The button automatically updates to show the current state.
|
||||
|
||||
---
|
||||
|
||||
### Toggle Deafen
|
||||
|
||||
**What it does**: Toggles deafen status (mutes speakers and microphone)
|
||||
|
||||
**Configuration**: No configuration needed
|
||||
|
||||
**Visual Feedback**:
|
||||
- Gray speaker icon = not deafened
|
||||
- Red X over speaker = deafened
|
||||
|
||||
**Usage**: Press to toggle between deafened and not deafened. The button automatically updates to show the current state.
|
||||
|
||||
---
|
||||
|
||||
### Change Channel
|
||||
|
||||
**What it does**: Switches to a different room/channel
|
||||
|
||||
**Configuration**:
|
||||
1. Drag action to a button
|
||||
2. Click the button in Stream Deck to open settings
|
||||
3. Select a channel from the dropdown OR enter a room ID manually
|
||||
4. Click away to save
|
||||
|
||||
**Usage**: Press to instantly switch to the configured channel.
|
||||
|
||||
**Tips**:
|
||||
- Channels are grouped by server in the dropdown
|
||||
- Use manual room ID for rooms not in the list
|
||||
- Room ID format: `!abc123:matrix.org`
|
||||
|
||||
---
|
||||
|
||||
### Send Message
|
||||
|
||||
**What it does**: Sends a predefined message to the currently active room
|
||||
|
||||
**Configuration**:
|
||||
1. Drag action to a button
|
||||
2. Click the button in Stream Deck to open settings
|
||||
3. Type your message in the text area
|
||||
4. Click away to save
|
||||
|
||||
**Usage**: Press to send the message to whichever room is currently active.
|
||||
|
||||
**Example Messages**:
|
||||
- "BRB!" - Be right back
|
||||
- "AFK for 5 minutes"
|
||||
- "On my way!"
|
||||
- "Thanks everyone!"
|
||||
- "GG!" - Good game
|
||||
|
||||
---
|
||||
|
||||
### Get Status
|
||||
|
||||
**What it does**: Displays current mute/deafen status on the button
|
||||
|
||||
**Configuration**: No configuration needed
|
||||
|
||||
**Visual Feedback**:
|
||||
- Shows "OK" when not muted or deafened
|
||||
- Shows "M" when muted
|
||||
- Shows "D" when deafened
|
||||
- Shows "MD" when both muted and deafened
|
||||
|
||||
**Usage**: Button automatically updates every second. Press to force an immediate update.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin not showing up
|
||||
|
||||
- Restart Stream Deck software
|
||||
- Check that the plugin folder is in the correct location
|
||||
- Ensure folder is named exactly `com.paarrot.streamdeck.sdPlugin`
|
||||
|
||||
### Actions not working
|
||||
|
||||
- Verify Paarrot is running
|
||||
- Test API at `http://127.0.0.1:33384/health`
|
||||
- Check console for errors (View > Open Console in Stream Deck)
|
||||
- Ensure port 33384 is not blocked by firewall
|
||||
|
||||
### Button states not updating
|
||||
|
||||
- Check that Paarrot API is responding
|
||||
- Try pressing the action to force an update
|
||||
- Check Stream Deck console for connection errors
|
||||
|
||||
### "No channels available" in dropdown
|
||||
|
||||
- Ensure you're logged into Paarrot
|
||||
- Verify you've joined at least one room
|
||||
- Try entering the room ID manually instead
|
||||
|
||||
### Actions show red X (alert)
|
||||
|
||||
This means the API call failed. Common causes:
|
||||
- Paarrot app not running
|
||||
- API server not started
|
||||
- Network/firewall blocking localhost connections
|
||||
- Invalid room ID or message configuration
|
||||
|
||||
## Development
|
||||
|
||||
### Building from Source
|
||||
|
||||
No build step required! The plugin uses vanilla JavaScript.
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
com.paarrot.streamdeck.sdPlugin/
|
||||
├── manifest.json # Plugin metadata and action definitions
|
||||
├── plugin/
|
||||
│ ├── index.html # Plugin entry point
|
||||
│ └── index.js # Main plugin logic
|
||||
├── propertyinspector/
|
||||
│ ├── change-channel.html # Channel selector UI
|
||||
│ └── send-message.html # Message input UI
|
||||
└── images/ # Action icons
|
||||
├── mute-off.svg
|
||||
├── mute-on.svg
|
||||
├── deafen-off.svg
|
||||
├── deafen-on.svg
|
||||
├── change-channel.svg
|
||||
├── send-message.svg
|
||||
├── status.svg
|
||||
├── toggle-mute.svg
|
||||
├── toggle-deafen.svg
|
||||
├── plugin-icon.svg
|
||||
└── category-icon.svg
|
||||
```
|
||||
|
||||
### Modifying the Plugin
|
||||
|
||||
1. Make changes to the files
|
||||
2. Restart Stream Deck software to reload the plugin
|
||||
3. Test your changes
|
||||
|
||||
### API Endpoints Used
|
||||
|
||||
- `GET /health` - Check API is running
|
||||
- `GET /status` - Get current mute/deafen state
|
||||
- `POST /mute/toggle` - Toggle mute
|
||||
- `POST /deafen/toggle` - Toggle deafen
|
||||
- `POST /channel` - Change channel
|
||||
- `POST /message/current` - Send message
|
||||
- `GET /channels` - Get channel list
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check the troubleshooting section above
|
||||
2. Enable Stream Deck console logging (View > Open Console)
|
||||
3. Check Paarrot's console for API errors
|
||||
4. File an issue on GitHub with logs and details
|
||||
|
||||
## License
|
||||
|
||||
This plugin is provided as-is. See LICENSE file for details.
|
||||
|
||||
## Credits
|
||||
|
||||
Created for Paarrot desktop app.
|
||||
|
||||
Icons designed specifically for this plugin.
|
||||
|
||||
## Version History
|
||||
|
||||
### 1.0.0 (Initial Release)
|
||||
- Toggle Mute action
|
||||
- Toggle Deafen action
|
||||
- Change Channel action with dropdown
|
||||
- Send Message action
|
||||
- Get Status action
|
||||
- Auto-updating button states
|
||||
- Visual feedback for all actions
|
||||
69
streamdeck-plugin/build.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Build script for Paarrot Stream Deck Plugin
|
||||
* Creates a distributable .streamDeckPlugin file
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const PLUGIN_DIR = 'com.paarrot.streamdeck.sdPlugin';
|
||||
const OUTPUT_DIR = 'dist';
|
||||
|
||||
console.log('Building Paarrot Stream Deck Plugin...');
|
||||
|
||||
// Create dist directory if it doesn't exist
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
|
||||
// Check if plugin directory exists
|
||||
if (!fs.existsSync(PLUGIN_DIR)) {
|
||||
console.error(`Error: Plugin directory '${PLUGIN_DIR}' not found!`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read manifest to get version
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(path.join(PLUGIN_DIR, 'manifest.json'), 'utf8')
|
||||
);
|
||||
|
||||
const version = manifest.Version || '1.0.0.0';
|
||||
const uuid = manifest.UUID || 'com.paarrot.streamdeck';
|
||||
const outputPath = path.join(OUTPUT_DIR, `${uuid}-v${version}.streamDeckPlugin`);
|
||||
|
||||
console.log(`Creating plugin archive: ${outputPath}`);
|
||||
|
||||
try {
|
||||
// Clean up old archive
|
||||
if (fs.existsSync(outputPath)) {
|
||||
fs.unlinkSync(outputPath);
|
||||
}
|
||||
if (fs.existsSync(`${outputPath}.zip`)) {
|
||||
fs.unlinkSync(`${outputPath}.zip`);
|
||||
}
|
||||
|
||||
// Use PowerShell's Compress-Archive on Windows
|
||||
// IMPORTANT: Archive must contain the .sdPlugin folder at root level
|
||||
if (process.platform === 'win32') {
|
||||
const command = `powershell -Command "Compress-Archive -Path '${PLUGIN_DIR}' -DestinationPath '${outputPath}.zip' -Force"`;
|
||||
execSync(command);
|
||||
|
||||
// Rename .zip to .streamDeckPlugin
|
||||
if (fs.existsSync(`${outputPath}.zip`)) {
|
||||
fs.renameSync(`${outputPath}.zip`, outputPath);
|
||||
}
|
||||
} else {
|
||||
// Use zip command on macOS/Linux - include the folder itself
|
||||
execSync(`zip -r "${outputPath}" "${PLUGIN_DIR}"`);
|
||||
}
|
||||
|
||||
console.log('✓ Plugin built successfully!');
|
||||
console.log(` Output: ${outputPath}`);
|
||||
console.log(` Version: ${version}`);
|
||||
console.log('\nInstallation:');
|
||||
console.log(` Double-click ${path.basename(outputPath)} to install`);
|
||||
} catch (error) {
|
||||
console.error('Error building plugin:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<rect x="30" y="30" width="40" height="40" rx="8" fill="#666" stroke="none"/>
|
||||
<rect x="74" y="30" width="40" height="40" rx="8" fill="#888" stroke="none"/>
|
||||
<rect x="30" y="74" width="40" height="40" rx="8" fill="#666" stroke="none"/>
|
||||
<rect x="74" y="74" width="40" height="40" rx="8" fill="#666" stroke="none"/>
|
||||
<path d="M50 50 L50 42 L58 50 L50 58 Z" fill="#444" stroke="none"/>
|
||||
<path d="M94 50 L94 42 L102 50 L94 58 Z" fill="#fff" stroke="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 606 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M40 50 L40 94 L56 94 L76 110 L76 34 L56 50 Z" fill="#888" stroke="none"/>
|
||||
<path d="M88 58 C94 64 94 80 88 86" stroke="#888" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M98 50 C108 60 108 84 98 94" stroke="#888" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 444 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M40 50 L40 94 L56 94 L76 110 L76 34 L56 50 Z" fill="#444" stroke="none"/>
|
||||
<path d="M88 58 C94 64 94 80 88 86" stroke="#444" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M98 50 C108 60 108 84 98 94" stroke="#444" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<line x1="112" y1="32" x2="32" y2="112" stroke="#ff4444" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 545 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M72 36 L90 54 L90 90 C90 100 82 108 72 108 C62 108 54 100 54 90 L54 54 Z" fill="#888" stroke="none"/>
|
||||
<ellipse cx="72" cy="54" rx="18" ry="24" fill="#888"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M72 36 L90 54 L90 90 C90 100 82 108 72 108 C62 108 54 100 54 90 L54 54 Z" fill="#444" stroke="none"/>
|
||||
<ellipse cx="72" cy="54" rx="18" ry="24" fill="#444"/>
|
||||
<line x1="108" y1="36" x2="36" y2="108" stroke="#ff4444" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 416 B |
5634
streamdeck-plugin/com.paarrot.streamdeck.sdPlugin/images/paarrot.svg
Normal file
|
After Width: | Height: | Size: 889 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M30 72 L114 36 L90 108 L64 88 L74 64 L50 78 Z" fill="#888" stroke="none"/>
|
||||
<line x1="64" y1="88" x2="74" y2="64" stroke="#1a1a1a" stroke-width="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<circle cx="72" cy="72" r="36" fill="none" stroke="#888" stroke-width="6"/>
|
||||
<circle cx="72" cy="72" r="8" fill="#888"/>
|
||||
<line x1="72" y1="72" x2="72" y2="48" stroke="#888" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="72" y1="72" x2="90" y2="72" stroke="#888" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 461 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M40 50 L40 94 L56 94 L76 110 L76 34 L56 50 Z" fill="#888" stroke="none"/>
|
||||
<path d="M88 58 C94 64 94 80 88 86" stroke="#888" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M98 50 C108 60 108 84 98 94" stroke="#888" stroke-width="6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M108 54 L118 64 M108 90 L118 80" stroke="#888" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 544 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#1a1a1a"/>
|
||||
<path d="M72 36 L90 54 L90 90 C90 100 82 108 72 108 C62 108 54 100 54 90 L54 54 Z" fill="#888" stroke="none"/>
|
||||
<ellipse cx="72" cy="54" rx="18" ry="24" fill="#888"/>
|
||||
<path d="M98 58 L108 68 M98 86 L108 76" stroke="#888" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 413 B |
113
streamdeck-plugin/com.paarrot.streamdeck.sdPlugin/manifest.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
|
||||
"Actions": [
|
||||
{
|
||||
"DisableAutomaticStates": true,
|
||||
"Icon": "images/toggle-mute",
|
||||
"Name": "Toggle Mute",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/mute-off",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
},
|
||||
{
|
||||
"Image": "images/mute-on",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Toggle microphone mute",
|
||||
"UUID": "com.paarrot.streamdeck.togglemute"
|
||||
},
|
||||
{
|
||||
"DisableAutomaticStates": true,
|
||||
"Icon": "images/toggle-deafen",
|
||||
"Name": "Toggle Deafen",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/deafen-off",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
},
|
||||
{
|
||||
"Image": "images/deafen-on",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Toggle deafen (mute speakers)",
|
||||
"UUID": "com.paarrot.streamdeck.toggledeafen"
|
||||
},
|
||||
{
|
||||
"Icon": "images/change-channel",
|
||||
"Name": "Change Channel",
|
||||
"PropertyInspectorPath": "propertyinspector/change-channel.html",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/change-channel",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Switch to a specific channel/room",
|
||||
"UUID": "com.paarrot.streamdeck.changechannel"
|
||||
},
|
||||
{
|
||||
"Icon": "images/send-message",
|
||||
"Name": "Send Message",
|
||||
"PropertyInspectorPath": "propertyinspector/send-message.html",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/send-message",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Send a message to current room",
|
||||
"UUID": "com.paarrot.streamdeck.sendmessage"
|
||||
},
|
||||
{
|
||||
"Icon": "images/status",
|
||||
"Name": "Get Status",
|
||||
"States": [
|
||||
{
|
||||
"Image": "images/status",
|
||||
"TitleAlignment": "bottom",
|
||||
"ShowTitle": true
|
||||
}
|
||||
],
|
||||
"SupportedInMultiActions": false,
|
||||
"Tooltip": "Display current status (mute/deafen state)",
|
||||
"UUID": "com.paarrot.streamdeck.getstatus"
|
||||
}
|
||||
],
|
||||
"Author": "Paarrot",
|
||||
"Category": "Paarrot",
|
||||
"CategoryIcon": "images/paarrot",
|
||||
"CodePath": "plugin/index.html",
|
||||
"Description": "Control Paarrot voice chat (mute, deafen, channels, messages)",
|
||||
"Icon": "images/paarrot",
|
||||
"Name": "Paarrot",
|
||||
"OS": [
|
||||
{
|
||||
"Platform": "windows",
|
||||
"MinimumVersion": "10"
|
||||
},
|
||||
{
|
||||
"Platform": "mac",
|
||||
"MinimumVersion": "10.14"
|
||||
}
|
||||
],
|
||||
"SDKVersion": 2,
|
||||
"Software": {
|
||||
"MinimumVersion": "6.6"
|
||||
},
|
||||
"UUID": "com.paarrot.streamdeck",
|
||||
"URL": "https://github.com/yourusername/paarrot-streamdeck",
|
||||
"Version": "1.0.0.0"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Paarrot Plugin</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Paarrot Stream Deck Plugin
|
||||
* Controls the Paarrot desktop app via its local API
|
||||
*/
|
||||
|
||||
const API_BASE_URL = 'http://127.0.0.1:33384';
|
||||
const POLL_INTERVAL = 1000; // Poll status every second
|
||||
|
||||
/**
|
||||
* Global plugin instance
|
||||
*/
|
||||
let websocket = null;
|
||||
let pluginUUID = null;
|
||||
let statusPollInterval = null;
|
||||
|
||||
/**
|
||||
* Track state for each action instance
|
||||
*/
|
||||
const actionStates = new Map();
|
||||
|
||||
/**
|
||||
* Action UUIDs
|
||||
*/
|
||||
const ACTIONS = {
|
||||
TOGGLE_MUTE: 'com.paarrot.streamdeck.togglemute',
|
||||
TOGGLE_DEAFEN: 'com.paarrot.streamdeck.toggledeafen',
|
||||
CHANGE_CHANNEL: 'com.paarrot.streamdeck.changechannel',
|
||||
SEND_MESSAGE: 'com.paarrot.streamdeck.sendmessage',
|
||||
GET_STATUS: 'com.paarrot.streamdeck.getstatus'
|
||||
};
|
||||
|
||||
/**
|
||||
* Make API call to Paarrot
|
||||
*/
|
||||
async function callAPI(endpoint, method = 'GET', body = null) {
|
||||
try {
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'API call failed');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.error(`API Error (${endpoint}):`, error);
|
||||
showAlert();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current app status
|
||||
*/
|
||||
async function getStatus() {
|
||||
return await callAPI('/status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mute state explicitly
|
||||
* @param {boolean} muted - Whether to mute
|
||||
*/
|
||||
async function setMute(muted) {
|
||||
return await callAPI('/mute', 'POST', { muted });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set deafen state explicitly
|
||||
* @param {boolean} deafened - Whether to deafen
|
||||
*/
|
||||
async function setDeafen(deafened) {
|
||||
return await callAPI('/deafen', 'POST', { deafened });
|
||||
}
|
||||
|
||||
/**
|
||||
* Change channel
|
||||
*/
|
||||
async function changeChannel(roomId) {
|
||||
return await callAPI('/channel', 'POST', { roomId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to current room
|
||||
*/
|
||||
async function sendMessage(message) {
|
||||
return await callAPI('/message/current', 'POST', { message });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of channels
|
||||
*/
|
||||
async function getChannels() {
|
||||
return await callAPI('/channels');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button state based on status
|
||||
*/
|
||||
function updateButtonStates(status) {
|
||||
actionStates.forEach((state, context) => {
|
||||
if (state.action === ACTIONS.TOGGLE_MUTE) {
|
||||
const newState = status.muted ? 1 : 0;
|
||||
if (state.currentState !== newState) {
|
||||
setState(context, newState);
|
||||
state.currentState = newState;
|
||||
}
|
||||
} else if (state.action === ACTIONS.TOGGLE_DEAFEN) {
|
||||
const newState = status.deafened ? 1 : 0;
|
||||
if (state.currentState !== newState) {
|
||||
setState(context, newState);
|
||||
state.currentState = newState;
|
||||
}
|
||||
} else if (state.action === ACTIONS.GET_STATUS) {
|
||||
const title = `${status.muted ? 'M' : ''}${status.deafened ? 'D' : ''}${!status.muted && !status.deafened ? 'OK' : ''}`;
|
||||
setTitle(context, title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling for status updates
|
||||
*/
|
||||
function startStatusPolling() {
|
||||
if (statusPollInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
statusPollInterval = setInterval(async () => {
|
||||
try {
|
||||
const status = await getStatus();
|
||||
updateButtonStates(status);
|
||||
} catch (error) {
|
||||
// Silently fail - server might not be running
|
||||
}
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop polling for status updates
|
||||
*/
|
||||
function stopStatusPolling() {
|
||||
if (statusPollInterval) {
|
||||
clearInterval(statusPollInterval);
|
||||
statusPollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send command to Stream Deck
|
||||
*/
|
||||
function sendToStreamDeck(event, context, payload = {}) {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
event,
|
||||
context,
|
||||
...payload
|
||||
};
|
||||
websocket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set button state
|
||||
*/
|
||||
function setState(context, state) {
|
||||
sendToStreamDeck('setState', context, { payload: { state } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set button title
|
||||
*/
|
||||
function setTitle(context, title) {
|
||||
sendToStreamDeck('setTitle', context, { payload: { title } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show alert on button
|
||||
*/
|
||||
function showAlert(context) {
|
||||
if (context) {
|
||||
sendToStreamDeck('showAlert', context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show OK on button
|
||||
*/
|
||||
function showOk(context) {
|
||||
sendToStreamDeck('showOk', context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle button press
|
||||
*/
|
||||
async function handleKeyDown(context, settings, action) {
|
||||
try {
|
||||
switch (action) {
|
||||
case ACTIONS.TOGGLE_MUTE: {
|
||||
// Get current state and set opposite
|
||||
const status = await getStatus();
|
||||
await setMute(!status.muted);
|
||||
showOk(context);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTIONS.TOGGLE_DEAFEN: {
|
||||
// Get current state and set opposite
|
||||
const status = await getStatus();
|
||||
await setDeafen(!status.deafened);
|
||||
showOk(context);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTIONS.CHANGE_CHANNEL:
|
||||
if (settings.roomId) {
|
||||
await changeChannel(settings.roomId);
|
||||
showOk(context);
|
||||
} else {
|
||||
showAlert(context);
|
||||
}
|
||||
break;
|
||||
|
||||
case ACTIONS.SEND_MESSAGE:
|
||||
if (settings.message) {
|
||||
await sendMessage(settings.message);
|
||||
showOk(context);
|
||||
} else {
|
||||
showAlert(context);
|
||||
}
|
||||
break;
|
||||
|
||||
case ACTIONS.GET_STATUS:
|
||||
const status = await getStatus();
|
||||
updateButtonStates(status);
|
||||
showOk(context);
|
||||
break;
|
||||
}
|
||||
|
||||
// Force immediate status update
|
||||
const status = await getStatus();
|
||||
updateButtonStates(status);
|
||||
} catch (error) {
|
||||
showAlert(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action appearing
|
||||
*/
|
||||
function handleWillAppear(context, settings, action) {
|
||||
actionStates.set(context, {
|
||||
action,
|
||||
settings,
|
||||
currentState: 0
|
||||
});
|
||||
|
||||
// Get initial state
|
||||
getStatus()
|
||||
.then(status => updateButtonStates(status))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action disappearing
|
||||
*/
|
||||
function handleWillDisappear(context) {
|
||||
actionStates.delete(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle settings change
|
||||
*/
|
||||
function handleDidReceiveSettings(context, settings, action) {
|
||||
const state = actionStates.get(context);
|
||||
if (state) {
|
||||
state.settings = settings;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle property inspector request
|
||||
*/
|
||||
function handleSendToPlugin(context, action, payload) {
|
||||
// Handle requests from property inspector
|
||||
if (payload.event === 'getChannels') {
|
||||
getChannels()
|
||||
.then(channels => {
|
||||
sendToStreamDeck('sendToPropertyInspector', context, {
|
||||
action,
|
||||
payload: {
|
||||
event: 'channelList',
|
||||
channels
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to get channels:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup WebSocket connection to Stream Deck
|
||||
*/
|
||||
function connectElgatoStreamDeckSocket(inPort, inPluginUUID, inRegisterEvent, inInfo) {
|
||||
pluginUUID = inPluginUUID;
|
||||
|
||||
websocket = new WebSocket(`ws://127.0.0.1:${inPort}`);
|
||||
|
||||
websocket.onopen = () => {
|
||||
const registerPayload = {
|
||||
event: inRegisterEvent,
|
||||
uuid: inPluginUUID
|
||||
};
|
||||
websocket.send(JSON.stringify(registerPayload));
|
||||
|
||||
// Start status polling
|
||||
startStatusPolling();
|
||||
};
|
||||
|
||||
websocket.onmessage = (evt) => {
|
||||
try {
|
||||
const message = JSON.parse(evt.data);
|
||||
const { event, action, context, payload } = message;
|
||||
const settings = payload?.settings || {};
|
||||
|
||||
switch (event) {
|
||||
case 'keyDown':
|
||||
handleKeyDown(context, settings, action);
|
||||
break;
|
||||
|
||||
case 'willAppear':
|
||||
handleWillAppear(context, settings, action);
|
||||
break;
|
||||
|
||||
case 'willDisappear':
|
||||
handleWillDisappear(context);
|
||||
break;
|
||||
|
||||
case 'didReceiveSettings':
|
||||
handleDidReceiveSettings(context, settings, action);
|
||||
break;
|
||||
|
||||
case 'sendToPlugin':
|
||||
handleSendToPlugin(context, action, payload);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket message error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
stopStatusPolling();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Change Channel Settings</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 10px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #d0d0d0;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select, input {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #3d3d3d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
color: #d0d0d0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
select:focus, input:focus {
|
||||
outline: none;
|
||||
border-color: #0e99e0;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 8px;
|
||||
background-color: #3d3d3d;
|
||||
border-left: 3px solid #0e99e0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #3d3d3d;
|
||||
color: #d0d0d0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="info">
|
||||
Select a room/channel to switch to when this button is pressed.
|
||||
</div>
|
||||
|
||||
<label for="roomId">Channel/Room:</label>
|
||||
<select id="roomId">
|
||||
<option value="">Loading channels...</option>
|
||||
</select>
|
||||
|
||||
<label for="customRoomId">Or enter Room ID manually:</label>
|
||||
<input type="text" id="customRoomId" placeholder="!abc123:matrix.org" />
|
||||
|
||||
<script>
|
||||
let websocket = null;
|
||||
let uuid = null;
|
||||
let actionInfo = null;
|
||||
let currentSettings = {};
|
||||
|
||||
function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
|
||||
uuid = inUUID;
|
||||
actionInfo = JSON.parse(inActionInfo);
|
||||
currentSettings = actionInfo.payload.settings || {};
|
||||
|
||||
websocket = new WebSocket('ws://127.0.0.1:' + inPort);
|
||||
|
||||
websocket.onopen = function() {
|
||||
const json = {
|
||||
event: inRegisterEvent,
|
||||
uuid: inUUID
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
|
||||
// Request channel list
|
||||
sendToPlugin({ event: 'getChannels' });
|
||||
};
|
||||
|
||||
websocket.onmessage = function(evt) {
|
||||
const message = JSON.parse(evt.data);
|
||||
|
||||
if (message.event === 'sendToPropertyInspector') {
|
||||
if (message.payload.event === 'channelList') {
|
||||
populateChannels(message.payload.channels);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set current values
|
||||
if (currentSettings.roomId) {
|
||||
document.getElementById('customRoomId').value = currentSettings.roomId;
|
||||
}
|
||||
}
|
||||
|
||||
function sendToPlugin(payload) {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
action: actionInfo.action,
|
||||
event: 'sendToPlugin',
|
||||
context: uuid,
|
||||
payload: payload
|
||||
};
|
||||
websocket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
const settings = {
|
||||
roomId: document.getElementById('customRoomId').value || document.getElementById('roomId').value
|
||||
};
|
||||
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
event: 'setSettings',
|
||||
context: uuid,
|
||||
payload: settings
|
||||
};
|
||||
websocket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
function populateChannels(channels) {
|
||||
const select = document.getElementById('roomId');
|
||||
select.innerHTML = '<option value="">-- Select a channel --</option>';
|
||||
|
||||
if (!channels || channels.length === 0) {
|
||||
select.innerHTML = '<option value="">No channels available</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by server
|
||||
const grouped = {};
|
||||
channels.forEach(channel => {
|
||||
if (!grouped[channel.server]) {
|
||||
grouped[channel.server] = [];
|
||||
}
|
||||
grouped[channel.server].push(channel);
|
||||
});
|
||||
|
||||
// Add options grouped by server
|
||||
Object.keys(grouped).sort().forEach(server => {
|
||||
const optgroup = document.createElement('optgroup');
|
||||
optgroup.label = server;
|
||||
|
||||
grouped[server].forEach(channel => {
|
||||
const option = document.createElement('option');
|
||||
option.value = channel.roomId;
|
||||
option.textContent = channel.name;
|
||||
if (currentSettings.roomId === channel.roomId) {
|
||||
option.selected = true;
|
||||
}
|
||||
optgroup.appendChild(option);
|
||||
});
|
||||
|
||||
select.appendChild(optgroup);
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('roomId').addEventListener('change', saveSettings);
|
||||
document.getElementById('customRoomId').addEventListener('input', saveSettings);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Send Message Settings</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 10px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #d0d0d0;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #3d3d3d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
color: #d0d0d0;
|
||||
font-size: 13px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #0e99e0;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 8px;
|
||||
background-color: #3d3d3d;
|
||||
border-left: 3px solid #0e99e0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.examples {
|
||||
padding: 8px;
|
||||
background-color: #3d3d3d;
|
||||
border-left: 3px solid #666;
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.examples h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.examples ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.examples li {
|
||||
margin-bottom: 4px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="info">
|
||||
Enter the message to send to the currently active room when this button is pressed.
|
||||
</div>
|
||||
|
||||
<label for="message">Message:</label>
|
||||
<textarea id="message" placeholder="Type your message here..."></textarea>
|
||||
|
||||
<div class="examples">
|
||||
<h4>Example Messages:</h4>
|
||||
<ul>
|
||||
<li>BRB! (Be Right Back)</li>
|
||||
<li>AFK for a few minutes</li>
|
||||
<li>On my way!</li>
|
||||
<li>Thanks everyone!</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let websocket = null;
|
||||
let uuid = null;
|
||||
let actionInfo = null;
|
||||
let currentSettings = {};
|
||||
let saveTimeout = null;
|
||||
|
||||
function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
|
||||
uuid = inUUID;
|
||||
actionInfo = JSON.parse(inActionInfo);
|
||||
currentSettings = actionInfo.payload.settings || {};
|
||||
|
||||
websocket = new WebSocket('ws://127.0.0.1:' + inPort);
|
||||
|
||||
websocket.onopen = function() {
|
||||
const json = {
|
||||
event: inRegisterEvent,
|
||||
uuid: inUUID
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
};
|
||||
|
||||
// Set current value
|
||||
if (currentSettings.message) {
|
||||
document.getElementById('message').value = currentSettings.message;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
// Debounce saving
|
||||
if (saveTimeout) {
|
||||
clearTimeout(saveTimeout);
|
||||
}
|
||||
|
||||
saveTimeout = setTimeout(() => {
|
||||
const settings = {
|
||||
message: document.getElementById('message').value
|
||||
};
|
||||
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
event: 'setSettings',
|
||||
context: uuid,
|
||||
payload: settings
|
||||
};
|
||||
websocket.send(JSON.stringify(message));
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Event listener
|
||||
document.getElementById('message').addEventListener('input', saveSettings);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
18
streamdeck-plugin/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "paarrot-streamdeck-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Elgato Stream Deck plugin for Paarrot voice chat app",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"validate": "node validate.js"
|
||||
},
|
||||
"keywords": [
|
||||
"stream-deck",
|
||||
"elgato",
|
||||
"paarrot",
|
||||
"voice-chat",
|
||||
"matrix"
|
||||
],
|
||||
"author": "Paarrot",
|
||||
"license": "MIT"
|
||||
}
|
||||
156
streamdeck-plugin/validate.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Validation script for Paarrot Stream Deck Plugin
|
||||
* Checks plugin structure and manifest
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PLUGIN_DIR = 'com.paarrot.streamdeck.sdPlugin';
|
||||
|
||||
let errors = 0;
|
||||
let warnings = 0;
|
||||
|
||||
function error(message) {
|
||||
console.error(`❌ ERROR: ${message}`);
|
||||
errors++;
|
||||
}
|
||||
|
||||
function warn(message) {
|
||||
console.warn(`⚠️ WARNING: ${message}`);
|
||||
warnings++;
|
||||
}
|
||||
|
||||
function success(message) {
|
||||
console.log(`✓ ${message}`);
|
||||
}
|
||||
|
||||
console.log('Validating Paarrot Stream Deck Plugin...\n');
|
||||
|
||||
// Check plugin directory exists
|
||||
if (!fs.existsSync(PLUGIN_DIR)) {
|
||||
error(`Plugin directory '${PLUGIN_DIR}' not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check manifest.json
|
||||
const manifestPath = path.join(PLUGIN_DIR, 'manifest.json');
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
error('manifest.json not found');
|
||||
} else {
|
||||
success('manifest.json exists');
|
||||
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = ['Actions', 'Author', 'CodePath', 'Description', 'Name', 'Version', 'SDKVersion'];
|
||||
requiredFields.forEach(field => {
|
||||
if (!manifest[field]) {
|
||||
error(`manifest.json missing required field: ${field}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (manifest.Actions && Array.isArray(manifest.Actions)) {
|
||||
success(`Found ${manifest.Actions.length} actions`);
|
||||
|
||||
// Validate each action
|
||||
manifest.Actions.forEach((action, index) => {
|
||||
const actionName = action.Name || `Action ${index}`;
|
||||
|
||||
if (!action.UUID) {
|
||||
error(`${actionName}: Missing UUID`);
|
||||
}
|
||||
|
||||
if (!action.Icon) {
|
||||
error(`${actionName}: Missing Icon`);
|
||||
} else {
|
||||
// Check if icon file exists
|
||||
const iconPath = path.join(PLUGIN_DIR, `${action.Icon}.svg`);
|
||||
const iconPathPng = path.join(PLUGIN_DIR, `${action.Icon}.png`);
|
||||
if (!fs.existsSync(iconPath) && !fs.existsSync(iconPathPng)) {
|
||||
warn(`${actionName}: Icon file not found: ${action.Icon}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.States || !Array.isArray(action.States) || action.States.length === 0) {
|
||||
error(`${actionName}: Missing or invalid States`);
|
||||
} else {
|
||||
action.States.forEach((state, stateIndex) => {
|
||||
if (!state.Image) {
|
||||
warn(`${actionName} State ${stateIndex}: Missing Image`);
|
||||
} else {
|
||||
const imagePath = path.join(PLUGIN_DIR, `${state.Image}.svg`);
|
||||
const imagePathPng = path.join(PLUGIN_DIR, `${state.Image}.png`);
|
||||
if (!fs.existsSync(imagePath) && !fs.existsSync(imagePathPng)) {
|
||||
warn(`${actionName} State ${stateIndex}: Image file not found: ${state.Image}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (action.PropertyInspectorPath) {
|
||||
const piPath = path.join(PLUGIN_DIR, action.PropertyInspectorPath);
|
||||
if (!fs.existsSync(piPath)) {
|
||||
error(`${actionName}: PropertyInspector file not found: ${action.PropertyInspectorPath}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
error(`manifest.json parse error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check plugin files
|
||||
const pluginPath = path.join(PLUGIN_DIR, 'plugin', 'index.html');
|
||||
if (!fs.existsSync(pluginPath)) {
|
||||
error('plugin/index.html not found');
|
||||
} else {
|
||||
success('plugin/index.html exists');
|
||||
}
|
||||
|
||||
const pluginJsPath = path.join(PLUGIN_DIR, 'plugin', 'index.js');
|
||||
if (!fs.existsSync(pluginJsPath)) {
|
||||
error('plugin/index.js not found');
|
||||
} else {
|
||||
success('plugin/index.js exists');
|
||||
}
|
||||
|
||||
// Check property inspectors
|
||||
const piDir = path.join(PLUGIN_DIR, 'propertyinspector');
|
||||
if (fs.existsSync(piDir)) {
|
||||
const piFiles = fs.readdirSync(piDir);
|
||||
success(`Found ${piFiles.length} property inspector file(s)`);
|
||||
} else {
|
||||
warn('propertyinspector directory not found');
|
||||
}
|
||||
|
||||
// Check images directory
|
||||
const imagesDir = path.join(PLUGIN_DIR, 'images');
|
||||
if (fs.existsSync(imagesDir)) {
|
||||
const imageFiles = fs.readdirSync(imagesDir);
|
||||
success(`Found ${imageFiles.length} image file(s)`);
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
warn('images directory is empty');
|
||||
}
|
||||
} else {
|
||||
error('images directory not found');
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(50));
|
||||
if (errors === 0 && warnings === 0) {
|
||||
console.log('✓ Plugin validation passed!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`Found ${errors} error(s) and ${warnings} warning(s)`);
|
||||
if (errors > 0) {
|
||||
console.log('\nPlease fix errors before building the plugin.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\nPlugin can be built, but consider fixing warnings.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
26
test-electron.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
console.log('TEST: Electron starting...');
|
||||
|
||||
app.whenReady().then(() => {
|
||||
console.log('TEST: App ready, creating window...');
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
title: 'TEST WINDOW',
|
||||
show: true,
|
||||
center: true
|
||||
});
|
||||
|
||||
console.log('TEST: Window created, loading content...');
|
||||
win.loadURL('https://www.google.com');
|
||||
|
||||
win.on('ready-to-show', () => {
|
||||
console.log('TEST: Window ready-to-show fired');
|
||||
});
|
||||
|
||||
win.on('show', () => {
|
||||
console.log('TEST: Window shown!');
|
||||
});
|
||||
});
|
||||