Compare commits
241 Commits
v4.10.2
...
d17df23b29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | |||
|
|
46d2969cfb | ||
| 31225233c5 | |||
| 796659318e | |||
|
|
bd544193be | ||
| f0428b7bfc | |||
|
|
0073678b1f | ||
| 933f83aca9 | |||
|
|
fe99952fd5 | ||
|
|
6b160b76de | ||
|
|
39d33b9a3f | ||
|
|
b01a2b69b3 | ||
|
|
83ee6d868b | ||
|
|
8ee4250231 | ||
|
|
91e92e8b7d | ||
|
|
d1a458feb3 | ||
|
|
b27669f98f | ||
|
|
1e9599ee1a | ||
|
|
6115b7fe6f | ||
|
|
7336454a05 | ||
|
|
8a2d6d715e | ||
| 72cb5b6fe4 | |||
|
|
47ba333368 | ||
|
|
70ed4513d8 | ||
|
|
feb3188516 | ||
|
|
252d515970 | ||
|
|
853a1db0bb | ||
|
|
015769bdaf | ||
|
|
7663e1d1c0 | ||
| 1b06e46b06 | |||
|
|
033d323b6f | ||
|
|
cae4296b92 | ||
|
|
232aa3daa7 | ||
|
|
170947fe1e | ||
| 2d73d3a043 | |||
|
|
342ca4cfe0 | ||
|
|
b570d3e992 | ||
|
|
9dd115698b | ||
|
|
c7897d1516 | ||
|
|
2063350683 | ||
|
|
f6f72d4b81 | ||
|
|
0bb14b5f22 | ||
|
|
d636fc0708 | ||
|
|
62392d3197 | ||
|
|
f652ce8c61 | ||
|
|
324b9ed562 | ||
|
|
3c16a796d6 | ||
|
|
baa82f1f0f | ||
|
|
e38c9d5dcc | ||
|
|
e076bde9b0 | ||
|
|
f3b118aacc | ||
|
|
00080782a3 | ||
|
|
413957b6d3 | ||
|
|
be1781518c | ||
|
|
e105a1fd6b | ||
|
|
0768ed0116 | ||
|
|
24bed419a2 | ||
|
|
f23a07384c | ||
|
|
4e782409ee | ||
|
|
b9e8fec8d4 | ||
|
|
59adbcbdf4 | ||
|
|
448c4c2dfd | ||
|
|
b926ea9c88 | ||
|
|
bcdeaf6e48 | ||
|
|
7945e2d0be | ||
|
|
b839d5e98a | ||
|
|
7b65b5a8c2 | ||
|
|
dbf70fd9c4 | ||
|
|
775f18f601 | ||
|
|
58b911fbd5 | ||
|
|
ebe9fa3983 | ||
|
|
72193d2490 | ||
|
|
62c42f7b63 | ||
|
|
3df5432127 | ||
|
|
1e455a00f2 | ||
|
|
7508ebacab | ||
|
|
22f5c5e7e4 | ||
|
|
caf1869e3e | ||
|
|
c112fd23b0 | ||
| 1533b38cac | |||
|
|
3ba3c0ca82 | ||
| b44917e52d | |||
| 158da6f608 | |||
|
|
55679915c5 | ||
| 9e6417c47e | |||
| f5515b421d | |||
| 3b0092d972 | |||
|
|
eb806e4054 | ||
| 5d9304e3ba | |||
|
|
9e74e65d2d | ||
| a00e257845 | |||
|
|
d62a964758 | ||
| dafd8c538e | |||
|
|
d51c787c2a | ||
| e77eda66e9 | |||
| ba636fe6fc | |||
|
|
d94a1e66b8 | ||
| 809e06f9c9 | |||
|
|
f67a3cd498 | ||
| 8739ddd5c4 | |||
|
|
fb1430ee59 | ||
| 32ad73fd65 | |||
| 22ac1bf4b7 | |||
|
|
93043e39cc | ||
| 0eae03cb60 | |||
| 3aac8e79db | |||
|
|
08cd0d0a30 | ||
| 176f1e1604 | |||
| b2f7697865 | |||
|
|
bd7db55251 | ||
| cb001319ae | |||
|
|
5248194ceb | ||
| f57f127951 | |||
| 3ecf1a5f03 | |||
|
|
f7e2cc8250 | ||
| 187534dd6c | |||
|
|
9b20339c57 | ||
| 0194d6acfd | |||
|
|
0b4b8f11e7 | ||
| a0aee6ddbd | |||
| ce3c236e2e | |||
| 41159d206c | |||
| 87bb7347e3 | |||
| 11cdf0acf2 | |||
| c69c5f3c9c | |||
| b53c5d34ef | |||
|
|
887a3ef7b3 | ||
| 3ee4def6db | |||
| 3e09e908f5 | |||
| ebeede58d2 | |||
| da43506c01 | |||
| 1f23647433 | |||
| 55cf5fdf8a | |||
| 20a764b870 | |||
| 5ad2747a2f | |||
| ece77ccba6 | |||
| 89b0da0854 | |||
| 4b185556f8 | |||
| 579b77e8d5 | |||
| 0090b6bd84 | |||
| 7296b0cd34 | |||
| 7a5e258f23 | |||
| 3ccae09dca | |||
| 90407c49c6 | |||
| 61c1e3857d | |||
| 6cc8d06e18 | |||
| 27a7e61ff8 | |||
| 72af2b3ef7 | |||
| c612dfa824 | |||
| ca049e53f5 | |||
| 5a3cbd02e6 | |||
| 02bc4caaaf | |||
| 72b9a1480c | |||
| ca56725984 | |||
| e4d8f6152e | |||
| a191fb0609 | |||
| b7031f85e0 | |||
| 0e6d140d01 | |||
| 2d0398f5d3 | |||
| b6c07916af | |||
| 37141d3d31 | |||
| 58626aa942 | |||
| d4264f0e8d | |||
| 90fc67c805 | |||
| 6cabc01f77 | |||
| d2619004b5 | |||
| eaa8e3949a | |||
| 6a32c023ee | |||
| 773c036404 | |||
| 2a8c3a1475 | |||
| a749db75f7 | |||
| d16a55e06d | |||
| 93085b7029 | |||
| fce3c8ea59 | |||
| d73a6874f6 | |||
| 8531cfdbf6 | |||
| 430f37a8a4 | |||
| 1c4165b536 | |||
| 6f987b6be3 | |||
| 1264065aeb | |||
| 058d55af7b | |||
| a7156f1a76 | |||
| 94b72d0fe8 | |||
| 3198e496c4 | |||
| 1de0ed67a2 | |||
| 47f58730ee | |||
| 01312be50d | |||
| 8637654e88 | |||
| 0fd03b8496 | |||
| 890b060ab4 | |||
| 9d02cc6c5b |
@@ -8,321 +8,278 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
increment-version:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/')
|
||||
if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
outputs:
|
||||
should_release: ${{ steps.check.outputs.should_release }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
version: ${{ steps.bump.outputs.VERSION }}
|
||||
should_build: ${{ steps.bump.outputs.SHOULD_BUILD }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if version tag exists
|
||||
id: check
|
||||
- name: Bump patch version
|
||||
id: bump
|
||||
run: |
|
||||
VERSION=$(grep -Po '"version":\s*"\K[^"]+' src-tauri/tauri.conf.json | head -1)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag v$VERSION already exists"
|
||||
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Tag v$VERSION does not exist, will create release"
|
||||
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
create-tag:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
# Get current version from package.json
|
||||
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||
echo "Current version: $CURRENT"
|
||||
|
||||
# Split and increment patch
|
||||
MAJOR=$(echo $CURRENT | cut -d. -f1)
|
||||
MINOR=$(echo $CURRENT | cut -d. -f2)
|
||||
PATCH=$(echo $CURRENT | cut -d. -f3)
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Update package.json
|
||||
sed -i "s/\"version\": \"${CURRENT}\"/\"version\": \"${NEW_VERSION}\"/" package.json
|
||||
|
||||
# Verify the change
|
||||
grep '"version"' package.json | head -1
|
||||
|
||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "SHOULD_BUILD=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push tag
|
||||
- name: Commit and push version bump
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git tag -a "v${{ needs.check-version.outputs.version }}" -m "Release v${{ needs.check-version.outputs.version }}"
|
||||
git push origin "v${{ needs.check-version.outputs.version }}"
|
||||
git add package.json
|
||||
git commit -m "chore: bump version to ${{ steps.bump.outputs.VERSION }} [skip ci]"
|
||||
git push
|
||||
|
||||
build-windows:
|
||||
runs-on: windows
|
||||
needs: increment-version
|
||||
if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
submodules: false
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Checkout submodules manually
|
||||
shell: powershell
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
shell: powershell
|
||||
run: git pull origin main
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install dependencies (root)
|
||||
run: npm install
|
||||
run: npm install --prefer-offline
|
||||
|
||||
- name: Install dependencies (cinny)
|
||||
working-directory: ./cinny
|
||||
run: npm install
|
||||
run: npm install --prefer-offline
|
||||
|
||||
- name: Build Tauri app
|
||||
run: npm run tauri build
|
||||
|
||||
- name: Rename MSI
|
||||
- name: Clean previous builds
|
||||
shell: powershell
|
||||
run: |
|
||||
$msi = Get-ChildItem "src-tauri/target/release/bundle/msi/*.msi" | Select-Object -First 1
|
||||
$newName = "Cinny-Windows-x64.msi"
|
||||
Copy-Item $msi.FullName "src-tauri/target/release/bundle/msi/$newName"
|
||||
if (Test-Path cinny/dist) { Remove-Item -Recurse -Force cinny/dist }
|
||||
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
|
||||
|
||||
- name: Upload MSI installer
|
||||
- name: Build Electron app
|
||||
run: npm run build:win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Squirrel Setup (x64)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Cinny-Windows-x64.msi
|
||||
path: src-tauri/target/release/bundle/msi/Cinny-Windows-x64.msi
|
||||
name: Paarrot-Setup-x64.exe
|
||||
path: dist-electron/squirrel-windows/Paarrot-*-win-x64.exe
|
||||
|
||||
- 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
|
||||
needs: increment-version
|
||||
if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: git pull origin main
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev rpm
|
||||
|
||||
- name: Install dependencies (root)
|
||||
run: npm install
|
||||
run: npm ci --prefer-offline
|
||||
|
||||
- name: Install dependencies (cinny)
|
||||
working-directory: ./cinny
|
||||
run: npm install
|
||||
run: npm ci --prefer-offline
|
||||
|
||||
- name: Build Tauri app
|
||||
run: npm run tauri build
|
||||
|
||||
- name: Rename Linux packages
|
||||
- name: Clean previous builds
|
||||
run: |
|
||||
cp src-tauri/target/release/bundle/appimage/*.AppImage src-tauri/target/release/bundle/appimage/Cinny-Linux-x64.AppImage
|
||||
cp src-tauri/target/release/bundle/deb/*.deb src-tauri/target/release/bundle/deb/Cinny-Linux-x64.deb
|
||||
cp src-tauri/target/release/bundle/rpm/*.rpm src-tauri/target/release/bundle/rpm/Cinny-Linux-x64.rpm
|
||||
# Copy signature file for updater
|
||||
if [ -f src-tauri/target/release/bundle/appimage/*.AppImage.sig ]; then
|
||||
cp src-tauri/target/release/bundle/appimage/*.AppImage.sig src-tauri/target/release/bundle/appimage/Cinny-Linux-x64.AppImage.sig
|
||||
fi
|
||||
rm -rf cinny/dist dist-electron
|
||||
|
||||
- name: Build Electron app
|
||||
run: npm run build:linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Cinny-Linux-x64.AppImage
|
||||
path: src-tauri/target/release/bundle/appimage/Cinny-Linux-x64.AppImage
|
||||
|
||||
- name: Upload AppImage Signature
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: Cinny-Linux-x64.AppImage.sig
|
||||
path: src-tauri/target/release/bundle/appimage/Cinny-Linux-x64.AppImage.sig
|
||||
|
||||
- name: Upload DEB package
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Cinny-Linux-x64.deb
|
||||
path: src-tauri/target/release/bundle/deb/Cinny-Linux-x64.deb
|
||||
|
||||
- name: Upload RPM package
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Cinny-Linux-x64.rpm
|
||||
path: src-tauri/target/release/bundle/rpm/Cinny-Linux-x64.rpm
|
||||
|
||||
build-android:
|
||||
runs-on: windows
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
target: aarch64-linux-android
|
||||
|
||||
- name: Add Android targets
|
||||
run: |
|
||||
rustup target add aarch64-linux-android --toolchain stable
|
||||
rustup target add armv7-linux-androideabi --toolchain stable
|
||||
rustup target add x86_64-linux-android --toolchain stable
|
||||
rustup target add i686-linux-android --toolchain stable
|
||||
|
||||
- name: Install dependencies (root)
|
||||
run: npm install
|
||||
|
||||
- name: Install dependencies (cinny)
|
||||
working-directory: ./cinny
|
||||
run: npm install
|
||||
|
||||
- name: Build Android APK
|
||||
shell: powershell
|
||||
run: |
|
||||
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"
|
||||
npm run tauri android build
|
||||
env:
|
||||
ANDROID_HOME: ${{ env.ANDROID_HOME }}
|
||||
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/25.2.9519653
|
||||
RUSTUP_TOOLCHAIN: stable
|
||||
|
||||
- name: Sign APK
|
||||
shell: powershell
|
||||
run: |
|
||||
$unsignedApk = "src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk"
|
||||
$signedApk = "src-tauri/gen/android/app/build/outputs/apk/universal/release/cinny-release.apk"
|
||||
$keystore = "$env:USERPROFILE\.android\debug.keystore"
|
||||
|
||||
# Find Android SDK
|
||||
$androidHome = if ($env:ANDROID_HOME) { $env:ANDROID_HOME }
|
||||
elseif ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT }
|
||||
elseif (Test-Path "$env:LOCALAPPDATA\Android\Sdk") { "$env:LOCALAPPDATA\Android\Sdk" }
|
||||
else { "$env:USERPROFILE\AppData\Local\Android\Sdk" }
|
||||
|
||||
Write-Host "Using Android SDK at: $androidHome"
|
||||
|
||||
# Use debug keystore for signing (create if missing)
|
||||
if (-not (Test-Path $keystore)) {
|
||||
keytool -genkey -v -keystore $keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"
|
||||
}
|
||||
|
||||
# Find build tools
|
||||
$buildTools = Get-ChildItem "$androidHome\build-tools" | Sort-Object Name -Descending | Select-Object -First 1
|
||||
$apksigner = "$($buildTools.FullName)\apksigner.bat"
|
||||
$zipalign = "$($buildTools.FullName)\zipalign.exe"
|
||||
|
||||
Write-Host "Using build tools: $($buildTools.FullName)"
|
||||
|
||||
# Align the APK
|
||||
& $zipalign -v -p 4 $unsignedApk "$unsignedApk.aligned"
|
||||
|
||||
# Sign the APK
|
||||
& $apksigner sign --ks $keystore --ks-pass pass:android --key-pass pass:android --out $signedApk "$unsignedApk.aligned"
|
||||
|
||||
# Verify
|
||||
& $apksigner verify $signedApk
|
||||
|
||||
Write-Host "Signed APK created at: $signedApk"
|
||||
|
||||
# Rename to final name
|
||||
Copy-Item $signedApk "src-tauri/gen/android/app/build/outputs/apk/universal/release/Cinny-Android.apk"
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Cinny-Android.apk
|
||||
path: src-tauri/gen/android/app/build/outputs/apk/universal/release/Cinny-Android.apk
|
||||
name: Paarrot-Linux-x64.AppImage
|
||||
path: dist-electron/Paarrot-*.AppImage
|
||||
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-windows, build-linux, build-android]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [increment-version, build-windows, build-linux]
|
||||
if: always() && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: git pull origin main
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
# Use version from increment-version job if available, otherwise read from file
|
||||
if [ -n "${{ needs.increment-version.outputs.version }}" ]; then
|
||||
VERSION="${{ needs.increment-version.outputs.version }}"
|
||||
else
|
||||
VERSION=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||
fi
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Version: $VERSION"
|
||||
|
||||
- name: Check if tag exists
|
||||
id: tag_check
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag v$VERSION already exists"
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Tag v$VERSION does not exist"
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create and push tag
|
||||
if: steps.tag_check.outputs.exists == 'false'
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git tag -a "v${{ steps.version.outputs.VERSION }}" -m "Release v${{ steps.version.outputs.VERSION }}"
|
||||
git push origin "v${{ steps.version.outputs.VERSION }}"
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create update.json
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
APPIMAGE_SIG=""
|
||||
if [ -f "artifacts/Cinny-Linux-x64.AppImage.sig/Cinny-Linux-x64.AppImage.sig" ]; then
|
||||
APPIMAGE_SIG=$(cat "artifacts/Cinny-Linux-x64.AppImage.sig/Cinny-Linux-x64.AppImage.sig")
|
||||
fi
|
||||
|
||||
cat > update.json << EOF
|
||||
{
|
||||
"version": "${VERSION}",
|
||||
"notes": "Update to version ${VERSION}",
|
||||
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"platforms": {
|
||||
"linux-x86_64": {
|
||||
"signature": "${APPIMAGE_SIG}",
|
||||
"url": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases/download/v${VERSION}/Cinny-Linux-x64.AppImage"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat update.json
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release-files
|
||||
# Move artifacts out of subdirectories
|
||||
find artifacts -type f \( -name "*.msi" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" -o -name "*.apk" -o -name "*.sig" \) -exec cp {} release-files/ \;
|
||||
cp update.json release-files/
|
||||
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.nupkg" -o -name "RELEASES*" \) -exec cp {} release-files/ \;
|
||||
ls -la release-files/
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: release-files/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
- name: Create or Update Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
TAG_NAME="v${VERSION}"
|
||||
API_BASE="http://synbox.ruv.wtf:8418/api/v1"
|
||||
REPO="litruv/cinny-desktop"
|
||||
|
||||
# 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')
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Creating new release for ${TAG_NAME}..."
|
||||
RELEASE_ID=$(curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
||||
"${API_BASE}/repos/${REPO}/releases" | jq -r '.id')
|
||||
echo "Created release with ID: ${RELEASE_ID}"
|
||||
else
|
||||
echo "Release exists with ID: ${RELEASE_ID}"
|
||||
# Delete existing assets
|
||||
echo "Deleting existing assets..."
|
||||
ASSETS=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets" | jq -r '.[].id')
|
||||
for ASSET_ID in $ASSETS; do
|
||||
echo "Deleting asset ${ASSET_ID}..."
|
||||
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
|
||||
done
|
||||
fi
|
||||
|
||||
# Upload new assets
|
||||
echo "Uploading assets..."
|
||||
for FILE in release-files/*; do
|
||||
FILENAME=$(basename "$FILE")
|
||||
echo "Uploading ${FILENAME}..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${FILE}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "Release complete!"
|
||||
|
||||
173
.github/workflows/tauri.yml
vendored
@@ -1,173 +0,0 @@
|
||||
name: "Publish Tauri App"
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
# Windows-x86_64
|
||||
windows-x86_64:
|
||||
runs-on: windows-latest
|
||||
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: dtolnay/rust-toolchain@stable
|
||||
- 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 }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
with:
|
||||
releaseId: ${{ github.event.release.upload_url }}
|
||||
- name: Get app version (windows)
|
||||
run: |
|
||||
$json = (Get-Content "src-tauri\tauri.conf.json" -Raw) | ConvertFrom-Json
|
||||
$version = $json.package.version
|
||||
echo "Version: ${version}"
|
||||
echo "TAURI_VERSION=${version}" >> $Env:GITHUB_ENV
|
||||
echo "${Env:TAURI_VERSION}"
|
||||
shell: pwsh
|
||||
- name: Move msi
|
||||
run: Move-Item "src-tauri\target\release\bundle\msi\Cinny_${{ env.TAURI_VERSION }}_x64_en-US.msi" "src-tauri\target\release\bundle\msi\Cinny_desktop-x86_64.msi"
|
||||
shell: pwsh
|
||||
- name: Move msi.zip
|
||||
run: Move-Item "src-tauri\target\release\bundle\msi\Cinny_${{ env.TAURI_VERSION }}_x64_en-US.msi.zip" "src-tauri\target\release\bundle\msi\Cinny_desktop-x86_64.msi.zip"
|
||||
shell: pwsh
|
||||
- name: Move msi.zip.sig
|
||||
run: Move-Item "src-tauri\target\release\bundle\msi\Cinny_${{ env.TAURI_VERSION }}_x64_en-US.msi.zip.sig" "src-tauri\target\release\bundle\msi\Cinny_desktop-x86_64.msi.zip.sig"
|
||||
shell: pwsh
|
||||
- name: Upload tagged release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
with:
|
||||
files: |
|
||||
src-tauri/target/release/bundle/msi/Cinny_desktop-x86_64.msi
|
||||
src-tauri/target/release/bundle/msi/Cinny_desktop-x86_64.msi.zip
|
||||
src-tauri/target/release/bundle/msi/Cinny_desktop-x86_64.msi.zip.sig
|
||||
|
||||
# Linux-x86_64
|
||||
linux-x86_64:
|
||||
runs-on: ubuntu-22.04
|
||||
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: dtolnay/rust-toolchain@stable
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev 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 }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
with:
|
||||
releaseId: ${{ github.event.release.upload_url }}
|
||||
- name: Get app version
|
||||
id: vars
|
||||
run: echo "tag=$(jq .package.version src-tauri/tauri.conf.json | tr -d '"')" >> $GITHUB_OUTPUT
|
||||
- name: Move deb
|
||||
run: mv "src-tauri/target/release/bundle/deb/cinny_${{ steps.vars.outputs.tag }}_amd64.deb" "src-tauri/target/release/bundle/deb/Cinny_desktop-x86_64.deb"
|
||||
- name: Move AppImage
|
||||
run: mv "src-tauri/target/release/bundle/appimage/cinny_${{ steps.vars.outputs.tag }}_amd64.AppImage" "src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage"
|
||||
- name: Move AppImage.tar.gz
|
||||
run: mv "src-tauri/target/release/bundle/appimage/cinny_${{ steps.vars.outputs.tag }}_amd64.AppImage.tar.gz" "src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage.tar.gz"
|
||||
- name: Move AppImage.tar.gz.sig
|
||||
run: mv "src-tauri/target/release/bundle/appimage/cinny_${{ steps.vars.outputs.tag }}_amd64.AppImage.tar.gz.sig" "src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage.tar.gz.sig"
|
||||
- name: Upload tagged release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
with:
|
||||
files: |
|
||||
src-tauri/target/release/bundle/deb/Cinny_desktop-x86_64.deb
|
||||
src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage
|
||||
src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage.tar.gz
|
||||
src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage.tar.gz.sig
|
||||
|
||||
# macos-universal
|
||||
macos-universal:
|
||||
runs-on: macos-latest
|
||||
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: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: aarch64-apple-darwin,x86_64-apple-darwin
|
||||
- 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 }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
with:
|
||||
releaseId: ${{ github.event.release.upload_url }}
|
||||
args: "--target universal-apple-darwin"
|
||||
- name: Get app version
|
||||
id: vars
|
||||
run: echo "tag=$(jq .package.version src-tauri/tauri.conf.json | tr -d '"')" >> $GITHUB_OUTPUT
|
||||
- name: Move dmg
|
||||
run: mv "src-tauri/target/universal-apple-darwin/release/bundle/dmg/Cinny_${{ steps.vars.outputs.tag }}_universal.dmg" "src-tauri/target/universal-apple-darwin/release/bundle/dmg/Cinny_desktop-universal.dmg"
|
||||
- name: Move app.tar.gz
|
||||
run: mv "src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny.app.tar.gz" "src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny_desktop-universal.app.tar.gz"
|
||||
- name: Move app.tar.gz.sig
|
||||
run: mv "src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny.app.tar.gz.sig" "src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny_desktop-universal.app.tar.gz.sig"
|
||||
- name: Upload tagged release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
with:
|
||||
files: |
|
||||
src-tauri/target/universal-apple-darwin/release/bundle/dmg/Cinny_desktop-universal.dmg
|
||||
src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny_desktop-universal.app.tar.gz
|
||||
src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny_desktop-universal.app.tar.gz.sig
|
||||
|
||||
# Upload release.json
|
||||
release-update:
|
||||
if: always()
|
||||
needs: [windows-x86_64, linux-x86_64, macos-universal]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run release.json
|
||||
run: npm run release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
172
API-QUICKSTART.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 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
Normal file
@@ -0,0 +1,537 @@
|
||||
# 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
|
||||
34
README.md
@@ -1,39 +1,31 @@
|
||||
# Cinny desktop
|
||||
|
||||
<a href="https://github.com/cinnyapp/cinny-desktop/releases">
|
||||
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/cinnyapp/cinny-desktop/total?style=social"></a>
|
||||
|
||||
Cinny is a matrix client focusing primarily on simple, elegant and secure interface. The desktop app is made with Tauri.
|
||||
# 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.
|
||||
|
||||
## Download
|
||||
|
||||
Installers for macOS, Windows and Linux can be downloaded from [Github releases](https://github.com/cinnyapp/cinny-desktop/releases). Releases are signed with a [Ed25519](https://ed25519.cr.yp.to/) public-key.
|
||||
|
||||
Installers for Windows and Linux can be downloaded from [releases](http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases).
|
||||
|
||||
Operating System | Download
|
||||
---|---
|
||||
Windows | <a href='https://github.com/cinnyapp/cinny-desktop/releases/latest/download/Cinny_desktop-x86_64.msi'>Get it on Windows</a>
|
||||
macOS | <a href='https://github.com/cinnyapp/cinny-desktop/releases/latest/download/Cinny_desktop-universal.dmg'>Get it on macOS</a>
|
||||
Linux | <a href='https://github.com/cinnyapp/cinny-desktop/releases/latest/download/Cinny_desktop-x86_64.AppImage'>Get it on Linux</a> · <a href='https://flathub.org/apps/details/in.cinny.Cinny'>Flatpak</a>
|
||||
Windows (x64) | <a href='http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases'>Get it on Windows</a>
|
||||
Linux (AppImage) | <a href='http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases'>Get it on Linux</a>
|
||||
|
||||
Decoded public key:
|
||||
> RWRflTUQD3RHFtn25QNANCmePR9+4LSK89kAKTMEEB4OKpOFpLMgc64z
|
||||
### Linux Installation
|
||||
|
||||
To verify release files, you need to download [minisign](https://jedisct1.github.io/minisign/) tool and [decode](https://www.base64decode.org/) the *.sig* file before running:
|
||||
> minisign -Vm ***RELEASE_FILE.msi.zip*** -P RWRflTUQD3RHFtn25QNANCmePR9+4LSK89kAKTMEEB4OKpOFpLMgc64z -x ***SINGATURE.msi.zip.sig***
|
||||
For the best AppImage experience, we recommend using [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) which automatically integrates AppImages into your system.
|
||||
|
||||
## Local development
|
||||
|
||||
Firstly, to setup Rust, NodeJS and build tools follow [Tauri documentation](https://tauri.app/v1/guides/getting-started/prerequisites).
|
||||
|
||||
Now, to setup development locally run the following commands:
|
||||
* `git clone --recursive https://github.com/cinnyapp/cinny-desktop.git`
|
||||
To setup development locally run the following commands:
|
||||
* `git clone --recursive http://synbox.ruv.wtf:8418/litruv/cinny-desktop.git`
|
||||
* `cd cinny-desktop/cinny`
|
||||
* `npm ci`
|
||||
* `cd ..`
|
||||
* `npm ci`
|
||||
|
||||
To build the app locally, run:
|
||||
* `npm run tauri build`
|
||||
* `npm run build`
|
||||
|
||||
To start local dev server, run:
|
||||
* `npm run tauri dev`
|
||||
* `npm run dev`
|
||||
|
||||
BIN
appicon-square.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
appicon.png
Normal file
|
After Width: | Height: | Size: 730 KiB |
20
build-resources/linux/after-install.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Post-installation script for .deb and .rpm packages
|
||||
|
||||
# Update desktop database for the .desktop file
|
||||
if command -v update-desktop-database &> /dev/null; then
|
||||
update-desktop-database -q /usr/share/applications || true
|
||||
fi
|
||||
|
||||
# Update icon cache
|
||||
if command -v gtk-update-icon-cache &> /dev/null; then
|
||||
gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true
|
||||
fi
|
||||
|
||||
# Set appropriate permissions for chrome-sandbox (required for Electron)
|
||||
SANDBOX_PATH="/opt/Paarrot/chrome-sandbox"
|
||||
if [ -f "$SANDBOX_PATH" ]; then
|
||||
chmod 4755 "$SANDBOX_PATH" || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
14
build-resources/linux/after-remove.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Post-removal script for .deb and .rpm packages
|
||||
|
||||
# Update desktop database after removal
|
||||
if command -v update-desktop-database &> /dev/null; then
|
||||
update-desktop-database -q /usr/share/applications || true
|
||||
fi
|
||||
|
||||
# Update icon cache
|
||||
if command -v gtk-update-icon-cache &> /dev/null; then
|
||||
gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
2
cinny
94
electron-builder.json5
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"appId": "com.paarrot.app",
|
||||
"productName": "Paarrot",
|
||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
|
||||
"copyright": "Copyright © 2026 Mates.Media",
|
||||
"directories": {
|
||||
"output": "dist-electron",
|
||||
"buildResources": "build-resources"
|
||||
},
|
||||
"files": [
|
||||
"electron/**/*",
|
||||
"cinny/dist/**/*",
|
||||
"!**/*.map",
|
||||
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
|
||||
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
|
||||
"!**/node_modules/*.d.ts",
|
||||
"!**/node_modules/.bin",
|
||||
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
|
||||
"!.editorconfig",
|
||||
"!**/._*",
|
||||
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
|
||||
"!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
|
||||
"!**/{appveyor.yml,.travis.yml,circle.yml}",
|
||||
"!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "icons",
|
||||
"to": "icons",
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"category": "Network;InstantMessaging",
|
||||
"maintainer": "Mates.Media <mates.media@users.noreply.github.com>",
|
||||
"icon": "icons/icon.png",
|
||||
"desktop": {
|
||||
"entry": {
|
||||
"Name": "Paarrot",
|
||||
"GenericName": "Matrix Client",
|
||||
"Comment": "A Matrix client built with Cinny",
|
||||
"Categories": "Network;InstantMessaging;",
|
||||
"Keywords": "matrix;chat;messaging;",
|
||||
"StartupWMClass": "paarrot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"libnotify4",
|
||||
"libxtst6",
|
||||
"libnss3"
|
||||
],
|
||||
"afterInstall": "build-resources/linux/after-install.sh",
|
||||
"afterRemove": "build-resources/linux/after-remove.sh"
|
||||
},
|
||||
"rpm": {
|
||||
"depends": [
|
||||
"libnotify",
|
||||
"libXtst",
|
||||
"nss"
|
||||
],
|
||||
"afterInstall": "build-resources/linux/after-install.sh",
|
||||
"afterRemove": "build-resources/linux/after-remove.sh"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "squirrel",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"icon": "icons/icon.ico",
|
||||
"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
|
||||
},
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases/download/v${version}",
|
||||
"channel": "latest"
|
||||
}
|
||||
}
|
||||
267
electron/api-server.js
Normal file
@@ -0,0 +1,267 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
class PaarrotAPIServer {
|
||||
constructor(mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.app = express();
|
||||
this.server = null;
|
||||
this.port = 33384; // Default port for Paarrot API
|
||||
|
||||
this.setupMiddleware();
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
setupMiddleware() {
|
||||
// Enable CORS for all origins (you can restrict this if needed)
|
||||
this.app.use(cors());
|
||||
|
||||
// Parse JSON bodies
|
||||
this.app.use(bodyParser.json());
|
||||
|
||||
// Log all requests
|
||||
this.app.use((req, res, next) => {
|
||||
console.log(`Paarrot API: ${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
// Health check
|
||||
this.app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', app: 'Paarrot API', version: '1.0.0' });
|
||||
});
|
||||
|
||||
// Get current status
|
||||
this.app.get('/status', async (req, res) => {
|
||||
try {
|
||||
const status = await this.executeAction('get-status');
|
||||
res.json({ success: true, data: status });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Mute/unmute
|
||||
this.app.post('/mute', async (req, res) => {
|
||||
try {
|
||||
const { muted } = req.body;
|
||||
const result = await this.executeAction('set-mute', { muted });
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle mute
|
||||
this.app.post('/mute/toggle', async (req, res) => {
|
||||
try {
|
||||
const result = await this.executeAction('toggle-mute');
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Deafen/undeafen
|
||||
this.app.post('/deafen', async (req, res) => {
|
||||
try {
|
||||
const { deafened } = req.body;
|
||||
const result = await this.executeAction('set-deafen', { deafened });
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle deafen
|
||||
this.app.post('/deafen/toggle', async (req, res) => {
|
||||
try {
|
||||
const result = await this.executeAction('toggle-deafen');
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Change channel/room
|
||||
this.app.post('/channel', async (req, res) => {
|
||||
try {
|
||||
const { roomId } = req.body;
|
||||
if (!roomId) {
|
||||
return res.status(400).json({ success: false, error: 'roomId is required' });
|
||||
}
|
||||
const result = await this.executeAction('change-channel', { roomId });
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get list of rooms/channels
|
||||
this.app.get('/channels', async (req, res) => {
|
||||
try {
|
||||
const result = await this.executeAction('get-channels');
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Send message
|
||||
this.app.post('/message', async (req, res) => {
|
||||
try {
|
||||
const { roomId, message } = req.body;
|
||||
if (!roomId || !message) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'roomId and message are required'
|
||||
});
|
||||
}
|
||||
const result = await this.executeAction('send-message', { roomId, message });
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Send message to current room
|
||||
this.app.post('/message/current', async (req, res) => {
|
||||
try {
|
||||
const { message } = req.body;
|
||||
if (!message) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'message is required'
|
||||
});
|
||||
}
|
||||
const result = await this.executeAction('send-message-current', { message });
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get current room info
|
||||
this.app.get('/room/current', async (req, res) => {
|
||||
try {
|
||||
const result = await this.executeAction('get-current-room');
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
this.app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Endpoint not found',
|
||||
availableEndpoints: [
|
||||
'GET /health',
|
||||
'GET /status',
|
||||
'POST /mute',
|
||||
'POST /mute/toggle',
|
||||
'POST /deafen',
|
||||
'POST /deafen/toggle',
|
||||
'POST /channel',
|
||||
'GET /channels',
|
||||
'POST /message',
|
||||
'POST /message/current',
|
||||
'GET /room/current'
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// Error handler
|
||||
this.app.use((err, req, res, next) => {
|
||||
console.error('Paarrot API Error:', err);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an action by sending it to the renderer process
|
||||
* @param {string} action - The action to execute
|
||||
* @param {object} params - Parameters for the action
|
||||
* @returns {Promise<any>} - Result from the renderer
|
||||
*/
|
||||
executeAction(action, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.mainWindow || this.mainWindow.isDestroyed()) {
|
||||
return reject(new Error('Main window is not available'));
|
||||
}
|
||||
|
||||
// Create a unique response channel
|
||||
const responseChannel = `api-response-${Date.now()}-${Math.random()}`;
|
||||
|
||||
// Set up one-time listener for the response
|
||||
const { ipcMain } = require('electron');
|
||||
const timeout = setTimeout(() => {
|
||||
ipcMain.removeHandler(responseChannel);
|
||||
reject(new Error('Action timed out'));
|
||||
}, 10000); // 10 second timeout
|
||||
|
||||
ipcMain.handleOnce(responseChannel, async (event, result) => {
|
||||
clearTimeout(timeout);
|
||||
if (result.success) {
|
||||
resolve(result.data);
|
||||
} else {
|
||||
reject(new Error(result.error || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
|
||||
// Send the action to the renderer
|
||||
this.mainWindow.webContents.send('api-action', {
|
||||
action,
|
||||
params,
|
||||
responseChannel
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
start(port = this.port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = this.app.listen(port, '127.0.0.1', () => {
|
||||
console.log(`Paarrot API server listening on http://127.0.0.1:${port}`);
|
||||
console.log('Available endpoints:');
|
||||
console.log(' GET /health');
|
||||
console.log(' GET /status');
|
||||
console.log(' POST /mute');
|
||||
console.log(' POST /mute/toggle');
|
||||
console.log(' POST /deafen');
|
||||
console.log(' POST /deafen/toggle');
|
||||
console.log(' POST /channel');
|
||||
console.log(' GET /channels');
|
||||
console.log(' POST /message');
|
||||
console.log(' POST /message/current');
|
||||
console.log(' GET /room/current');
|
||||
resolve(port);
|
||||
}).on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`Paarrot API: Port ${port} is already in use`);
|
||||
reject(new Error(`Port ${port} is already in use`));
|
||||
} else {
|
||||
console.error('Paarrot API: Failed to start server:', err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
return new Promise((resolve) => {
|
||||
if (this.server) {
|
||||
this.server.close(() => {
|
||||
console.log('Paarrot API server stopped');
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PaarrotAPIServer;
|
||||
835
electron/main.js
Normal file
@@ -0,0 +1,835 @@
|
||||
const { app, BrowserWindow, ipcMain, shell, Tray, Menu, nativeImage, clipboard, session, desktopCapturer } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const Store = require('electron-store');
|
||||
const open = require('open');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const PaarrotAPIServer = require('./api-server');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// Handle Squirrel events for Windows installer
|
||||
if (require('electron-squirrel-startup')) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
// Handle Squirrel.Windows installation/update events
|
||||
if (process.platform === 'win32') {
|
||||
const handleSquirrelEvent = () => {
|
||||
if (process.argv.length === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const squirrelEvent = process.argv[1];
|
||||
|
||||
switch (squirrelEvent) {
|
||||
case '--squirrel-install':
|
||||
case '--squirrel-updated':
|
||||
// Install desktop and start menu shortcuts
|
||||
const cp = require('child_process');
|
||||
const updateDotExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe');
|
||||
const target = path.basename(process.execPath);
|
||||
const createShortcut = `${updateDotExe} --createShortcut=${target}`;
|
||||
|
||||
cp.exec(createShortcut, () => {
|
||||
app.quit();
|
||||
});
|
||||
return true;
|
||||
|
||||
case '--squirrel-uninstall':
|
||||
// Remove desktop and start menu shortcuts
|
||||
const cpUninstall = require('child_process');
|
||||
const updateDotExeUninstall = path.resolve(path.dirname(process.execPath), '..', 'Update.exe');
|
||||
const targetUninstall = path.basename(process.execPath);
|
||||
const removeShortcut = `${updateDotExeUninstall} --removeShortcut=${targetUninstall}`;
|
||||
|
||||
cpUninstall.exec(removeShortcut, () => {
|
||||
app.quit();
|
||||
});
|
||||
return true;
|
||||
|
||||
case '--squirrel-obsolete':
|
||||
// This is called on the outgoing version of your app before
|
||||
// we update to the new version - it's the opposite of
|
||||
// --squirrel-updated
|
||||
app.quit();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (handleSquirrelEvent()) {
|
||||
// Squirrel event handled and app will exit in 1000ms, so don't do anything else
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const store = new Store();
|
||||
|
||||
let mainWindow = null;
|
||||
let tray = null;
|
||||
let isQuitting = false;
|
||||
let apiServer = null;
|
||||
|
||||
// Set app name for notifications and system tray
|
||||
app.setName('Paarrot');
|
||||
|
||||
// Configure auto-updater
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
// Gitea update configuration
|
||||
const GITEA_API_BASE = 'http://synbox.ruv.wtf:8418/api/v1';
|
||||
const GITEA_REPO = 'litruv/cinny-desktop';
|
||||
|
||||
// Function to fetch latest release from Gitea and configure update feed
|
||||
async function configureGiteaUpdateFeed() {
|
||||
try {
|
||||
const url = `${GITEA_API_BASE}/repos/${GITEA_REPO}/releases?limit=1`;
|
||||
console.log('Fetching latest release from:', url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const releases = JSON.parse(data);
|
||||
if (releases && releases.length > 0) {
|
||||
const latestRelease = releases[0];
|
||||
const version = latestRelease.tag_name.replace(/^v/, '');
|
||||
const downloadUrl = `http://synbox.ruv.wtf:8418/${GITEA_REPO}/releases/download/${latestRelease.tag_name}`;
|
||||
|
||||
console.log('Latest release version:', version);
|
||||
console.log('Download URL:', downloadUrl);
|
||||
|
||||
// Configure auto-updater with the versioned release URL
|
||||
autoUpdater.setFeedURL({
|
||||
provider: 'generic',
|
||||
url: downloadUrl,
|
||||
channel: 'latest'
|
||||
});
|
||||
|
||||
resolve({ version, url: downloadUrl });
|
||||
} else {
|
||||
reject(new Error('No releases found'));
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to configure Gitea update feed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-updater event handlers
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
console.log('Checking for updates...');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
console.log('Update available:', info.version);
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('update-available', info);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
console.log('No updates available');
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
console.error('Auto-updater error:', err);
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progressObj) => {
|
||||
console.log(`Download progress: ${progressObj.percent}%`);
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('update-download-progress', progressObj);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
console.log('Update downloaded:', info.version);
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('update-downloaded', info);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Single instance lock
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', () => {
|
||||
// Someone tried to run a second instance, focus our window
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Development mode detection
|
||||
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||
const VITE_DEV_SERVER = 'http://localhost:8080';
|
||||
const PORT = 44548;
|
||||
|
||||
// Helper to get correct icon path in dev vs packaged app
|
||||
function getIconPath(iconName) {
|
||||
if (isDev) {
|
||||
return path.join(__dirname, '../icons', iconName);
|
||||
}
|
||||
// In packaged app, extraResources puts icons in resources/icons/
|
||||
return path.join(process.resourcesPath, 'icons', iconName);
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Restore window state or use defaults
|
||||
const windowState = store.get('windowState', {
|
||||
width: 1280,
|
||||
height: 905,
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
isMaximized: false
|
||||
});
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: windowState.width,
|
||||
height: windowState.height,
|
||||
x: windowState.x,
|
||||
y: windowState.y,
|
||||
title: 'Paarrot',
|
||||
frame: false, // Custom window decorations
|
||||
resizable: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
// Enable navigation gestures (works on macOS and some Linux environments)
|
||||
enableBlinkFeatures: 'OverscrollHistoryNavigation'
|
||||
},
|
||||
icon: getIconPath(process.platform === 'win32' ? 'icon.ico' : 'icon.png'),
|
||||
show: false // Don't show until ready
|
||||
});
|
||||
|
||||
// Enable trackpad swipe gestures for navigation on macOS
|
||||
if (process.platform === 'darwin') {
|
||||
try {
|
||||
mainWindow.on('swipe', (event, direction) => {
|
||||
console.log(`Paarrot: Swipe gesture detected: ${direction}`);
|
||||
if (direction === 'left' && mainWindow.webContents.canGoForward()) {
|
||||
console.log('Paarrot: Navigating forward');
|
||||
mainWindow.webContents.goForward();
|
||||
} else if (direction === 'right' && mainWindow.webContents.canGoBack()) {
|
||||
console.log('Paarrot: Navigating back');
|
||||
mainWindow.webContents.goBack();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Paarrot: Failed to set up swipe gestures:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// On Linux, the OverscrollHistoryNavigation Blink feature should handle it automatically
|
||||
// On Windows, mouse buttons 4 & 5 are typically used for navigation (handled by Chromium)
|
||||
|
||||
// Restore maximized state
|
||||
if (windowState.isMaximized) {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
|
||||
// Save window state on resize/move
|
||||
const saveWindowState = () => {
|
||||
if (!mainWindow.isMaximized() && !mainWindow.isMinimized() && !mainWindow.isFullScreen()) {
|
||||
const bounds = mainWindow.getBounds();
|
||||
store.set('windowState', {
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
isMaximized: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
mainWindow.on('resize', saveWindowState);
|
||||
mainWindow.on('move', saveWindowState);
|
||||
mainWindow.on('maximize', () => {
|
||||
store.set('windowState.isMaximized', true);
|
||||
});
|
||||
mainWindow.on('unmaximize', () => {
|
||||
store.set('windowState.isMaximized', false);
|
||||
});
|
||||
|
||||
// Set up session handlers BEFORE loading the app
|
||||
// Auto-approve media permissions including screen capture
|
||||
mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||
const allowedPermissions = ['media', 'microphone', 'camera', 'display-capture', 'notifications'];
|
||||
|
||||
if (allowedPermissions.includes(permission)) {
|
||||
console.log(`Paarrot: Auto-approving permission: ${permission}`);
|
||||
callback(true);
|
||||
} else {
|
||||
console.log(`Paarrot: Denying permission: ${permission}`);
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle screen capture requests - always allow
|
||||
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
console.log(`Paarrot: Permission check - ${permission} from ${requestingOrigin}`);
|
||||
return true; // Allow all permissions
|
||||
});
|
||||
|
||||
// Handle display media request (screen sharing) - this is the key for Electron screen capture
|
||||
mainWindow.webContents.session.setDisplayMediaRequestHandler(async (request, callback) => {
|
||||
console.log('Paarrot: Display media request received', request);
|
||||
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen', 'window'],
|
||||
thumbnailSize: { width: 150, height: 150 }
|
||||
});
|
||||
|
||||
console.log('Paarrot: Available sources:', sources.length);
|
||||
sources.forEach((s, i) => console.log(` ${i}: ${s.name} (${s.id})`));
|
||||
|
||||
if (sources.length === 0) {
|
||||
console.log('Paarrot: No sources available, denying request');
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer screens over windows
|
||||
const source = sources.find(s => s.id.startsWith('screen:')) || sources[0];
|
||||
console.log('Paarrot: Selected source:', source.name, source.id);
|
||||
|
||||
// Return the selected source - video is required
|
||||
callback({ video: source });
|
||||
} catch (error) {
|
||||
console.error('Paarrot: Error getting display sources:', error);
|
||||
callback({});
|
||||
}
|
||||
}, { useSystemPicker: false });
|
||||
|
||||
// Disable default context menu, only show for text selection and images
|
||||
mainWindow.webContents.on('context-menu', (event, params) => {
|
||||
const { selectionText, isEditable, mediaType, srcURL } = params;
|
||||
|
||||
// Show context menu for text selection, editable fields, or images
|
||||
if (selectionText || isEditable || mediaType === 'image') {
|
||||
// Build a minimal menu for text operations
|
||||
const template = [];
|
||||
|
||||
if (params.misspelledWord) {
|
||||
// Add spelling suggestions
|
||||
params.dictionarySuggestions.slice(0, 5).forEach(suggestion => {
|
||||
template.push({
|
||||
label: suggestion,
|
||||
click: () => mainWindow.webContents.replaceMisspelling(suggestion)
|
||||
});
|
||||
});
|
||||
if (template.length > 0) {
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
}
|
||||
|
||||
// Image context menu options
|
||||
if (mediaType === 'image' && srcURL) {
|
||||
template.push(
|
||||
{
|
||||
label: 'Copy Image',
|
||||
click: () => {
|
||||
mainWindow.webContents.copyImageAt(params.x, params.y);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Save Image As...',
|
||||
click: () => {
|
||||
mainWindow.webContents.downloadURL(srcURL);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (selectionText) {
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionText) {
|
||||
template.push(
|
||||
{
|
||||
label: 'Copy',
|
||||
role: 'copy',
|
||||
accelerator: 'CmdOrCtrl+C'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (isEditable) {
|
||||
if (selectionText) {
|
||||
template.push(
|
||||
{
|
||||
label: 'Cut',
|
||||
role: 'cut',
|
||||
accelerator: 'CmdOrCtrl+X'
|
||||
}
|
||||
);
|
||||
}
|
||||
template.push(
|
||||
{
|
||||
label: 'Paste',
|
||||
role: 'paste',
|
||||
accelerator: 'CmdOrCtrl+V'
|
||||
}
|
||||
);
|
||||
if (selectionText) {
|
||||
template.push({ type: 'separator' });
|
||||
template.push(
|
||||
{
|
||||
label: 'Select All',
|
||||
role: 'selectAll',
|
||||
accelerator: 'CmdOrCtrl+A'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (template.length > 0) {
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
menu.popup(mainWindow);
|
||||
}
|
||||
}
|
||||
// If no text selected, not editable, and not an image, suppress the context menu entirely
|
||||
});
|
||||
|
||||
// Load the app
|
||||
if (isDev) {
|
||||
mainWindow.loadURL(VITE_DEV_SERVER);
|
||||
// Open DevTools in development
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
// In production, serve from built files
|
||||
mainWindow.loadFile(path.join(__dirname, '../cinny/dist/index.html'));
|
||||
}
|
||||
|
||||
// Show window when ready
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
// Check for --minimized flag (autostart)
|
||||
if (!process.argv.includes('--minimized')) {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window close - minimize to tray
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!isQuitting) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Open external links in default browser
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url, features }) => {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
}
|
||||
// Allow blank windows for screen share pop-outs with custom features
|
||||
if (url === '' || url === 'about:blank') {
|
||||
return {
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: {
|
||||
frame: false,
|
||||
resizable: true,
|
||||
backgroundColor: '#000000',
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
backgroundThrottling: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return { action: 'allow' };
|
||||
});
|
||||
|
||||
// Navigation handler - open external links in browser
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
const allowedOrigins = [
|
||||
'http://localhost:8080',
|
||||
'http://localhost:44548',
|
||||
'http://127.0.0.1:8080',
|
||||
'http://127.0.0.1:44548'
|
||||
];
|
||||
|
||||
const isAllowed = allowedOrigins.some(origin => url.startsWith(origin)) ||
|
||||
url.startsWith('blob:') ||
|
||||
url.startsWith('data:');
|
||||
|
||||
if (!isAllowed && (url.startsWith('http://') || url.startsWith('https://'))) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
}
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
const iconPath = getIconPath('icon.png');
|
||||
const trayIcon = nativeImage.createFromPath(iconPath);
|
||||
tray = new Tray(trayIcon.resize({ width: 16, height: 16 }));
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show Paarrot',
|
||||
click: () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => {
|
||||
isQuitting = true;
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
tray.setContextMenu(contextMenu);
|
||||
tray.setToolTip('Paarrot');
|
||||
|
||||
// Click tray icon to show window
|
||||
tray.on('click', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// App lifecycle
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
createTray();
|
||||
|
||||
// Start API server
|
||||
apiServer = new PaarrotAPIServer(mainWindow);
|
||||
apiServer.start().catch(err => {
|
||||
console.error('Failed to start API server:', err);
|
||||
});
|
||||
|
||||
// Check for updates (not in development mode)
|
||||
if (!isDev) {
|
||||
// Check for updates on start (after 3 seconds)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await configureGiteaUpdateFeed();
|
||||
await autoUpdater.checkForUpdates();
|
||||
} catch (err) {
|
||||
console.error('Failed to check for updates:', err);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// Check for updates every 6 hours
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await configureGiteaUpdateFeed();
|
||||
await autoUpdater.checkForUpdates();
|
||||
} catch (err) {
|
||||
console.error('Failed to check for updates:', err);
|
||||
}
|
||||
}, 6 * 60 * 60 * 1000);
|
||||
}
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS, apps stay active until Cmd+Q
|
||||
if (process.platform !== 'darwin') {
|
||||
// But we want to stay in tray, so don't quit
|
||||
// app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS, recreate window when dock icon is clicked
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
} else if (mainWindow) {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
|
||||
// Stop API server
|
||||
if (apiServer) {
|
||||
apiServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== IPC Handlers ====================
|
||||
|
||||
// Window controls
|
||||
ipcMain.handle('window:minimize', () => {
|
||||
if (mainWindow) mainWindow.minimize();
|
||||
});
|
||||
|
||||
ipcMain.handle('window:maximize', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window:unmaximize', () => {
|
||||
if (mainWindow) mainWindow.unmaximize();
|
||||
});
|
||||
|
||||
ipcMain.handle('window:close', () => {
|
||||
if (mainWindow) mainWindow.close();
|
||||
});
|
||||
|
||||
ipcMain.handle('window:focus', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window:is-maximized', () => {
|
||||
return mainWindow ? mainWindow.isMaximized() : false;
|
||||
});
|
||||
|
||||
ipcMain.handle('window:start-drag', () => {
|
||||
if (mainWindow) mainWindow.webContents.startDrag({ file: '', icon: nativeImage.createEmpty() });
|
||||
});
|
||||
|
||||
// Get app version (Tauri-compatible API)
|
||||
ipcMain.handle('plugin:app|version', () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
// Open external URL
|
||||
ipcMain.handle('open-external-url', async (event, url) => {
|
||||
try {
|
||||
await shell.openExternal(url);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Read clipboard image
|
||||
ipcMain.handle('read-clipboard-image', async () => {
|
||||
try {
|
||||
const image = clipboard.readImage();
|
||||
if (image.isEmpty()) {
|
||||
return { success: true, data: null };
|
||||
}
|
||||
|
||||
// Convert to PNG base64
|
||||
const pngBuffer = image.toPNG();
|
||||
const base64 = pngBuffer.toString('base64');
|
||||
return {
|
||||
success: true,
|
||||
data: `data:image/png;base64,${base64}`
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Write text to clipboard
|
||||
ipcMain.handle('write-clipboard-text', async (event, text) => {
|
||||
try {
|
||||
clipboard.writeText(text);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Get YouTube stream using yt-dlp
|
||||
ipcMain.handle('get-youtube-stream', async (event, url) => {
|
||||
try {
|
||||
// Check if yt-dlp is available
|
||||
try {
|
||||
await execAsync('yt-dlp --version');
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: 'yt-dlp is not installed. Please install yt-dlp to use YouTube features.'
|
||||
};
|
||||
}
|
||||
|
||||
// Get title
|
||||
let title = 'YouTube Video';
|
||||
try {
|
||||
const titleResult = await execAsync(`yt-dlp --get-title "${url}"`);
|
||||
title = titleResult.stdout.trim();
|
||||
} catch (e) {
|
||||
console.warn('Failed to get video title:', e.message);
|
||||
}
|
||||
|
||||
// Get video URL
|
||||
const result = await execAsync(
|
||||
`yt-dlp -g -f "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best" "${url}"`
|
||||
);
|
||||
|
||||
const videoUrl = result.stdout.trim().split('\n')[0];
|
||||
|
||||
if (!videoUrl) {
|
||||
return { success: false, error: 'yt-dlp returned empty URL' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { video_url: videoUrl, title }
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `yt-dlp error: ${error.message}`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Background sync stubs (desktop doesn't need it)
|
||||
ipcMain.handle('start-background-sync', async () => {
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('stop-background-sync', async () => {
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('get-background-sync-state', async () => {
|
||||
return { success: true, data: 'NotApplicable' };
|
||||
});
|
||||
|
||||
// Auto-updater IPC handlers
|
||||
ipcMain.handle('check-for-updates', async () => {
|
||||
if (isDev) {
|
||||
return { success: false, error: 'Updates are not available in development mode' };
|
||||
}
|
||||
try {
|
||||
// First, fetch latest release info from Gitea and configure feed URL
|
||||
await configureGiteaUpdateFeed();
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('download-update', async () => {
|
||||
if (isDev) {
|
||||
return { success: false, error: 'Updates are not available in development mode' };
|
||||
}
|
||||
try {
|
||||
await autoUpdater.downloadUpdate();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('install-update', () => {
|
||||
if (isDev) {
|
||||
return { success: false, error: 'Updates are not available in development mode' };
|
||||
}
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Screen capture / desktopCapturer handler
|
||||
ipcMain.handle('get-desktop-sources', async (event, opts) => {
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources(opts);
|
||||
return { success: true, data: sources };
|
||||
} catch (error) {
|
||||
console.error('Failed to get desktop sources:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Desktop notification handler
|
||||
ipcMain.handle('show-notification', async (event, { title, body, icon, roomId, eventId }) => {
|
||||
try {
|
||||
const { Notification } = require('electron');
|
||||
|
||||
if (!Notification.isSupported()) {
|
||||
console.error('Notifications not supported on this system');
|
||||
return { success: false, error: 'Notifications not supported on this system' };
|
||||
}
|
||||
|
||||
const notification = new Notification({
|
||||
title: title || 'Paarrot',
|
||||
body: body || '',
|
||||
icon: icon || getIconPath(process.platform === 'win32' ? 'icon.ico' : 'icon.png'),
|
||||
silent: false,
|
||||
});
|
||||
|
||||
notification.on('click', () => {
|
||||
console.log('Paarrot: Notification clicked, roomId:', roomId);
|
||||
if (mainWindow) {
|
||||
console.log('Paarrot: Sending navigation to renderer');
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
if (roomId) {
|
||||
mainWindow.webContents.send('notification:navigate', { roomId, eventId });
|
||||
console.log('Paarrot: Navigation event sent');
|
||||
}
|
||||
} else {
|
||||
console.log('Paarrot: No mainWindow available');
|
||||
}
|
||||
});
|
||||
|
||||
notification.on('show', () => console.log('Paarrot: Notification shown'));
|
||||
notification.on('failed', (e, err) => console.error('Paarrot: Notification failed:', err));
|
||||
|
||||
notification.show();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to show notification:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Paarrot Electron main process started');
|
||||
console.log('Development mode:', isDev);
|
||||
console.log('Platform:', process.platform);
|
||||
168
electron/preload.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Map command names to Electron IPC channels
|
||||
const commandMap = {
|
||||
'window_minimize': 'window:minimize',
|
||||
'window_maximize': 'window:maximize',
|
||||
'window_unmaximize': 'window:unmaximize',
|
||||
'window_close': 'window:close',
|
||||
'window_is_maximized': 'window:is-maximized',
|
||||
'window_start_drag': 'window:start-drag',
|
||||
'read_clipboard_image': 'read-clipboard-image',
|
||||
'write_clipboard_text': 'write-clipboard-text',
|
||||
'open_external_url': 'open-external-url',
|
||||
'get_youtube_stream': 'get-youtube-stream',
|
||||
'start_background_sync': 'start-background-sync',
|
||||
'stop_background_sync': 'stop-background-sync',
|
||||
'get_background_sync_state': 'get-background-sync-state',
|
||||
'check_for_updates': 'check-for-updates',
|
||||
'download_update': 'download-update',
|
||||
'install_update': 'install-update',
|
||||
'get_desktop_sources': 'get-desktop-sources'
|
||||
};
|
||||
|
||||
// Shared invoke function for legacy API compatibility
|
||||
const invokeHandler = async (command, args = {}) => {
|
||||
try {
|
||||
const channel = commandMap[command] || command;
|
||||
const result = await ipcRenderer.invoke(channel, args);
|
||||
|
||||
// Return response in expected format
|
||||
if (result && result.success === false) {
|
||||
throw new Error(result.error || 'Unknown error');
|
||||
}
|
||||
|
||||
// For commands that return data in a 'data' field
|
||||
if (result && result.data !== undefined) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// For simple commands that just return success/failure
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`[Electron IPC] Error invoking ${command}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// ipcRenderer without exposing the entire object
|
||||
contextBridge.exposeInMainWorld('__TAURI_INTERNALS__', {
|
||||
// Legacy API compatibility layer for existing frontend code
|
||||
metadata: {
|
||||
currentVersion: {
|
||||
version: '2.0.0',
|
||||
platform: process.platform,
|
||||
arch: process.arch
|
||||
}
|
||||
},
|
||||
// Add invoke function for legacy plugins
|
||||
invoke: invokeHandler,
|
||||
// Add stub for transformCallback to prevent errors with legacy plugins
|
||||
transformCallback: (callback) => {
|
||||
return callback;
|
||||
}
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('__TAURI__', {
|
||||
// Core IPC invoke - legacy API
|
||||
invoke: invokeHandler,
|
||||
|
||||
// Event listeners (if needed for future events)
|
||||
listen: (event, handler) => {
|
||||
const subscription = (_, ...args) => handler(...args);
|
||||
ipcRenderer.on(event, subscription);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
ipcRenderer.removeListener(event, subscription);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Also expose under electron namespace for direct Electron APIs
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
versions: process.versions,
|
||||
|
||||
// Window controls
|
||||
window: {
|
||||
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||
maximize: () => ipcRenderer.invoke('window:maximize'),
|
||||
unmaximize: () => ipcRenderer.invoke('window:unmaximize'),
|
||||
close: () => ipcRenderer.invoke('window:close'),
|
||||
isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
|
||||
startDrag: () => ipcRenderer.invoke('window:start-drag'),
|
||||
focus: () => ipcRenderer.invoke('window:focus')
|
||||
},
|
||||
|
||||
// Clipboard
|
||||
clipboard: {
|
||||
readImage: () => ipcRenderer.invoke('read-clipboard-image'),
|
||||
writeText: (text) => ipcRenderer.invoke('write-clipboard-text', text)
|
||||
},
|
||||
|
||||
// External URLs
|
||||
shell: {
|
||||
openExternal: (url) => ipcRenderer.invoke('open-external-url', url)
|
||||
},
|
||||
|
||||
// YouTube
|
||||
youtube: {
|
||||
getStream: (url) => ipcRenderer.invoke('get-youtube-stream', url)
|
||||
},
|
||||
|
||||
// Background sync (stubs for desktop)
|
||||
sync: {
|
||||
start: (args) => ipcRenderer.invoke('start-background-sync', args),
|
||||
stop: () => ipcRenderer.invoke('stop-background-sync'),
|
||||
getState: () => ipcRenderer.invoke('get-background-sync-state')
|
||||
},
|
||||
|
||||
// Auto-updater
|
||||
updater: {
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||
onUpdateAvailable: (callback) => {
|
||||
ipcRenderer.on('update-available', (_, info) => callback(info));
|
||||
},
|
||||
onUpdateDownloadProgress: (callback) => {
|
||||
ipcRenderer.on('update-download-progress', (_, progress) => callback(progress));
|
||||
},
|
||||
onUpdateDownloaded: (callback) => {
|
||||
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
||||
}
|
||||
},
|
||||
|
||||
// Desktop capturer (for screen sharing)
|
||||
desktopCapturer: {
|
||||
getSources: (opts) => ipcRenderer.invoke('get-desktop-sources', opts)
|
||||
},
|
||||
|
||||
// API action handler (for external API server)
|
||||
api: {
|
||||
onAction: (callback) => {
|
||||
const handler = (_, data) => callback(data);
|
||||
ipcRenderer.on('api-action', handler);
|
||||
return () => ipcRenderer.removeListener('api-action', handler);
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Preload script ready
|
||||
|
||||
BIN
icons/icon.ico
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
icons/icon.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
274
paarrot-api.postman_collection.json
Normal file
@@ -0,0 +1,274 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "paarrot-api-collection",
|
||||
"name": "Paarrot API",
|
||||
"description": "API endpoints for controlling Paarrot (Cinny Desktop) features like muting, deafening, channel navigation, and messaging.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "paarrot-api"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Health & Status",
|
||||
"item": [
|
||||
{
|
||||
"name": "Health Check",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/health",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"health"
|
||||
]
|
||||
},
|
||||
"description": "Check if the API server is running and healthy."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Status",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/status",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"description": "Get current application status including mute/deafen state, current room, connection status, and user ID."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Audio Controls",
|
||||
"item": [
|
||||
{
|
||||
"name": "Toggle Mute",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/mute/toggle",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"mute",
|
||||
"toggle"
|
||||
]
|
||||
},
|
||||
"description": "Toggle microphone mute state. Works only when in an active call."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Set Mute",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"muted\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/mute",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"mute"
|
||||
]
|
||||
},
|
||||
"description": "Set microphone mute to a specific state (true/false). Works only when in an active call."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Toggle Deafen",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/deafen/toggle",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"deafen",
|
||||
"toggle"
|
||||
]
|
||||
},
|
||||
"description": "Toggle deafen state (mutes all incoming audio and your microphone). Works only when in an active call."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Set Deafen",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"deafened\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/deafen",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"deafen"
|
||||
]
|
||||
},
|
||||
"description": "Set deafen to a specific state (true/false). Works only when in an active call."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Rooms & Channels",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get Channels",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/channels",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"channels"
|
||||
]
|
||||
},
|
||||
"description": "Get list of all available rooms/channels with their details (roomId, name, isDirect, avatar)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Current Room",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/room/current",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"room",
|
||||
"current"
|
||||
]
|
||||
},
|
||||
"description": "Get information about the currently active room."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Change Channel",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"roomId\": \"!YourRoomId:server.com\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/channel",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"channel"
|
||||
]
|
||||
},
|
||||
"description": "Navigate to a different room/channel by providing its roomId."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Messaging",
|
||||
"item": [
|
||||
{
|
||||
"name": "Send Message to Room",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"roomId\": \"!YourRoomId:server.com\",\n \"message\": \"Hello from Postman!\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/message",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"message"
|
||||
]
|
||||
},
|
||||
"description": "Send a text message to a specific room by providing roomId and message content."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Send Message to Current Room",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"message\": \"Hello from Postman!\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/message/current",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"message",
|
||||
"current"
|
||||
]
|
||||
},
|
||||
"description": "Send a text message to the currently active room."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "http://127.0.0.1:33384",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
paarrot.afdesign
Normal file
6984
package-lock.json
generated
42
package.json
@@ -1,24 +1,46 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "4.10.2",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"name": "paarrot",
|
||||
"version": "4.11.42",
|
||||
"description": "Paarrot - A Matrix client based on Cinny",
|
||||
"homepage": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop",
|
||||
"main": "electron/main.js",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"tauri": "powershell Copy-Item config.json cinny/; tauri",
|
||||
"release": "node scripts/release.mjs"
|
||||
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"",
|
||||
"dev:vite": "cd cinny && npm start",
|
||||
"dev:electron": "wait-on http://localhost:8080 && 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 never",
|
||||
"build:linux": "npm run build -- --linux",
|
||||
"build:win": "npm run build -- --win",
|
||||
"postman:generate": "node scripts/generate-postman-collection.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Ajay Bura",
|
||||
"author": {
|
||||
"name": "Mates.Media",
|
||||
"email": "mates.media@users.noreply.github.com"
|
||||
},
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
"body-parser": "2.2.2",
|
||||
"cors": "2.8.6",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"express": "5.2.1",
|
||||
"open": "11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "6.0.0",
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"node-fetch": "3.3.2"
|
||||
"@electron/rebuild": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"cross-env": "10.1.0",
|
||||
"electron": "40.6.0",
|
||||
"electron-builder": "26.8.1",
|
||||
"node-fetch": "3.3.2",
|
||||
"png2icons": "2.0.1",
|
||||
"sharp": "0.34.5",
|
||||
"wait-on": "9.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
63
scripts/generate-icons.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
import sharp from 'sharp';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import png2icons from 'png2icons';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const sourceIcon = path.join(rootDir, 'appicon-square.png');
|
||||
|
||||
// Electron desktop icons
|
||||
const electronIcons = [
|
||||
{ name: '16x16.png', size: 16 },
|
||||
{ name: '24x24.png', size: 24 },
|
||||
{ name: '32x32.png', size: 32 },
|
||||
{ name: '48x48.png', size: 48 },
|
||||
{ name: '64x64.png', size: 64 },
|
||||
{ name: '128x128.png', size: 128 },
|
||||
{ name: '256x256.png', size: 256 },
|
||||
{ name: '512x512.png', size: 512 },
|
||||
{ name: '1024x1024.png', size: 1024 },
|
||||
{ name: 'icon.png', size: 512 },
|
||||
];
|
||||
|
||||
async function generateElectronIcons() {
|
||||
const iconsDir = path.join(rootDir, 'icons');
|
||||
|
||||
// Ensure icons directory exists
|
||||
await fs.mkdir(iconsDir, { recursive: true });
|
||||
|
||||
for (const icon of electronIcons) {
|
||||
const outputPath = path.join(iconsDir, icon.name);
|
||||
await sharp(sourceIcon)
|
||||
.resize(icon.size, icon.size)
|
||||
.png()
|
||||
.toFile(outputPath);
|
||||
console.log(`Generated: ${icon.name}`);
|
||||
}
|
||||
|
||||
// Generate ICO file (Windows)
|
||||
const sourceBuffer = await fs.readFile(sourceIcon);
|
||||
const icoBuffer = png2icons.createICO(sourceBuffer, png2icons.BEZIER, 0, true, true);
|
||||
await fs.writeFile(path.join(iconsDir, 'icon.ico'), icoBuffer);
|
||||
console.log('Generated: icon.ico');
|
||||
|
||||
// Generate ICNS file (macOS)
|
||||
const icnsBuffer = png2icons.createICNS(sourceBuffer, png2icons.BEZIER, 0);
|
||||
await fs.writeFile(path.join(iconsDir, 'icon.icns'), icnsBuffer);
|
||||
console.log('Generated: icon.icns');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Generating icons from:', sourceIcon);
|
||||
console.log('');
|
||||
|
||||
console.log('=== Electron Desktop Icons ===');
|
||||
await generateElectronIcons();
|
||||
console.log('');
|
||||
|
||||
console.log('Done!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
224
scripts/generate-postman-collection.js
Executable file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generate Postman collection from API documentation
|
||||
* This script reads the API.md file and generates a Postman collection
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const collection = {
|
||||
info: {
|
||||
_postman_id: 'paarrot-api-collection',
|
||||
name: 'Paarrot API',
|
||||
description: 'API endpoints for controlling Paarrot (Cinny Desktop) features like muting, deafening, channel navigation, and messaging.',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
_exporter_id: 'paarrot-api',
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'Health & Status',
|
||||
item: [
|
||||
{
|
||||
name: 'Health Check',
|
||||
request: {
|
||||
method: 'GET',
|
||||
header: [],
|
||||
url: {
|
||||
raw: '{{baseUrl}}/health',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['health'],
|
||||
},
|
||||
description: 'Check if the API server is running and healthy.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Get Status',
|
||||
request: {
|
||||
method: 'GET',
|
||||
header: [],
|
||||
url: {
|
||||
raw: '{{baseUrl}}/status',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['status'],
|
||||
},
|
||||
description: 'Get current application status including mute/deafen state, current room, connection status, and user ID.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Audio Controls',
|
||||
item: [
|
||||
{
|
||||
name: 'Toggle Mute',
|
||||
request: {
|
||||
method: 'POST',
|
||||
header: [],
|
||||
url: {
|
||||
raw: '{{baseUrl}}/mute/toggle',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['mute', 'toggle'],
|
||||
},
|
||||
description: 'Toggle microphone mute state. Works only when in an active call.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Set Mute',
|
||||
request: {
|
||||
method: 'POST',
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: JSON.stringify({ muted: true }, null, 2),
|
||||
},
|
||||
url: {
|
||||
raw: '{{baseUrl}}/mute',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['mute'],
|
||||
},
|
||||
description: 'Set microphone mute to a specific state (true/false). Works only when in an active call.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Toggle Deafen',
|
||||
request: {
|
||||
method: 'POST',
|
||||
header: [],
|
||||
url: {
|
||||
raw: '{{baseUrl}}/deafen/toggle',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['deafen', 'toggle'],
|
||||
},
|
||||
description: 'Toggle deafen state (mutes all incoming audio and your microphone). Works only when in an active call.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Set Deafen',
|
||||
request: {
|
||||
method: 'POST',
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: JSON.stringify({ deafened: true }, null, 2),
|
||||
},
|
||||
url: {
|
||||
raw: '{{baseUrl}}/deafen',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['deafen'],
|
||||
},
|
||||
description: 'Set deafen to a specific state (true/false). Works only when in an active call.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Rooms & Channels',
|
||||
item: [
|
||||
{
|
||||
name: 'Get Channels',
|
||||
request: {
|
||||
method: 'GET',
|
||||
header: [],
|
||||
url: {
|
||||
raw: '{{baseUrl}}/channels',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['channels'],
|
||||
},
|
||||
description: 'Get list of all available rooms/channels with their details (roomId, name, isDirect, avatar).',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Get Current Room',
|
||||
request: {
|
||||
method: 'GET',
|
||||
header: [],
|
||||
url: {
|
||||
raw: '{{baseUrl}}/room/current',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['room', 'current'],
|
||||
},
|
||||
description: 'Get information about the currently active room.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Change Channel',
|
||||
request: {
|
||||
method: 'POST',
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: JSON.stringify({ roomId: '!YourRoomId:server.com' }, null, 2),
|
||||
},
|
||||
url: {
|
||||
raw: '{{baseUrl}}/channel',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['channel'],
|
||||
},
|
||||
description: 'Navigate to a different room/channel by providing its roomId.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Messaging',
|
||||
item: [
|
||||
{
|
||||
name: 'Send Message to Room',
|
||||
request: {
|
||||
method: 'POST',
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: JSON.stringify(
|
||||
{
|
||||
roomId: '!YourRoomId:server.com',
|
||||
message: 'Hello from Postman!',
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
url: {
|
||||
raw: '{{baseUrl}}/message',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['message'],
|
||||
},
|
||||
description: 'Send a text message to a specific room by providing roomId and message content.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Send Message to Current Room',
|
||||
request: {
|
||||
method: 'POST',
|
||||
header: [{ key: 'Content-Type', value: 'application/json' }],
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: JSON.stringify({ message: 'Hello from Postman!' }, null, 2),
|
||||
},
|
||||
url: {
|
||||
raw: '{{baseUrl}}/message/current',
|
||||
host: ['{{baseUrl}}'],
|
||||
path: ['message', 'current'],
|
||||
},
|
||||
description: 'Send a text message to the currently active room.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
variable: [
|
||||
{
|
||||
key: 'baseUrl',
|
||||
value: 'http://127.0.0.1:33384',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Write the collection to file (in the project root)
|
||||
const outputPath = path.join(__dirname, '..', 'paarrot-api.postman_collection.json');
|
||||
fs.writeFileSync(outputPath, JSON.stringify(collection, null, 2) + '\n');
|
||||
|
||||
console.log('✅ Postman collection generated:', outputPath);
|
||||
@@ -1,108 +0,0 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
|
||||
async function getAssetSign(url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
async function createTauriRelease() {
|
||||
if (process.env.GITHUB_TOKEN === undefined) {
|
||||
throw new Error("GITHUB_TOKEN is not found!");
|
||||
}
|
||||
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
const { repos } = github.rest;
|
||||
const repoMetaData = {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
};
|
||||
|
||||
const tagsResult = await repos.listTags({ ...repoMetaData, per_page: 10, page: 1 });
|
||||
const latestTag = tagsResult.data.find((tag) => tag.name.startsWith("v"));
|
||||
console.log(latestTag);
|
||||
|
||||
const latestRelease = await repos.getReleaseByTag({ ...repoMetaData, tag: latestTag.name });
|
||||
const latestAssets = latestRelease.data.assets;
|
||||
|
||||
const windowsX86_64 = {};
|
||||
const linuxX86_64 = {};
|
||||
const darwinX86_64 = {};
|
||||
const darwinAarch64 = {};
|
||||
|
||||
const promises = latestAssets.map(async (asset) => {
|
||||
const { name, browser_download_url } = asset;
|
||||
|
||||
if (/\.msi\.zip$/.test(name)) {
|
||||
windowsX86_64.url = browser_download_url;
|
||||
}
|
||||
if (/\.msi\.zip\.sig$/.test(name)) {
|
||||
windowsX86_64.signature = await getAssetSign(browser_download_url);
|
||||
}
|
||||
|
||||
if (/\.AppImage\.tar\.gz$/.test(name)) {
|
||||
linuxX86_64.url = browser_download_url;
|
||||
}
|
||||
if (/\.AppImage\.tar\.gz\.sig$/.test(name)) {
|
||||
linuxX86_64.signature = await getAssetSign(browser_download_url);
|
||||
}
|
||||
|
||||
if (/universal\.app\.tar\.gz$/.test(name)) {
|
||||
darwinX86_64.url = browser_download_url;
|
||||
}
|
||||
if (/universal\.app\.tar\.gz\.sig$/.test(name)) {
|
||||
darwinX86_64.signature = await getAssetSign(browser_download_url);
|
||||
}
|
||||
|
||||
if (/universal\.app\.tar\.gz$/.test(name)) {
|
||||
darwinAarch64.url = browser_download_url;
|
||||
}
|
||||
if (/universal\.app\.tar\.gz\.sig$/.test(name)) {
|
||||
darwinAarch64.signature = await getAssetSign(browser_download_url);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
const releaseData = {
|
||||
name: latestTag.name,
|
||||
notes: `https://github.com/${repoMetaData.owner}/${repoMetaData.repo}/releases/tag/${latestTag.name}`,
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms: {},
|
||||
};
|
||||
|
||||
if (windowsX86_64.url) releaseData.platforms["windows-x86_64"] = windowsX86_64;
|
||||
else console.error('Failed to get release for windowsX86_64');
|
||||
|
||||
if (linuxX86_64.url) releaseData.platforms["linux-x86_64"] = linuxX86_64;
|
||||
else console.error('Failed to get release for linuxX86_64');
|
||||
|
||||
if (darwinX86_64.url) releaseData.platforms["darwin-x86_64"] = darwinX86_64;
|
||||
else console.error('Failed to get release for darwinX86_64');
|
||||
|
||||
if (darwinAarch64.url) releaseData.platforms["darwin-aarch64"] = darwinAarch64;
|
||||
else console.error('Failed to get release for darwinAarch64');
|
||||
|
||||
const releaseResult = await repos.getReleaseByTag({ ...repoMetaData, tag: 'tauri' });
|
||||
const tauriRelease = releaseResult.data;
|
||||
|
||||
const prevReleaseAsset = tauriRelease.assets.find((asset) => asset.name === 'release.json');
|
||||
if (prevReleaseAsset) {
|
||||
await repos.deleteReleaseAsset({ ...repoMetaData, asset_id: prevReleaseAsset.id });
|
||||
}
|
||||
|
||||
console.log(releaseData);
|
||||
await repos.uploadReleaseAsset({
|
||||
...repoMetaData,
|
||||
release_id: tauriRelease.id,
|
||||
name: 'release.json',
|
||||
data: JSON.stringify(releaseData, null, 2),
|
||||
});
|
||||
}
|
||||
createTauriRelease();
|
||||
6
src-tauri/.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
WixTools
|
||||
|
||||
.tauri-private.key
|
||||
@@ -1 +0,0 @@
|
||||
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExREU1NkVBMDY4MzcxMjAKUldRZ2NZTUc2bGJlRVNaQldnMXpJcHlocVpCWUNNaUdicjBGMVRsck1GQXhvTnJiOUNhVVRHSzQK
|
||||
5473
src-tauri/Cargo.lock
generated
@@ -1,39 +0,0 @@
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[package]
|
||||
name = "cinny"
|
||||
version = "4.10.2"
|
||||
description = "Yet another matrix client"
|
||||
authors = ["Ajay Bura"]
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/cinnyapp/cinny-desktop"
|
||||
default-run = "cinny"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
||||
[lib]
|
||||
name = "cinny_lib"
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "2", features = ["devtools"] }
|
||||
tauri-plugin-localhost = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
|
||||
[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies]
|
||||
tauri-plugin-deep-link = "2"
|
||||
|
||||
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
29
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability for all windows",
|
||||
"windows": ["*"],
|
||||
"platforms": ["linux", "windows", "macOS"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:webview:allow-internal-toggle-devtools",
|
||||
"opener:default",
|
||||
"opener:allow-open-url",
|
||||
"opener:allow-default-urls",
|
||||
"notification:default",
|
||||
"notification:allow-register-listener",
|
||||
"autostart:default",
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download",
|
||||
"updater:allow-download-and-install",
|
||||
"dialog:default",
|
||||
"process:allow-restart",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "url": "https://api.telegram.org/**" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
19
src-tauri/gen/android/.gitignore
vendored
@@ -1,19 +0,0 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
6
src-tauri/gen/android/app/.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
/src/main/java/in/cinny/app/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
@@ -1,70 +0,0 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("rust")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "in.cinny.app"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "in.cinny.app"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
21
src-tauri/gen/android/app/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.cinny"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Background sync service -->
|
||||
<service
|
||||
android:name=".SyncService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!-- Boot receiver to restart service -->
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,18 +0,0 @@
|
||||
package `in`.cinny.app
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Receives boot completed broadcast to restart the sync service
|
||||
* after device reboot.
|
||||
*/
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
// Start the sync service after boot
|
||||
SyncService.start(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package `in`.cinny.app
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/**
|
||||
* Main activity for Cinny. Handles permission requests and
|
||||
* starts the background sync service for notifications.
|
||||
*/
|
||||
class MainActivity : TauriActivity() {
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_PERMISSION_CODE = 1001
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestNotificationPermission()
|
||||
startSyncService()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Ensure sync service is running when app is in foreground
|
||||
startSyncService()
|
||||
}
|
||||
|
||||
private fun requestNotificationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startSyncService() {
|
||||
SyncService.start(this)
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package `in`.cinny.app
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
/**
|
||||
* Foreground service to keep Matrix sync connection alive in background.
|
||||
* This service maintains a wake lock and shows a persistent notification
|
||||
* to prevent Android from killing the app while syncing.
|
||||
*/
|
||||
class SyncService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "cinny_sync_channel"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
private const val WAKE_LOCK_TAG = "Cinny:SyncWakeLock"
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, SyncService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val intent = Intent(context, SyncService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
acquireWakeLock()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val notification = createNotification()
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseWakeLock()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Matrix Sync",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Keeps Cinny connected for message notifications"
|
||||
setShowBadge(false)
|
||||
}
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Cinny")
|
||||
.setContentText("Connected to Matrix")
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun acquireWakeLock() {
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
wakeLock = powerManager.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
WAKE_LOCK_TAG
|
||||
).apply {
|
||||
acquire(10 * 60 * 1000L) // 10 minutes, will be renewed
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseWakeLock() {
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) {
|
||||
it.release()
|
||||
}
|
||||
}
|
||||
wakeLock = null
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -1,170 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,6 +0,0 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.cinny" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Cinny</string>
|
||||
<string name="main_activity_title">Cinny</string>
|
||||
</resources>
|
||||
@@ -1,6 +0,0 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.cinny" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
@@ -1,22 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
delete("build")
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("pluginsForCoolKids") {
|
||||
id = "rust"
|
||||
implementationClass = "RustPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(gradleApi())
|
||||
implementation("com.android.tools.build:gradle:8.11.0")
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import java.io.File
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
var target: String? = null
|
||||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """npm""";
|
||||
try {
|
||||
runTauriCli(executable)
|
||||
} catch (e: Exception) {
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
// Try different Windows-specific extensions
|
||||
val fallbacks = listOf(
|
||||
"$executable.exe",
|
||||
"$executable.cmd",
|
||||
"$executable.bat",
|
||||
)
|
||||
|
||||
var lastException: Exception = e
|
||||
for (fallback in fallbacks) {
|
||||
try {
|
||||
runTauriCli(fallback)
|
||||
return
|
||||
} catch (fallbackException: Exception) {
|
||||
lastException = fallbackException
|
||||
}
|
||||
}
|
||||
throw lastException
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runTauriCli(executable: String) {
|
||||
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||
val target = target ?: throw GradleException("target cannot be null")
|
||||
val release = release ?: throw GradleException("release cannot be null")
|
||||
val args = listOf("run", "--", "tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
args("--release")
|
||||
}
|
||||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.get
|
||||
|
||||
const val TASK_GROUP = "rust"
|
||||
|
||||
open class Config {
|
||||
lateinit var rootDirRel: String
|
||||
}
|
||||
|
||||
open class RustPlugin : Plugin<Project> {
|
||||
private lateinit var config: Config
|
||||
|
||||
override fun apply(project: Project) = with(project) {
|
||||
config = extensions.create("rust", Config::class.java)
|
||||
|
||||
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||
|
||||
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||
|
||||
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
@Suppress("UnstableApiUsage")
|
||||
flavorDimensions.add("abi")
|
||||
productFlavors {
|
||||
create("universal") {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters += abiList
|
||||
}
|
||||
}
|
||||
defaultArchList.forEachIndexed { index, arch ->
|
||||
create(arch) {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters.add(defaultAbiList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
for (profile in listOf("debug", "release")) {
|
||||
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||
val buildTask = tasks.maybeCreate(
|
||||
"rustBuildUniversal$profileCapitalized",
|
||||
DefaultTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for all targets"
|
||||
}
|
||||
|
||||
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||
|
||||
for (targetPair in targetsList.withIndex()) {
|
||||
val targetName = targetPair.value
|
||||
val targetArch = archList[targetPair.index]
|
||||
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||
val targetBuildTask = project.tasks.maybeCreate(
|
||||
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||
BuildTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for $targetArch"
|
||||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||
targetBuildTask
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
@@ -1,6 +0,0 @@
|
||||
#Tue May 10 19:22:52 CST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
185
src-tauri/gen/android/gradlew
vendored
@@ -1,185 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
src-tauri/gen/android/gradlew.bat
vendored
@@ -1,89 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,3 +0,0 @@
|
||||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,35 +0,0 @@
|
||||
//! Cinny Desktop library for cross-platform builds including Android
|
||||
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub use mobile::*;
|
||||
|
||||
/// Runs the Tauri application
|
||||
pub fn run() {
|
||||
let port = 44548;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let builder = {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_localhost::Builder::new(port).build())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_single_instance::init(|_app, _args, _cwd| {}))
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
let builder = {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_localhost::Builder::new(port).build())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
};
|
||||
|
||||
builder
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while building tauri application");
|
||||
}
|
||||
@@ -4,5 +4,5 @@
|
||||
)]
|
||||
|
||||
fn main() {
|
||||
cinny_lib::run();
|
||||
paarrot_lib::run();
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
//! Mobile-specific entry points for Android and iOS
|
||||
|
||||
/// Mobile entry point
|
||||
#[tauri::mobile_entry_point]
|
||||
fn main() {
|
||||
crate::run();
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Cinny",
|
||||
"version": "4.10.2",
|
||||
"identifier": "in.cinny.app",
|
||||
"build": {
|
||||
"frontendDist": "../cinny/dist",
|
||||
"devUrl": "http://localhost:8080",
|
||||
"beforeDevCommand": "cd cinny && npm start",
|
||||
"beforeBuildCommand": "cd cinny && npm run build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Cinny",
|
||||
"width": 1280,
|
||||
"height": 905,
|
||||
"center": true,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"dragDropEnabled": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "script-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' https: http: data: blob:"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"notification": {
|
||||
"sound": true
|
||||
},
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases/download/latest/update.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExREU1NkVBMDY4MzcxMjAKUldRZ2NZTUc2bGJlRVNaQldnMXpJcHlocVpCWUNNaUdicjBGMVRsck1GQXhvTnJiOUNhVVRHSzQK"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["msi", "appimage", "deb", "rpm"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "",
|
||||
"category": "SocialNetworking",
|
||||
"shortDescription": "Yet another matrix client",
|
||||
"longDescription": "",
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": []
|
||||
}
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"providerShortName": null,
|
||||
"entitlements": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "",
|
||||
"wix": {
|
||||
"bannerPath": "wix/banner.bmp",
|
||||
"dialogImagePath": "wix/dialogImage.bmp"
|
||||
},
|
||||
"nsis": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 451 KiB |
91
test-api.js
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Paarrot API Test Script
|
||||
*
|
||||
* Simple test script to verify the API server is working.
|
||||
* Run this while Paarrot is running to test the API endpoints.
|
||||
*
|
||||
* Usage: node test-api.js
|
||||
*/
|
||||
|
||||
const API_BASE = 'http://127.0.0.1:33384';
|
||||
|
||||
async function testEndpoint(name, method, path, body = null) {
|
||||
console.log(`\n🧪 Testing: ${name}`);
|
||||
console.log(` ${method} ${API_BASE}${path}`);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
console.log(` Body:`, body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, options);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log(` ✅ Success (${response.status})`);
|
||||
console.log(` Response:`, JSON.stringify(data, null, 2));
|
||||
} else {
|
||||
console.log(` ❌ Failed (${response.status})`);
|
||||
console.log(` Error:`, data.error || data);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('🚀 Paarrot API Test Suite\n');
|
||||
console.log('Make sure Paarrot is running before running these tests!\n');
|
||||
|
||||
// Health check
|
||||
await testEndpoint('Health Check', 'GET', '/health');
|
||||
|
||||
// Get status
|
||||
await testEndpoint('Get Status', 'GET', '/status');
|
||||
|
||||
// Get channels
|
||||
await testEndpoint('Get Channels', 'GET', '/channels');
|
||||
|
||||
// Get current room
|
||||
await testEndpoint('Get Current Room', 'GET', '/room/current');
|
||||
|
||||
// Toggle mute
|
||||
await testEndpoint('Toggle Mute', 'POST', '/mute/toggle');
|
||||
|
||||
// Set mute
|
||||
await testEndpoint('Set Mute (true)', 'POST', '/mute', { muted: true });
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await testEndpoint('Set Mute (false)', 'POST', '/mute', { muted: false });
|
||||
|
||||
// Toggle deafen
|
||||
await testEndpoint('Toggle Deafen', 'POST', '/deafen/toggle');
|
||||
|
||||
// Send message to current room (commented out to avoid spam)
|
||||
// await testEndpoint('Send Message to Current', 'POST', '/message/current', {
|
||||
// message: 'Test message from API'
|
||||
// });
|
||||
|
||||
// Test invalid endpoint
|
||||
await testEndpoint('Invalid Endpoint (should 404)', 'GET', '/invalid');
|
||||
|
||||
console.log('\n✨ Tests complete!\n');
|
||||
}
|
||||
|
||||
// Run tests if this script is executed directly
|
||||
if (require.main === module) {
|
||||
runTests().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = { testEndpoint, runTests };
|
||||