Compare commits

..

2 Commits

Author SHA1 Message Date
Krishan 45b679f042 Fix to latest release 2026-02-25 12:44:54 +00:00
renovate[bot] c22e34d3cd Update softprops/action-gh-release digest to e798e6a 2026-02-24 07:00:32 +00:00
64 changed files with 2410 additions and 28325 deletions
-1
View File
@@ -1 +0,0 @@
81e1a25de641f0292863b1404cba728c1eadd00d
-301
View File
@@ -1,301 +0,0 @@
name: Build Lotus Chat Desktop
on:
push:
branches: [main]
workflow_dispatch:
env:
GITEA_URL: https://code.lotusguild.org
REPO: LotusGuild/cinny-desktop
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
release_id: ${{ steps.release.outputs.release_id }}
steps:
- name: Compute version
id: ver
run: echo "version=4.12.${{ github.run_number }}" >> $GITHUB_OUTPUT
- name: Create or update release
id: release
env:
TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
VERSION="4.12.${{ github.run_number }}"
EXISTING=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/tags/latest" \
-H "Authorization: token $TOKEN" 2>/dev/null || true)
RELEASE_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ] && [ "$RELEASE_ID" != "" ]; then
curl -sf -X PATCH "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"Lotus Chat $VERSION\",\"body\":\"Built from ${{ github.sha }}\"}" > /dev/null
else
RELEASE_ID=$(curl -sf -X POST "$GITEA_URL/api/v1/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"latest\",\"name\":\"Lotus Chat $VERSION\",\"prerelease\":true,\"body\":\"Built from ${{ github.sha }}\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
fi
echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT
build-windows:
needs: prepare
runs-on: windows
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Checkout submodules (shallow)
shell: powershell
run: git submodule update --init --depth=1
- name: Patch version
shell: powershell
run: |
$ver = '${{ needs.prepare.outputs.version }}'
node -e "const fs=require('fs');const d=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8'));d.version='$ver';fs.writeFileSync('src-tauri/tauri.conf.json',JSON.stringify(d,null,2),'utf8');"
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install frontend deps
shell: powershell
run: cd cinny; npm ci
- name: Install Tauri deps
shell: powershell
run: npm ci
- name: Build
shell: powershell
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ''
NODE_OPTIONS: '--max_old_space_size=4096'
# Sparse registry avoids a full git clone of the crates.io index —
# eliminates the curl SSL handshake failures seen on Windows runners.
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
# Disable HTTP/2 multiplexing — ALPN negotiation can reset on Windows Schannel.
CARGO_HTTP_MULTIPLEXING: 'false'
# Retry transient network errors before failing.
CARGO_NET_RETRY: '5'
run: |
# USERPROFILE is set by Windows directly; more reliable than C:\Users\$USERNAME
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"
# Also add the actual toolchain bin to bypass rustup shim execution issues
$toolchain = Get-ChildItem "$env:USERPROFILE\.rustup\toolchains" -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match 'stable' } | Select-Object -First 1
if ($toolchain) { $env:PATH = "$($toolchain.FullName)\bin;$env:PATH" }
Write-Host "cargo: $((Get-Command cargo -ErrorAction SilentlyContinue).Source)"
cargo --version
npm run tauri -- build --bundles nsis
- name: Upload to release
shell: powershell
env:
TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
$releaseId = $env:RELEASE_ID
$VERSION = $env:VERSION
Write-Host "Version: $VERSION Release: $releaseId"
$nsis = "src-tauri\target\release\bundle\nsis"
$files = @(
"$nsis\Lotus Chat_${VERSION}_x64-setup.exe",
"$nsis\Lotus Chat_${VERSION}_x64-setup.nsis.zip",
"$nsis\Lotus Chat_${VERSION}_x64-setup.nsis.zip.sig"
)
$names = @("LotusChat-x86_64-setup.exe", "LotusChat-x86_64-setup.nsis.zip", "LotusChat-x86_64-setup.nsis.zip.sig")
for ($i = 0; $i -lt $files.Length; $i++) {
$existing = (Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets" `
-Headers @{ Authorization = "token $env:TOKEN" }) | Where-Object { $_.name -eq $names[$i] }
if ($existing) {
Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets/$($existing.id)" `
-Method Delete -Headers @{ Authorization = "token $env:TOKEN" }
}
$bytes = [System.IO.File]::ReadAllBytes($files[$i])
Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets?name=$($names[$i])" `
-Method Post `
-Headers @{ Authorization = "token $env:TOKEN"; "Content-Type" = "application/octet-stream" } `
-Body $bytes
}
build-linux:
needs: prepare
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Install system deps
run: |
apt-get update
apt-get install -y \
curl wget file gcc imagemagick \
libwebkit2gtk-4.1-dev \
libssl-dev \
libxdo-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
xdg-utils \
squashfs-tools
- name: Ensure icons are RGBA PNG
run: |
for f in src-tauri/icons/*.png; do
info=$(identify -verbose "$f" 2>/dev/null | grep "Type:" | head -1)
echo "$f: $info"
convert "$f" -type TrueColorAlpha PNG32:"$f"
done
- name: Set up Rust toolchain
run: |
source "$HOME/.cargo/env" 2>/dev/null || true
if command -v cargo >/dev/null 2>&1; then
echo "Using existing Rust: $(cargo --version)"
else
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
source "$HOME/.cargo/env"
fi
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Patch version
run: python3 -c "import json; d=json.load(open('src-tauri/tauri.conf.json')); d['version']='${{ needs.prepare.outputs.version }}'; open('src-tauri/tauri.conf.json','w').write(json.dumps(d,indent=2))"
- name: Install frontend deps
run: cd cinny && npm ci
- name: Install Tauri deps
run: npm ci
- name: Stage AppRun and linuxdeploy for AppImage bundler
run: |
set -e
mkdir -p ~/.cache/tauri
cp tools/AppRun-x86_64 ~/.cache/tauri/AppRun-x86_64
chmod +x ~/.cache/tauri/AppRun-x86_64
wget -q \
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-x86_64.AppImage" \
-O /tmp/linuxdeploy.AppImage
chmod +x /tmp/linuxdeploy.AppImage
rm -rf /root/linuxdeploy-root
(cd /root && /tmp/linuxdeploy.AppImage --appimage-extract)
mv /root/squashfs-root /root/linuxdeploy-root
echo "Extracted linuxdeploy:"
ls /root/linuxdeploy-root/
ls /root/linuxdeploy-root/usr/bin/ 2>/dev/null || echo "no usr/bin"
# Pre-stage plugin scripts next to linuxdeploy so it finds them via /proc/self/exe lookup
wget -q "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" \
-O /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gtk.sh
wget -q "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh" \
-O /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gstreamer.sh
chmod +x /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gtk.sh \
/root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gstreamer.sh
gcc -o ~/.cache/tauri/linuxdeploy-x86_64.AppImage tools/ld_wrapper.c
chmod +x ~/.cache/tauri/linuxdeploy-x86_64.AppImage
- name: Build
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ''
NODE_OPTIONS: '--max_old_space_size=4096'
RUST_LOG: tauri_bundler=debug
run: npm run tauri -- build --bundles appimage,deb
- name: Show linuxdeploy wrapper log
if: always()
run: cat /tmp/ld-wrapper.log 2>/dev/null || echo "no wrapper log found"
- name: Upload to release
env:
TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
APPIMAGE_DIR="src-tauri/target/release/bundle/appimage"
DEB_DIR="src-tauri/target/release/bundle/deb"
upload() {
local name="$1" path="$2"
local existing_id
existing_id=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets" \
-H "Authorization: token $TOKEN" \
| python3 -c "import sys,json; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']=='$name'), ''))" 2>/dev/null || true)
if [ -n "$existing_id" ]; then
curl -sf -X DELETE "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets/$existing_id" \
-H "Authorization: token $TOKEN" || true
fi
echo "Uploading $name"
curl -sf -X POST \
"$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$path"
}
upload "LotusChat-x86_64.AppImage" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage"
upload "LotusChat-x86_64.AppImage.tar.gz" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage.tar.gz"
upload "LotusChat-x86_64.AppImage.tar.gz.sig" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage.tar.gz.sig"
upload "LotusChat-x86_64.deb" "$DEB_DIR/Lotus Chat_${VERSION}_amd64.deb"
update-manifest:
needs: [prepare, build-windows, build-linux]
runs-on: ubuntu-latest
steps:
- name: Generate and upload release.json
env:
TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
BASE="$GITEA_URL/LotusGuild/cinny-desktop/releases/download/latest"
WIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64-setup.nsis.zip.sig")
LIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64.AppImage.tar.gz.sig")
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
python3 -c "import json,sys; v,d,wu,ws,lu,ls=sys.argv[1:]; print(json.dumps({'version':v,'notes':'Latest Lotus Chat release','pub_date':d,'platforms':{'windows-x86_64':{'url':wu,'signature':ws},'linux-x86_64':{'url':lu,'signature':ls}}},indent=2))" \
"$VERSION" "$DATE" \
"$BASE/LotusChat-x86_64-setup.nsis.zip" "$WIN_SIG" \
"$BASE/LotusChat-x86_64.AppImage.tar.gz" "$LIN_SIG" \
> release.json
cat release.json
OLD=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets" \
-H "Authorization: token $TOKEN" \
| python3 -c "import sys,json; print(next((str(a['id']) for a in json.load(sys.stdin) if a['name']=='release.json'), ''))" 2>/dev/null || true)
[ -n "$OLD" ] && curl -sf -X DELETE \
"$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets/$OLD" \
-H "Authorization: token $TOKEN" || true
curl -sf -X POST \
"$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=release.json" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @release.json
+58
View File
@@ -0,0 +1,58 @@
name: 🐞 Bug Report
description: Report a bug
labels: 'type: bug'
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/cinnyapp/cinny-desktop/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Cinny version: [e.g. 1.8.1]
3. Matrix homeserver: [e.g. matrix.org]
4. Downloaded from: [e.g. GitHub, Flatpak]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+3 -4
View File
@@ -1,5 +1,4 @@
blank_issues_enabled: false
contact_links:
- name: Features, Bug Reports, Questions
url: https://github.com/cinnyapp/cinny/discussions/new/choose
about: Our preferred starting point if you have any questions or suggestions about features or behavior.
- name: 💬 Matrix Chat
url: https://matrix.to/#/#cinny:matrix.org
about: Ask questions and talk to other Cinny users and the maintainers
@@ -0,0 +1,34 @@
name: 💡 Feature Request
description: Suggest an idea
labels: 'type: feature'
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+22
View File
@@ -0,0 +1,22 @@
<!-- Please read https://github.com/cinnyapp/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #
#### Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
### Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
+3
View File
@@ -0,0 +1,3 @@
# Reporting a Vulnerability
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
+7 -4
View File
@@ -2,13 +2,16 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboardApproval",
":semanticCommits"
":dependencyDashboardApproval"
],
"labels": [
"Dependencies"
],
"labels": ["Dependencies"],
"packageRules": [
{
"matchUpdateTypes": ["lockFileMaintenance"]
"matchUpdateTypes": [
"lockFileMaintenance"
]
}
],
"lockFileMaintenance": {
+2 -2
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Create zip including submodules
@@ -16,7 +16,7 @@ jobs:
cd ..
zip ${{ github.event.repository.name }}/${{ github.event.repository.name }}-${{ github.ref_name }}.zip ${{ github.event.repository.name }} -r
- name: Upload zip to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: |
${{ github.event.repository.name }}-${{ github.ref_name }}.zip
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: 'CLA Assistant'
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: cla-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
uses: cla-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
+2 -2
View File
@@ -14,9 +14,9 @@ jobs:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6.0.2
- name: NPM Lockfile Changes
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891 # v1.0.0
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Optional inputs, can be deleted safely if you are happy with default values.
-15
View File
@@ -1,15 +0,0 @@
name: Check PR title
on:
pull_request_target:
types:
- opened
- edited
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+42 -37
View File
@@ -9,31 +9,33 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: ".node-version"
package-manager-cache: false
node-version: 24.13.1
cache: 'npm'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable # They use branch based releases
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@73fb865345c54760d875b94642314f8c0c894afa # v0.6.1
uses: tauri-apps/tauri-action@v0.5.14
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
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.version
$version = $json.package.version
echo "Version: ${version}"
echo "TAURI_VERSION=${version}" >> $Env:GITHUB_ENV
echo "${Env:TAURI_VERSION}"
@@ -48,7 +50,7 @@ jobs:
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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: |
src-tauri/target/release/bundle/msi/Cinny_desktop-x86_64.msi
@@ -60,44 +62,46 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: ".node-version"
package-manager-cache: false
node-version: 24.13.1
cache: 'npm'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable # They use branch based releases
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
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@73fb865345c54760d875b94642314f8c0c894afa # v0.6.1
uses: tauri-apps/tauri-action@v0.5.14
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
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 .version src-tauri/tauri.conf.json | tr -d '"')" >> $GITHUB_OUTPUT
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"
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"
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"
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"
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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: |
src-tauri/target/release/bundle/deb/Cinny_desktop-x86_64.deb
@@ -110,34 +114,35 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: ".node-version"
package-manager-cache: false
node-version: 24.13.1
cache: 'npm'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable # They use branch based releases
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
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@73fb865345c54760d875b94642314f8c0c894afa # v0.6.1
uses: tauri-apps/tauri-action@v0.5.14
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
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 .version src-tauri/tauri.conf.json | tr -d '"')" >> $GITHUB_OUTPUT
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
@@ -145,7 +150,7 @@ jobs:
- 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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: |
src-tauri/target/universal-apple-darwin/release/bundle/dmg/Cinny_desktop-universal.dmg
@@ -159,7 +164,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6.0.2
- name: Install dependencies
run: npm ci
- name: Run release.json
+13 -51
View File
@@ -1,78 +1,40 @@
name: "Build pull request"
on:
pull_request:
types: ['opened', 'synchronize']
#pull_request:
#types: ['opened', 'synchronize']
jobs:
publish-tauri:
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest'
args: '--target universal-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
- platform: 'windows-latest'
args: ''
platform: [macos-latest, ubuntu-20.04, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6.0.2
with:
submodules: true
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v6.2.0
with:
node-version-file: ".node-version"
package-manager-cache: false
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable # They use branch based releases
node-version: 24.13.1
cache: 'npm'
- name: Install Rust stable
uses: actions-rs/toolchain@v1.0.7
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
toolchain: stable
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install cinny dependencies
run: cd cinny && npm ci
- name: Install tauri dependencies
run: npm ci
- name: Build desktop app with Tauri
uses: tauri-apps/tauri-action@73fb865345c54760d875b94642314f8c0c894afa # v0.6.1
uses: tauri-apps/tauri-action@v0.5.14
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: "--max_old_space_size=4096"
with:
args: ${{ matrix.args }} --config '{"bundle":{"createUpdaterArtifacts":false}}'
- name: Upload macOS artifacts
if: matrix.platform == 'macos-latest'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-artifacts-macos
path: |
src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny*.app
if-no-files-found: warn
- name: Upload Linux artifacts
if: matrix.platform == 'ubuntu-22.04'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-artifacts-linux
path: |
src-tauri/target/release/bundle/deb/Cinny_*.deb
src-tauri/target/release/bundle/rpm/Cinny-*.rpm
src-tauri/target/release/bundle/appimage/Cinny_*.AppImage
if-no-files-found: warn
- name: Upload Windows artifacts
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-artifacts-windows
path: |
src-tauri/target/release/bundle/msi/Cinny_*_x64_*.msi
src-tauri/target/release/bundle/nsis/Cinny_*_x64-setup.exe
if-no-files-found: warn
+1 -2
View File
@@ -1,4 +1,3 @@
[submodule "cinny"]
path = cinny
url = https://code.lotusguild.org/LotusGuild/cinny.git
branch = lotus
url = https://github.com/cinnyapp/cinny.git
-1
View File
@@ -1 +0,0 @@
24.13.1
+1 -1
View File
@@ -23,7 +23,7 @@ To verify release files, you need to download [minisign](https://jedisct1.github
## Local development
Firstly, to setup Rust, NodeJS and build tools follow [Tauri documentation](https://v2.tauri.app/start/prerequisites/).
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`
+1 -1
Submodule cinny updated: f589182709...6347640a35
+27 -3
View File
@@ -1,11 +1,35 @@
{
"defaultHomeserver": 0,
"defaultHomeserver": 1,
"homeserverList": [
"matrix.lotusguild.org",
"converser.eu",
"matrix.org",
"mozilla.org"
"mozilla.org",
"unredacted.org",
"xmr.se"
],
"allowCustomHomeservers": true,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [
"#cinny-space:matrix.org",
"#community:matrix.org",
"#space:unredacted.org",
"#science-space:matrix.org",
"#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org"
],
"rooms": [
"#cinny:matrix.org",
"#freesoftware:matrix.org",
"#pcapdroid:matrix.org",
"#gentoo:matrix.org",
"#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org"
],
"servers": [ "matrix.org", "mozilla.org", "unredacted.org" ]
},
"hashRouter": {
"enabled": true,
"basename": "/"
+56 -751
View File
@@ -1,31 +1,20 @@
{
"name": "cinny",
"version": "4.12.2",
"version": "4.10.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cinny",
"version": "4.12.2",
"version": "4.10.5",
"license": "AGPL-3.0-only",
"dependencies": {
"@tauri-apps/api": "2.7.0",
"@tauri-apps/plugin-clipboard-manager": "2.3.0",
"@tauri-apps/plugin-dialog": "2.3.2",
"@tauri-apps/plugin-fs": "2.4.1",
"@tauri-apps/plugin-global-shortcut": "2.3.0",
"@tauri-apps/plugin-http": "2.5.1",
"@tauri-apps/plugin-notification": "2.3.0",
"@tauri-apps/plugin-os": "2.3.0",
"@tauri-apps/plugin-process": "2.3.0",
"@tauri-apps/plugin-shell": "2.3.0",
"@tauri-apps/plugin-updater": "2.9.0"
"@tauri-apps/api": "1.6.0"
},
"devDependencies": {
"@actions/github": "6.0.0",
"@tauri-apps/cli": "2.7.1",
"node-fetch": "3.3.2",
"shx": "0.4.0"
"@tauri-apps/cli": "1.6.2",
"node-fetch": "3.3.2"
},
"engines": {
"node": ">=16.0.0"
@@ -62,44 +51,6 @@
"node": ">=14"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
@@ -231,18 +182,23 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.7.0.tgz",
"integrity": "sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz",
"integrity": "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==",
"engines": {
"node": ">= 14.6.0",
"npm": ">= 6.6.0",
"yarn": ">= 1.19.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.7.1.tgz",
"integrity": "sha512-RcGWR4jOUEl92w3uvI0h61Llkfj9lwGD1iwvDRD2isMrDhOzjeeeVn9aGzeW1jubQ/kAbMYfydcA4BA0Cy733Q==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.2.tgz",
"integrity": "sha512-zpfZdxhm20s7d/Uejpg/T3a9sqLVe3Ih2ztINfy8v6iLw9Ohowkb9g+agZffYKlEWfOSpmCy69NFyBLj7OZL0A==",
"dev": true,
"bin": {
"tauri": "tauri.js"
@@ -255,23 +211,22 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.7.1",
"@tauri-apps/cli-darwin-x64": "2.7.1",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.7.1",
"@tauri-apps/cli-linux-arm64-gnu": "2.7.1",
"@tauri-apps/cli-linux-arm64-musl": "2.7.1",
"@tauri-apps/cli-linux-riscv64-gnu": "2.7.1",
"@tauri-apps/cli-linux-x64-gnu": "2.7.1",
"@tauri-apps/cli-linux-x64-musl": "2.7.1",
"@tauri-apps/cli-win32-arm64-msvc": "2.7.1",
"@tauri-apps/cli-win32-ia32-msvc": "2.7.1",
"@tauri-apps/cli-win32-x64-msvc": "2.7.1"
"@tauri-apps/cli-darwin-arm64": "1.6.2",
"@tauri-apps/cli-darwin-x64": "1.6.2",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.2",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.2",
"@tauri-apps/cli-linux-arm64-musl": "1.6.2",
"@tauri-apps/cli-linux-x64-gnu": "1.6.2",
"@tauri-apps/cli-linux-x64-musl": "1.6.2",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.2",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.2",
"@tauri-apps/cli-win32-x64-msvc": "1.6.2"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.7.1.tgz",
"integrity": "sha512-j2NXQN6+08G03xYiyKDKqbCV2Txt+hUKg0a8hYr92AmoCU8fgCjHyva/p16lGFGUG3P2Yu0xiNe1hXL9ZuRMzA==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.2.tgz",
"integrity": "sha512-6mdRyf9DaLqlZvj8kZB09U3rwY+dOHSGzTZ7+GDg665GJb17f4cb30e8dExj6/aghcsOie9EGpgiURcDUvLNSQ==",
"cpu": [
"arm64"
],
@@ -285,9 +240,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.7.1.tgz",
"integrity": "sha512-CdYAefeM35zKsc91qIyKzbaO7FhzTyWKsE8hj7tEJ1INYpoh1NeNNyL/NSEA3Nebi5ilugioJ5tRK8ZXG8y3gw==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.2.tgz",
"integrity": "sha512-PLxZY5dn38H3R9VRmBN/l0ZDB5JFanCwlK4rmpzDQPPg3tQmbu5vjSCP6TVj5U6aLKsj79kFyULblPr5Dn9+vw==",
"cpu": [
"x64"
],
@@ -301,9 +256,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.7.1.tgz",
"integrity": "sha512-dnvyJrTA1UJxJjQ8q1N/gWomjP8Twij1BUQu2fdcT3OPpqlrbOk5R1yT0oD/721xoKNjroB5BXCsmmlykllxNg==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.2.tgz",
"integrity": "sha512-xnpj4BLeeGOh5I/ewCQlYJZwHH0CBNBN+4q8BNWNQ9MKkjN9ST366RmHRzl2ANNgWwijOPxyce7GiUmvuH8Atw==",
"cpu": [
"arm"
],
@@ -317,9 +272,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.7.1.tgz",
"integrity": "sha512-FtBW6LJPNRTws3qyUc294AqCWU91l/H0SsFKq6q4Q45MSS4x6wxLxou8zB53tLDGEPx3JSoPLcDaSfPlSbyujQ==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.2.tgz",
"integrity": "sha512-uaiRE0vE2P+tdsCngfKt+7yKr3VZXIq/t3w01DzSdnBgHSp0zmRsRR4AhZt7ibvoEgA8GzBP+eSHJdFNZsTU9w==",
"cpu": [
"arm64"
],
@@ -333,9 +288,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.7.1.tgz",
"integrity": "sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.2.tgz",
"integrity": "sha512-o9JunVrMrhqTBLrdvEbS64W0bo1dPm0lxX51Mx+6x9SmbDjlEWGgaAHC3iKLK9khd5Yu1uO1e+8TJltAcScvmw==",
"cpu": [
"arm64"
],
@@ -348,26 +303,10 @@
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.7.1.tgz",
"integrity": "sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.7.1.tgz",
"integrity": "sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.2.tgz",
"integrity": "sha512-jL9f+o61DdQmNYKIt2Q3BA8YJ+hyC5+GdNxqDf7j5SoQ85j//YfUWbmp9ZgsPHVBxgSGZVvgGMNvf64Ykp0buQ==",
"cpu": [
"x64"
],
@@ -381,9 +320,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.7.1.tgz",
"integrity": "sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.2.tgz",
"integrity": "sha512-xsa4Pu9YMHKAX0J8pIoXfN/uhvAAAoECZDixDhWw8zi57VZ4QX28ycqolS+NscdD9NAGSgHk45MpBZWdvRtvjQ==",
"cpu": [
"x64"
],
@@ -397,9 +336,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.7.1.tgz",
"integrity": "sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.2.tgz",
"integrity": "sha512-eJtUOx2UFhJpCCkm5M5+4Co9JbjvgIHTdyS/hTSZfOEdT58CNEGVJXMA39FsSZXYoxYPE+9K7Km6haMozSmlxw==",
"cpu": [
"arm64"
],
@@ -413,9 +352,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.7.1.tgz",
"integrity": "sha512-1oeibfyWQPVcijOrTg709qhbXArjX3x1MPjrmA5anlygwrbByxLBcLXvotcOeULFcnH2FYUMMLLant8kgvwE5A==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.2.tgz",
"integrity": "sha512-9Jwx3PrhNw3VKOgPISRRXPkvoEAZP+7rFRHXIo49dvlHy2E/o9qpWi1IntE33HWeazP6KhvsCjvXB2Ai4eGooA==",
"cpu": [
"ia32"
],
@@ -429,9 +368,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.7.1.tgz",
"integrity": "sha512-D7Q9kDObutuirCNLxYQ7KAg2Xxg99AjcdYz/KuMw5HvyEPbkC9Q7JL0vOrQOrHEHxIQ2lYzFOZvKKoC2yyqXcg==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.2.tgz",
"integrity": "sha512-5Z+ZjRFJE8MXghJe1UXvGephY5ZcgVhiTI9yuMi9xgX3CEaAXASatyXllzsvGJ9EDaWMEpa0PHjAzi7LBAWROw==",
"cpu": [
"x64"
],
@@ -444,122 +383,12 @@
"node": ">= 10"
}
},
"node_modules/@tauri-apps/plugin-clipboard-manager": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.0.tgz",
"integrity": "sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.3.2.tgz",
"integrity": "sha512-cNLo9YeQSC0MF4IgXnotHsqEgJk72MBZLXmQPrLA95qTaaWiiaFQ38hIMdZ6YbGUNkr3oni3EhU+AD5jLHcdUA==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-fs": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.1.tgz",
"integrity": "sha512-vJlKZVGF3UAFGoIEVT6Oq5L4HGDCD78WmA4uhzitToqYiBKWAvZR61M6zAyQzHqLs0ADemkE4RSy/5sCmZm6ZQ==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-global-shortcut": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-global-shortcut/-/plugin-global-shortcut-2.3.0.tgz",
"integrity": "sha512-WbAz0ElhpP+0kzQZRScdCC7UQ7OPH8PAn//fsBNu7+ywihsnVSVOg1L9YhieAtLNtAlnmFI69Yl5AGaA3ge5IQ==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-http": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.1.tgz",
"integrity": "sha512-SpQ1azXEdQI0UB2NZTIPljJTDEe0bIaKzHYR/k4UQp6yzRYGLC/ktmIgEfQ2RvKAWus8GcYgGr5K6LJPbo/NZw==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-notification": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.0.tgz",
"integrity": "sha512-QDwXo9VzAlH97c0veuf19TZI6cRBPfJDl2O6hNEDvI66j60lOO9z+PL6MJrj8A6Y+t55r7mGhe3rQWLmOre2HA==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-os": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.3.0.tgz",
"integrity": "sha512-dm3bDsMuUngpIQdJ1jaMkMfyQpHyDcaTIKTFaAMHoKeUd+Is3UHO2uzhElr6ZZkfytIIyQtSVnCWdW2Kc58f3g==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-process": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.0.tgz",
"integrity": "sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.0.tgz",
"integrity": "sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/@tauri-apps/plugin-updater": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.9.0.tgz",
"integrity": "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
}
},
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"dev": true
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cross-spawn": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
"integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
},
"engines": {
"node": ">=4.8"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
@@ -575,72 +404,6 @@
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/execa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^6.0.0",
"get-stream": "^4.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -664,19 +427,6 @@
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -689,114 +439,6 @@
"node": ">=12.20.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/hasown": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-core-module": {
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
@@ -806,64 +448,6 @@
"node": ">=0.10.0"
}
},
"node_modules/is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true,
"license": "MIT"
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -901,19 +485,6 @@
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
"integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^2.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -923,259 +494,6 @@
"wrappy": "1"
}
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
"integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
"dev": true,
"dependencies": {
"resolve": "^1.1.6"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shelljs": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz",
"integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"execa": "^1.0.0",
"fast-glob": "^3.3.2",
"interpret": "^1.0.0",
"rechoir": "^0.6.2"
},
"bin": {
"shjs": "bin/shjs"
},
"engines": {
"node": ">=18"
}
},
"node_modules/shx": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/shx/-/shx-0.4.0.tgz",
"integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.8",
"shelljs": "^0.9.2"
},
"bin": {
"shx": "lib/cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true,
"license": "ISC"
},
"node_modules/strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -1212,19 +530,6 @@
"node": ">= 8"
}
},
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"which": "bin/which"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+6 -18
View File
@@ -1,36 +1,24 @@
{
"name": "cinny",
"version": "4.12.2",
"version": "4.10.5",
"description": "Yet another matrix client",
"main": "index.js",
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"tauri": "shx cp config.json cinny/ && tauri",
"release": "node scripts/release.mjs",
"bump": "node scripts/update-version.mjs"
"tauri": "cp config.json cinny/ && tauri",
"release": "node scripts/release.mjs"
},
"keywords": [],
"author": "Ajay Bura",
"license": "AGPL-3.0-only",
"dependencies": {
"@tauri-apps/api": "2.7.0",
"@tauri-apps/plugin-clipboard-manager": "2.3.0",
"@tauri-apps/plugin-dialog": "2.3.2",
"@tauri-apps/plugin-fs": "2.4.1",
"@tauri-apps/plugin-global-shortcut": "2.3.0",
"@tauri-apps/plugin-http": "2.5.1",
"@tauri-apps/plugin-notification": "2.3.0",
"@tauri-apps/plugin-os": "2.3.0",
"@tauri-apps/plugin-process": "2.3.0",
"@tauri-apps/plugin-shell": "2.3.0",
"@tauri-apps/plugin-updater": "2.9.0"
"@tauri-apps/api": "1.6.0"
},
"devDependencies": {
"@actions/github": "6.0.0",
"@tauri-apps/cli": "2.7.1",
"node-fetch": "3.3.2",
"shx": "0.4.0"
"@tauri-apps/cli": "1.6.2",
"node-fetch": "3.3.2"
}
}
-65
View File
@@ -1,65 +0,0 @@
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
const version = process.argv[2];
if (!version) {
console.error("Missing version");
process.exit(1);
}
console.log(`Preparing release ${version}`);
// 1. Update npm versions
execSync(`npm version ${version} --no-git-tag-version`, {
stdio: "inherit"
});
console.log(`Updated package.json and package-lock.json → ${version}`);
// 2. Update Cargo.toml
const cargoToml = "src-tauri/Cargo.toml";
let cargoContent = fs.readFileSync(cargoToml, "utf8");
cargoContent = cargoContent.replace(
/^version\s*=\s*".*"/m,
`version = "${version}"`
);
fs.writeFileSync(cargoToml, cargoContent);
console.log(`Updated ${cargoToml}${version}`);
// 3. Update tauri.conf.json
const tauriConfigPath = "src-tauri/tauri.conf.json";
const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath));
tauriConfig.version = version;
fs.writeFileSync(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2) + "\n"
);
console.log(`Updated ${tauriConfigPath}${version}`);
// 4. Update Cinny web submodule to latest tag
console.log("Updating cinny web submodule");
execSync("git submodule update --init --recursive", { stdio: "inherit" });
execSync("git fetch --tags", { cwd: "cinny", stdio: "inherit" });
const latestCommit = execSync("git rev-list --tags --max-count=1", {
cwd: "cinny",
}).toString().trim();
const latestTag = execSync(`git describe --tags ${latestCommit}`, {
cwd: "cinny",
}).toString().trim();
console.log(`Latest cinny tag: ${latestTag}`);
execSync(`git checkout ${latestTag}`, { cwd: "cinny", stdio: "inherit" });
execSync("git add cinny", { stdio: "inherit" });
console.log("Release preparation complete");
+1986 -3761
View File
File diff suppressed because it is too large Load Diff
+5 -48
View File
@@ -2,7 +2,7 @@
[package]
name = "cinny"
version = "4.12.2"
version = "4.10.5"
description = "Yet another matrix client"
authors = ["Ajay Bura"]
license = "AGPL-3.0-only"
@@ -12,24 +12,14 @@ edition = "2021"
rust-version = "1.61"
[build-dependencies]
tauri-build = { version = "2", features = [] }
tauri-build = { version = "1.5.5", features = [] }
[dependencies]
serde_json = "1.0.109"
serde = { version = "1.0.193", features = ["derive"] }
tauri = { version = "2", features = ["devtools", "wry", "tray-icon", "image-png"] }
tauri-plugin-localhost = "2"
tauri-plugin-window-state = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-notification = "2"
tauri-plugin-fs = "2"
tauri-plugin-shell = "2"
tauri-plugin-http = "2"
tauri-plugin-process = "2"
tauri-plugin-os = "2"
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2"
tauri-plugin-deep-link = "2"
tauri = { version = "1.8.0", features = ["api-all", "devtools", "updater"] }
tauri-plugin-localhost = "0.1.0"
tauri-plugin-window-state = "0.1.1"
[features]
# by default Tauri runs in production mode
@@ -38,36 +28,3 @@ default = [ "custom-protocol" ]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2"
tauri-plugin-updater = "2"
tauri-plugin-single-instance = "2"
[target.'cfg(target_os = "windows")'.dependencies]
webview2-com = "0.38"
window-vibrancy = "0.6"
windows = { version = "0.61", features = [
# WinRT namespaces
"Data_Xml_Dom", # P5-41 toast XML
"Foundation",
"Foundation_Collections", # P5-41 toast UserInput IMap
"Media",
"UI_Notifications", # P5-41 WinRT toast notifications
# Win32 namespaces
"Win32_Foundation",
"Win32_Storage_EnhancedStorage", # P5-36 jump list (PKEY_Title)
"Win32_Graphics_Gdi",
"Win32_Networking_NetworkListManager", # P5-49 network awareness
"Win32_System_Com",
"Win32_System_Com_StructuredStorage", # P5-36 jump list (PROPVARIANT)
"Win32_System_Power", # P5-46 no-sleep
"Win32_System_WinRT", # P5-43 SMTC interop
"Win32_UI_Shell",
"Win32_UI_Shell_PropertiesSystem", # P5-36 jump list (IPropertyStore/PKEY_Title)
"Win32_UI_WindowsAndMessaging",
] }
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Request camera access for WebRTC calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Request microphone access for WebRTC calls.</string>
</dict>
</plist>
-16
View File
@@ -1,16 +0,0 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"windows": [
"main"
],
"permissions": [
"updater:default",
"global-shortcut:default",
"deep-link:default"
]
}
-91
View File
@@ -1,91 +0,0 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"remote": {
"urls": [
"http://localhost:44548"
]
},
"windows": [
"main"
],
"permissions": [
"core:default",
"fs:allow-read-file",
"fs:allow-write-file",
"fs:allow-read-dir",
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"core:window:allow-create",
"core:window:allow-center",
"core:window:allow-request-user-attention",
"core:window:allow-set-resizable",
"core:window:allow-set-maximizable",
"core:window:allow-set-minimizable",
"core:window:allow-set-closable",
"core:window:allow-set-title",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-close",
"core:window:allow-set-decorations",
"core:window:allow-set-always-on-top",
"core:window:allow-set-content-protected",
"core:window:allow-set-size",
"core:window:allow-set-min-size",
"core:window:allow-set-max-size",
"core:window:allow-set-position",
"core:window:allow-set-fullscreen",
"core:window:allow-set-focus",
"core:window:allow-set-icon",
"core:window:allow-set-skip-taskbar",
"core:window:allow-set-cursor-grab",
"core:window:allow-set-cursor-visible",
"core:window:allow-set-cursor-icon",
"core:window:allow-set-cursor-position",
"core:window:allow-set-ignore-cursor-events",
"core:window:allow-start-dragging",
"core:webview:allow-print",
"shell:allow-execute",
"shell:allow-open",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
"http:default",
"notification:default",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-register-all",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all",
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname",
"process:allow-restart",
"process:allow-exit",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"core:app:allow-app-show",
"core:app:allow-app-hide",
"clipboard-manager:default",
{
"identifier": "opener:allow-open-url",
"allow": [{ "url": "http://*" }, { "url": "https://*" }]
}
]
}
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["updater:default","global-shortcut:default"],"platforms":["macOS","windows","linux"]},"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","http:default","notification:default","global-shortcut:allow-is-registered","global-shortcut:allow-register","global-shortcut:allow-register-all","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:app:allow-app-show","core:app:allow-app-hide","clipboard-manager:default",{"identifier":"opener:allow-open-url","allow":[{"url":"http://*"},{"url":"https://*"}]}]}}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 15 KiB

-619
View File
@@ -1,619 +0,0 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
webview::{NewWindowResponse, PageLoadEvent, WebviewWindowBuilder},
Manager, WebviewUrl,
};
use tauri_plugin_opener::OpenerExt;
mod native;
/// Bring the main window to the foreground from the tray / a hidden /
/// minimized state. Shared by the tray, single-instance, and deep-link paths.
fn show_main(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
/// Hand a `matrix:` / `matrix.to` URL to the web app by dispatching a DOM
/// CustomEvent the client listens for (see useDeepLinkNavigate.ts). Uses
/// `eval` so we don't need the @tauri-apps/api event package on the web side.
fn forward_deeplink(app: &tauri::AppHandle, url: &str) {
show_main(app);
if let Some(window) = app.get_webview_window("main") {
if let Ok(json) = serde_json::to_string(url) {
let _ = window.eval(&format!(
"window.dispatchEvent(new CustomEvent('lotus-deeplink',{{detail:{json}}}))"
));
}
}
}
/// Pull the first `matrix:` link out of a process's CLI args (Windows/Linux
/// pass deep-link URLs as argv to a freshly launched instance).
fn matrix_url_from_args(args: &[String]) -> Option<String> {
args.iter().find(|a| a.starts_with("matrix:")).cloned()
}
// Injected into every page before app scripts load.
// Patches window.Notification to route through tauri-plugin-notification so
// WebView2's default "denied" state never reaches cinny's permission check.
// Also patches navigator.permissions.query so the React hook sees "granted".
const NOTIFICATION_BRIDGE: &str = r#"(function(){
function TauriNotification(title,options){
var opts=options||{};
try{
var body=opts.body!=null?String(opts.body):undefined;
// cinny tags message notifications with the roomId (options.tag) and the
// in-app route (options.data.path). When present, route to the rich WinRT
// toast (click-opens-room + quick reply); otherwise a plain toast.
var roomId=opts.tag!=null?String(opts.tag):undefined;
var path=(opts.data&&opts.data.path!=null)?String(opts.data.path):undefined;
if(roomId){
window.__TAURI_INTERNALS__.invoke('show_rich_toast',{
title:String(title),body:body,roomId:roomId,path:path
}).catch(function(){});
}else{
window.__TAURI_INTERNALS__.invoke('send_notification',{
title:String(title),body:body
}).catch(function(){});
}
}catch(_){}
}
TauriNotification.prototype=Object.create(EventTarget.prototype);
TauriNotification.prototype.constructor=TauriNotification;
TauriNotification.prototype.close=function(){};
// get-only 'permission' threw "Cannot set property permission ... which has
// only a getter" when the notification plugin / a polyfill assigned it. Add a
// no-op setter so the value stays 'granted' but assignment can't crash.
Object.defineProperty(TauriNotification,'permission',{get:function(){return 'granted';},set:function(){},configurable:true});
TauriNotification.requestPermission=function(){return Promise.resolve('granted');};
TauriNotification.maxActions=0;
Object.defineProperty(window,'Notification',{value:TauriNotification,writable:true,configurable:true});
var _q=navigator.permissions.query.bind(navigator.permissions);
navigator.permissions.query=function(desc){
if(desc&&desc.name==='notifications'){
return Promise.resolve(Object.assign(new EventTarget(),{state:'granted',onchange:null}));
}
return _q(desc);
};
})();"#;
#[tauri::command]
fn send_notification(
app: tauri::AppHandle,
title: String,
body: Option<String>,
) -> Result<(), String> {
use tauri_plugin_notification::NotificationExt;
let mut builder = app.notification().builder().title(&title);
if let Some(b) = &body {
builder = builder.body(b);
}
builder.show().map_err(|e| e.to_string())
}
#[derive(serde::Serialize)]
struct UpdateInfo {
available: bool,
version: Option<String>,
}
#[tauri::command]
async fn check_for_update(app: tauri::AppHandle) -> Result<UpdateInfo, String> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
use tauri_plugin_updater::UpdaterExt;
return match app.updater().map_err(|e| e.to_string())?.check().await {
Ok(Some(update)) => Ok(UpdateInfo { available: true, version: Some(update.version) }),
Ok(None) => Ok(UpdateInfo { available: false, version: None }),
Err(e) => Err(e.to_string()),
};
}
#[cfg(any(target_os = "android", target_os = "ios"))]
Ok(UpdateInfo { available: false, version: None })
}
#[tauri::command]
async fn install_update(app: tauri::AppHandle) -> Result<(), String> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
use tauri_plugin_updater::UpdaterExt;
if let Some(update) = app
.updater()
.map_err(|e| e.to_string())?
.check()
.await
.map_err(|e| e.to_string())?
{
update
.download_and_install(|_chunk, _total| {}, || {})
.await
.map_err(|e| e.to_string())?;
}
}
Ok(())
}
#[tauri::command]
fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
use windows::{
core::{BOOL, PCWSTR},
Win32::{
Foundation::{COLORREF, HWND, RECT},
Graphics::Gdi::{
CreateBitmap, CreateCompatibleDC, CreateDIBSection, CreateFontW, CreatePen,
CreateSolidBrush, DeleteDC, DeleteObject, DIB_RGB_COLORS, DrawTextW, Ellipse,
ReleaseDC, SelectObject, SetBkMode, SetTextColor, BITMAPINFO,
BITMAPINFOHEADER, BI_RGB, CLIP_DEFAULT_PRECIS, DEFAULT_CHARSET, DEFAULT_PITCH,
DEFAULT_QUALITY, DT_CENTER, DT_SINGLELINE, DT_VCENTER, FF_DONTCARE,
FW_BOLD, OUT_DEFAULT_PRECIS, PS_NULL, TRANSPARENT,
},
UI::{
Shell::{ITaskbarList3, TaskbarList},
WindowsAndMessaging::{CreateIconIndirect, DestroyIcon, HICON, ICONINFO},
},
System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
},
};
let hwnd = HWND(window.hwnd().map_err(|e| e.to_string())?.0 as _);
let hicon: Option<HICON> = if count > 0 {
let label = if count > 99 {
"99+".to_string()
} else {
count.to_string()
};
let mut label_wide: Vec<u16> = label.encode_utf16().chain(std::iter::once(0)).collect();
unsafe {
let size = 20i32;
let hdc_screen = windows::Win32::Graphics::Gdi::GetDC(None);
let hdc = CreateCompatibleDC(Some(hdc_screen));
let bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: size,
biHeight: -size,
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
..Default::default()
},
..Default::default()
};
let mut bits: *mut std::ffi::c_void = std::ptr::null_mut();
let hbm_color =
CreateDIBSection(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0)
.map_err(|e| e.to_string())?;
// Zero-init so undrawn pixels are fully transparent (CreateDIBSection
// does not guarantee zeroed memory; garbage bytes cause a black square).
if !bits.is_null() {
std::ptr::write_bytes(bits as *mut u8, 0, (size * size * 4) as usize);
}
let old_bm = SelectObject(hdc, hbm_color.into());
let hbrush = CreateSolidBrush(COLORREF(0x003030DD));
let old_brush = SelectObject(hdc, hbrush.into());
let hpen = CreatePen(PS_NULL, 0, COLORREF(0));
let old_pen = SelectObject(hdc, hpen.into());
let _ = Ellipse(hdc, 0, 0, size, size);
let hfont = CreateFontW(
14,
0,
0,
0,
FW_BOLD.0 as i32,
0,
0,
0,
DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY,
(DEFAULT_PITCH.0 | FF_DONTCARE.0) as u32,
windows::core::w!("Segoe UI"),
);
let old_font = SelectObject(hdc, hfont.into());
SetTextColor(hdc, COLORREF(0x00FFFFFF));
let _ = SetBkMode(hdc, TRANSPARENT);
let mut rect = RECT { left: 0, top: 0, right: size, bottom: size };
let label_len = label_wide.len() - 1;
let _ = DrawTextW(
hdc,
&mut label_wide[..label_len],
&mut rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
);
SelectObject(hdc, old_brush);
SelectObject(hdc, old_pen);
SelectObject(hdc, old_font);
SelectObject(hdc, old_bm);
let _ = DeleteObject(hbrush.into());
let _ = DeleteObject(hpen.into());
let _ = DeleteObject(hfont.into());
// GDI drawing leaves the alpha channel at 0 for all pixels.
// Set alpha=255 for every painted pixel so Windows uses per-pixel
// alpha compositing instead of falling back to the opaque mask,
// which would render unpainted corner pixels as a black square.
let pixel_count = (size * size) as usize;
let pixels =
std::slice::from_raw_parts_mut(bits as *mut u32, pixel_count);
for pixel in pixels.iter_mut() {
if *pixel != 0 {
*pixel |= 0xFF00_0000u32;
}
}
let hbm_mask = CreateBitmap(size, size, 1, 1, None);
if hbm_mask.0 as usize == 0 {
let _ = DeleteObject(hbm_color.into());
return Err("CreateBitmap failed".to_string());
}
let icon_info = ICONINFO {
fIcon: BOOL(1),
xHotspot: 0,
yHotspot: 0,
hbmMask: hbm_mask,
hbmColor: hbm_color,
};
let hicon = CreateIconIndirect(&icon_info).map_err(|e| {
let _ = DeleteObject(hbm_color.into());
let _ = DeleteObject(hbm_mask.into());
e.to_string()
})?;
let _ = DeleteObject(hbm_color.into());
let _ = DeleteObject(hbm_mask.into());
let _ = DeleteDC(hdc);
let _ = ReleaseDC(None, hdc_screen);
Some(hicon)
}
} else {
None
};
unsafe {
let taskbar: ITaskbarList3 =
CoCreateInstance(&TaskbarList, None, CLSCTX_INPROC_SERVER)
.map_err(|e| e.to_string())?;
taskbar.HrInit().map_err(|e| e.to_string())?;
taskbar
.SetOverlayIcon(hwnd, hicon.unwrap_or_default(), PCWSTR::null())
.map_err(|e| e.to_string())?;
if let Some(icon) = hicon {
let _ = DestroyIcon(icon);
}
}
}
Ok(())
}
/// Held in managed state so the tray's unread overlay can be updated at runtime.
/// Keeping the TrayIcon handle here also keeps the tray alive.
struct TrayUnreadState {
tray: tauri::tray::TrayIcon,
base_rgba: Vec<u8>,
width: u32,
height: u32,
}
/// Paint a small white-ringed red "unread" dot into the bottom-right corner of
/// an RGBA buffer, in place. Cross-platform (operates on raw pixels).
fn draw_unread_dot(rgba: &mut [u8], width: u32, height: u32) {
let w = width as i32;
let h = height as i32;
if w <= 0 || h <= 0 {
return;
}
let r = ((w.min(h) as f32) * 0.30) as i32;
let ring = ((r as f32) * 0.18).max(1.0) as i32;
let cx = w - r - ((w as f32) * 0.06) as i32;
let cy = h - r - ((h as f32) * 0.06) as i32;
for y in (cy - r - ring).max(0)..(cy + r + ring).min(h) {
for x in (cx - r - ring).max(0)..(cx + r + ring).min(w) {
let dx = x - cx;
let dy = y - cy;
let dist2 = dx * dx + dy * dy;
let idx = ((y * w + x) as usize) * 4;
if idx + 3 >= rgba.len() {
continue;
}
if dist2 <= r * r {
rgba[idx] = 0xDD;
rgba[idx + 1] = 0x30;
rgba[idx + 2] = 0x30;
rgba[idx + 3] = 0xFF;
} else if dist2 <= (r + ring) * (r + ring) {
rgba[idx] = 0xFF;
rgba[idx + 1] = 0xFF;
rgba[idx + 2] = 0xFF;
rgba[idx + 3] = 0xFF;
}
}
}
}
/// Overlay (or clear) the unread dot on the tray icon.
#[tauri::command]
fn set_tray_unread(unread: bool, state: tauri::State<'_, TrayUnreadState>) -> Result<(), String> {
let mut rgba = state.base_rgba.clone();
if unread {
draw_unread_dot(&mut rgba, state.width, state.height);
}
let icon = tauri::image::Image::new_owned(rgba, state.width, state.height);
state.tray.set_icon(Some(icon)).map_err(|e| e.to_string())
}
/// Flash the taskbar button to draw attention (e.g. a new mention while the
/// window is unfocused). Clears automatically once the window is focused.
#[tauri::command]
fn flash_window(window: tauri::Window) -> Result<(), String> {
window
.request_user_attention(Some(tauri::UserAttentionType::Informational))
.map_err(|e| e.to_string())
}
pub fn run() {
let port: u16 = 44548;
let context = tauri::generate_context!();
#[allow(unused_mut)]
let mut builder = tauri::Builder::default();
// Single-instance MUST be registered first: a second launch focuses the
// existing window (and forwards any matrix: link) instead of colliding on
// the localhost port. Desktop-only plugin.
#[cfg(desktop)]
{
builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| {
show_main(app);
if let Some(url) = matrix_url_from_args(&argv) {
forward_deeplink(app, &url);
}
}));
}
builder = builder
.invoke_handler(tauri::generate_handler![
set_badge_count,
set_tray_unread,
flash_window,
send_notification,
check_for_update,
install_update,
native::power::set_call_active,
native::jumplist::set_jump_list,
native::thumbbar::set_thumbbar,
native::smtc::set_smtc_call_state,
native::chrome::set_custom_chrome,
native::chrome::window_minimize,
native::chrome::window_toggle_maximize,
native::chrome::window_start_drag,
native::chrome::window_close,
native::toast::show_rich_toast,
])
.plugin(tauri_plugin_localhost::Builder::new(port).build())
.plugin(
// DECORATIONS is excluded: the custom-chrome toggle (set_custom_chrome)
// owns the decorated flag. Letting window-state restore a saved
// decorated=false at startup would re-create the frameless window
// BEFORE lib.rs applies Mica (a broken combination) and before the web
// side has pushed the user's current setting.
tauri_plugin_window_state::Builder::default()
.with_state_flags(
tauri_plugin_window_state::StateFlags::all()
& !tauri_plugin_window_state::StateFlags::DECORATIONS,
)
.build(),
)
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_deep_link::init());
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
}
builder
.setup(move |app| {
// --- System tray: keeps Lotus Chat running in the background so
// notifications keep arriving after the window is closed-to-tray. ---
let base_icon = app.default_window_icon().cloned();
let open_item = MenuItem::with_id(app, "open", "Open Lotus Chat", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit Lotus Chat", true, None::<&str>)?;
let separator = PredefinedMenuItem::separator(app)?;
let tray_menu = Menu::with_items(app, &[&open_item, &separator, &quit_item])?;
let tray = TrayIconBuilder::with_id("main-tray")
.icon(base_icon.clone().expect("bundled window icon"))
.tooltip("Lotus Chat")
.menu(&tray_menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
"open" => show_main(app),
"quit" => app.exit(0),
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
} else {
show_main(app);
}
}
}
})
.build(app)?;
// Keep the tray handle (and base icon pixels) in managed state so
// set_tray_unread can re-render the icon at runtime.
if let Some(img) = base_icon {
let base_rgba = img.rgba().to_vec();
let (width, height) = (img.width(), img.height());
app.manage(TrayUnreadState {
tray,
base_rgba,
width,
height,
});
}
#[cfg(debug_assertions)]
let window_url = WebviewUrl::App(Default::default());
#[cfg(not(debug_assertions))]
let window_url = {
let url = format!("http://localhost:{}", port).parse().unwrap();
WebviewUrl::External(url)
};
let app_handle = app.handle().clone();
let window = WebviewWindowBuilder::new(app, "main".to_string(), window_url)
.title("Lotus Chat")
// First-run defaults; tauri-plugin-window-state restores geometry
// on later launches.
.inner_size(1100.0, 720.0)
.min_inner_size(480.0, 600.0)
.center()
// Start hidden and reveal once the page has painted, to avoid the
// white launch flash.
.visible(false)
.initialization_script(NOTIFICATION_BRIDGE)
.disable_drag_drop_handler()
// P5-42: keep the WebView2 renderer running full-speed while the
// app is closed to the tray, so the Matrix /sync loop and
// notifications aren't throttled/backgrounded by Chromium. Preserves
// Tauri's default WebView2 args (setting this overrides them) and
// appends the Chromium background-throttling disables. Windows-only
// in effect; harmless elsewhere. Does not block system sleep.
.additional_browser_args(
"--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows",
)
.on_page_load(|window, payload| {
if matches!(payload.event(), PageLoadEvent::Finished) {
let _ = window.show();
}
})
.on_new_window(move |url, _features| {
let _ = app_handle.opener().open_url(url.as_str(), None::<&str>);
NewWindowResponse::Deny
})
.build()?;
// Close-to-tray: hide instead of exiting; the app is quit explicitly
// from the tray menu.
let window_for_close = window.clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = window_for_close.hide();
}
});
// Failsafe: never leave the window stuck hidden if the page-load
// event doesn't fire for some reason.
let window_for_show = window.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(8));
let _ = window_for_show.show();
});
// Deep links (matrix:): route both the cold-start case and the
// already-running case (forwarded via single-instance argv) into the
// web client.
{
use tauri_plugin_deep_link::DeepLinkExt;
// Runtime scheme registration is a Linux/Windows-only API; macOS
// registers the scheme from the bundle config at build time.
#[cfg(any(target_os = "linux", target_os = "windows"))]
let _ = app.deep_link().register_all();
let deep_link_handle = app.handle().clone();
app.deep_link().on_open_url(move |event| {
for url in event.urls() {
forward_deeplink(&deep_link_handle, url.as_str());
}
});
if let Some(url) = matrix_url_from_args(&std::env::args().collect::<Vec<_>>()) {
forward_deeplink(&app.handle().clone(), &url);
}
}
// Windows 11 Mica backdrop. The app paints an opaque TDS background,
// so this is subtle (mainly window chrome); harmless if unsupported.
#[cfg(target_os = "windows")]
{
let _ = window_vibrancy::apply_mica(&window, Some(true));
}
// Auto-grant camera, microphone, and notification permissions in WebView2.
#[cfg(target_os = "windows")]
window.with_webview(|webview| {
use webview2_com::{
Microsoft::Web::WebView2::Win32::{
COREWEBVIEW2_PERMISSION_KIND,
COREWEBVIEW2_PERMISSION_KIND_CAMERA,
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
COREWEBVIEW2_PERMISSION_STATE_ALLOW,
},
PermissionRequestedEventHandler,
};
let controller = webview.controller();
if let Ok(core) = unsafe { controller.CoreWebView2() } {
let handler = PermissionRequestedEventHandler::create(Box::new(
|_sender, args| {
if let Some(args) = args {
let mut kind = COREWEBVIEW2_PERMISSION_KIND(0);
unsafe { args.PermissionKind(&mut kind) }?;
if kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE
|| kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA
|| kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS
{
unsafe {
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)
}?;
}
}
Ok(())
},
));
let mut token = Default::default();
let _ = unsafe { core.add_PermissionRequested(&handler, &mut token) };
}
})?;
// Native desktop feature modules (power/call-continuity, etc.).
native::setup(app.handle())?;
Ok(())
})
.run(context)
.expect("error while building tauri application");
}
+27 -2
View File
@@ -1,5 +1,30 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#[cfg(target_os = "macos")]
mod menu;
use tauri::{utils::config::AppUrl, WindowUrl};
fn main() {
app_lib::run();
let port = 44548;
let mut context = tauri::generate_context!();
let url = format!("http://localhost:{}", port).parse().unwrap();
let window_url = WindowUrl::External(url);
// rewrite the config so the IPC is enabled on this URL
context.config_mut().build.dist_dir = AppUrl::Url(window_url.clone());
context.config_mut().build.dev_path = AppUrl::Url(window_url.clone());
let builder = tauri::Builder::default();
#[cfg(target_os = "macos")]
let builder = builder.menu(menu::menu());
builder
.plugin(tauri_plugin_localhost::Builder::new(port).build())
.plugin(tauri_plugin_window_state::Builder::default().build())
.run(context)
.expect("error while building tauri application")
}
+41
View File
@@ -0,0 +1,41 @@
use tauri::{AboutMetadata, Menu, MenuItem, Submenu};
//for macOS
pub(crate) fn menu() -> Menu {
Menu::new()
.add_submenu(Submenu::new(
"Cinny",
Menu::new()
.add_native_item(MenuItem::About(
"Cinny".to_string(),
AboutMetadata::new(),
))
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit),
))
.add_submenu(Submenu::new(
"Edit",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::SelectAll),
))
.add_submenu(Submenu::new(
"View",
Menu::new()
.add_native_item(MenuItem::EnterFullScreen),
))
.add_submenu(Submenu::new(
"Window",
Menu::new()
.add_native_item(MenuItem::Minimize)
.add_native_item(MenuItem::Zoom),
))
}
-85
View File
@@ -1,85 +0,0 @@
//! P5-47 — TDS Custom Window Chrome (opt-in, runtime-reversible).
//!
//! When the user opts into custom window chrome, the web client renders its own
//! `<TitleBar/>` (folds/TDS styled) and we strip the OS-native window frame so
//! the two don't stack. This is entirely opt-in: the window is built with native
//! `decorations(true)` and only `set_custom_chrome(true)` makes it frameless, so
//! the safe default is the untouched native frame.
//!
//! Everything here goes through the cross-platform Tauri v2 window API (plus a
//! Windows-only `window_vibrancy` dance in `set_custom_chrome`, since Mica and a
//! frameless window can't coexist). Each command resolves the "main" window and
//! silently no-ops if it isn't present (e.g. during teardown); the `Result`s are
//! intentionally ignored since a failed chrome tweak should never surface as an
//! error to the user.
use tauri::{AppHandle, Manager};
/// Toggle the native window frame. `enabled` = custom chrome on, which means the
/// OS decorations must come **off** (`set_decorations(!enabled)`). Passing
/// `false` restores the native frame, making the feature fully reversible at
/// runtime without a restart.
#[tauri::command]
pub fn set_custom_chrome(app: AppHandle, enabled: bool) {
if let Some(window) = app.get_webview_window("main") {
// Windows: the Mica backdrop (applied at startup in lib.rs) and a
// frameless window are a known-bad combo — stripping WS_CAPTION under a
// system backdrop glitches the whole surface (black/blank window). Drop
// the backdrop before undecorating, and restore it together with the
// native frame when custom chrome turns off.
#[cfg(target_os = "windows")]
if enabled {
let _ = window_vibrancy::clear_mica(&window);
}
let _ = window.set_decorations(!enabled);
// Re-assert the DWM shadow so a frameless window keeps its drop shadow
// and resize borders on Windows (no-op / harmless elsewhere).
let _ = window.set_shadow(true);
#[cfg(target_os = "windows")]
if !enabled {
let _ = window_vibrancy::apply_mica(&window, Some(true));
}
}
}
/// Minimize the main window (custom titlebar min button).
#[tauri::command]
pub fn window_minimize(app: AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.minimize();
}
}
/// Toggle maximize/restore the main window (custom titlebar max button and
/// drag-region double-click).
#[tauri::command]
pub fn window_toggle_maximize(app: AppHandle) {
if let Some(window) = app.get_webview_window("main") {
if window.is_maximized().unwrap_or(false) {
let _ = window.unmaximize();
} else {
let _ = window.maximize();
}
}
}
/// Begin an OS-level window drag from the custom titlebar drag region. The web
/// side also marks the drag area with `data-tauri-drag-region`; this command is
/// the explicit fallback so behaviour is identical across platforms.
#[tauri::command]
pub fn window_start_drag(app: AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.start_dragging();
}
}
/// Close from the custom titlebar. Mirrors the app's close-to-tray behaviour
/// (see the `CloseRequested` handler in `lib.rs`): we `hide()` the window rather
/// than exiting, so the tray keeps the app running and the tray menu remains the
/// single explicit quit path.
#[tauri::command]
pub fn window_close(app: AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
-100
View File
@@ -1,100 +0,0 @@
//! P5-56 — Windows Focus Assist ↔ Do-Not-Disturb sync.
//!
//! Mirrors the shell's own notification-suppression state so Lotus Chat stops
//! popping desktop notifications while the user is in Focus Assist / Quiet Hours,
//! presenting, gaming full-screen, or otherwise "busy". The web client keeps this
//! in a live jotai atom (`focusAssistActiveAtom`) that the notification gate reads
//! alongside its existing quiet-hours check.
//!
//! Windows: a lightweight background thread polls `SHQueryUserNotificationState`
//! (the same API the shell exposes for "should I show a toast right now?") every
//! ~5 seconds. We prefer a robust poll over hooking shell events — the poll is
//! trivial to reason about and a 5s cadence is more than responsive enough for a
//! notification-suppression hint. We emit **only on a boolean transition**, so the
//! web side gets one event per change rather than a steady heartbeat; the first
//! read always emits so the frontend learns the initial state.
//!
//! Other platforms are a no-op: there's no equivalent cross-platform signal, and
//! the web hook stays unconditional so nothing there needs guarding.
use tauri::AppHandle;
/// Payload for the `focus-assist-changed` DOM event (`{ active: bool }`).
#[cfg(target_os = "windows")]
#[derive(serde::Serialize)]
struct St {
active: bool,
}
/// Called once from lib.rs `native::setup()`. On Windows, spawns the poll
/// thread; elsewhere it does nothing.
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
#[cfg(target_os = "windows")]
{
// Own a handle inside the thread so the poll outlives this call and runs
// for the lifetime of the app.
let app = app.clone();
std::thread::spawn(move || watch_focus_assist(app));
}
#[cfg(not(target_os = "windows"))]
{
// No-op on non-Windows platforms (see module docs). Bind the arg so the
// signature stays identical cross-platform with no unused warning.
let _ = app;
}
Ok(())
}
/// Poll loop, runs on its own thread for the app's lifetime.
#[cfg(target_os = "windows")]
fn watch_focus_assist(app: AppHandle) {
use std::time::Duration;
use windows::Win32::System::Com::{CoInitializeEx, COINIT_MULTITHREADED};
use windows::Win32::UI::Shell::{
SHQueryUserNotificationState, QUNS_BUSY, QUNS_PRESENTATION_MODE, QUNS_QUIET_TIME,
QUNS_RUNNING_D3D_FULL_SCREEN,
};
// Initialize COM for this thread in the multithreaded apartment. This is a
// dedicated thread, so it should be the first to init and succeed (S_FALSE —
// "already initialized, same mode" — also counts as success). If it fails
// outright (e.g. RPC_E_CHANGED_MODE) we can't proceed, so bail.
// Safety: FFI call; `None` reserved param per the API contract.
if unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_err() {
return;
}
// `None` = unknown; the first successful read is treated as a transition so
// the web side always learns the initial suppression state.
let mut last: Option<bool> = None;
loop {
// `SHQueryUserNotificationState` reports the shell's current
// notification-presentation state. Treat the states where the shell
// itself suppresses toasts as "focus/DND active". Skip transient read
// errors without emitting.
// Safety: FFI call; writes the state into the provided out-param.
if let Ok(state) = unsafe { SHQueryUserNotificationState() } {
let active = state == QUNS_QUIET_TIME
|| state == QUNS_PRESENTATION_MODE
|| state == QUNS_RUNNING_D3D_FULL_SCREEN
|| state == QUNS_BUSY;
if last != Some(active) {
last = Some(active);
super::emit_to_web(
&app,
"focus-assist-changed",
&serde_json::to_string(&St { active }).unwrap_or_default(),
);
}
}
std::thread::sleep(Duration::from_secs(5));
}
// Note: the loop never returns, so we intentionally don't call
// `CoUninitialize` here — COM stays initialized for this thread until the
// process exits, which is exactly the desired lifetime.
}
-145
View File
@@ -1,145 +0,0 @@
//! P5-36 — Windows taskbar Jump List ("Recent Rooms").
//!
//! Publishes a custom Jump List category so users can right-click the taskbar
//! (or Start) icon and jump straight into a recently-active room. The web client
//! calls `set_jump_list([{ title, uri }])` from `useTauriJumpList` whenever its
//! recent-room list changes; each `uri` is a `matrix:` deep link.
//!
//! Windows: builds an `ICustomDestinationList` with an `IObjectCollection` of
//! `IShellLinkW` task links. Each link relaunches the current executable with the
//! room's `matrix:` URI as its single argument — the existing deep-link handler
//! in lib.rs (`forward_deeplink` → `lotus-deeplink`) then routes it to the room.
//! The link's visible label is set via `IPropertyStore` + `PKEY_Title`
//! (System.Title) using a `PROPVARIANT`.
//!
//! COM here runs on the command's (thread-pool) thread, so we initialize an STA
//! apartment with `CoInitializeEx` and balance it with `CoUninitialize` only when
//! we were the ones that initialized it (mirrors the COM usage in
//! `set_badge_count`). All COM interfaces are scoped so they release before the
//! apartment is torn down.
//!
//! Other platforms are a no-op (macOS has no direct equivalent; Linux desktop
//! files differ) — the command stays cross-platform so the web side is
//! unconditional.
use tauri::AppHandle;
/// One Jump List entry supplied by the web client. `uri` is a `matrix:` deep
/// link accepted by the deep-link handler in lib.rs.
#[derive(serde::Deserialize)]
pub struct JumpItem {
pub title: String,
pub uri: String,
}
#[tauri::command]
pub fn set_jump_list(app: AppHandle, items: Vec<JumpItem>) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
// `app` is unused on Windows (COM runs on the calling thread); bind it so
// the signature stays identical cross-platform and no warning fires.
let _ = &app;
use std::os::windows::ffi::OsStrExt;
use windows::{
core::{w, Interface, PCWSTR},
Win32::{
Storage::EnhancedStorage::PKEY_Title,
System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, IObjectArray,
IObjectCollection, StructuredStorage::PROPVARIANT, CLSCTX_INPROC_SERVER,
COINIT_APARTMENTTHREADED,
},
UI::Shell::{
DestinationList, EnumerableObjectCollection, ICustomDestinationList,
IShellLinkW, PropertiesSystem::IPropertyStore, ShellLink,
},
},
};
// Wide, NUL-terminated path to the running executable; reused for every
// link's target and icon. Computed before touching COM so a failure here
// doesn't leak an initialized apartment.
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
let exe_wide: Vec<u16> = exe
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
// STA is required for the shell Jump List objects. S_OK means we
// initialized (and must uninitialize); S_FALSE means it was already
// initialized on this thread (still balance it); RPC_E_CHANGED_MODE (an
// error) means don't touch it. Note: `unsafe` does not reach into the
// closure below, so its body carries its own `unsafe` block; the COM
// interfaces it creates are all released (dropped) before we uninitialize.
let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
let result = (|| -> windows::core::Result<()> {
unsafe {
let list: ICustomDestinationList =
CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER)?;
if items.is_empty() {
// Nothing to show — clear any list we previously published.
list.DeleteList(PCWSTR::null())?;
return Ok(());
}
// BeginList hands back the items the user manually removed; we
// don't re-add anything, so we can ignore it. `min_slots` is the
// max entries the shell will display.
let mut min_slots: u32 = 0;
let _removed: IObjectArray = list.BeginList(&mut min_slots)?;
let collection: IObjectCollection =
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
for item in &items {
let link: IShellLinkW =
CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
// Relaunch this exe with the matrix: URI as its only argument.
link.SetPath(PCWSTR(exe_wide.as_ptr()))?;
let arg_wide: Vec<u16> =
item.uri.encode_utf16().chain(std::iter::once(0)).collect();
link.SetArguments(PCWSTR(arg_wide.as_ptr()))?;
// Use the app's own icon for the entry.
link.SetIconLocation(PCWSTR(exe_wide.as_ptr()), 0)?;
// The visible label comes from System.Title on the link's
// property store (a bare IShellLink has no display name).
let store: IPropertyStore = link.cast()?;
// From<&str> builds a VT_LPWSTR PROPVARIANT (what System.Title expects).
let title = PROPVARIANT::from(item.title.as_str());
store.SetValue(&PKEY_Title, &title)?;
store.Commit()?;
collection.AddObject(&link)?;
}
let array: IObjectArray = collection.cast()?;
list.AppendCategory(w!("Recent Rooms"), &array)?;
list.CommitList()?;
Ok(())
}
})();
// All interfaces above are dropped (released) by the time we get here, so
// it's safe to tear the apartment down.
if hr.is_ok() {
unsafe { CoUninitialize() };
}
result.map_err(|e| e.to_string())?;
}
#[cfg(not(target_os = "windows"))]
{
// No-op on non-Windows platforms (see module docs). Bind the args so the
// signature stays identical cross-platform and no unused warnings fire.
let _ = (&app, &items);
}
Ok(())
}
-42
View File
@@ -1,42 +0,0 @@
//! Native desktop feature modules (Lotus Chat).
//!
//! Each feature lives in its own submodule exposing `#[tauri::command]`(s) and,
//! when it needs to register listeners/state, a `setup(&AppHandle)`. lib.rs adds
//! the commands to `generate_handler!` and calls `native::setup()` once during
//! app setup. Windows-only pieces are guarded with `#[cfg(target_os = "windows")]`
//! and compile-verified in CI (Gitea `windows` runner / GitHub `windows-latest`).
use tauri::{AppHandle, Manager};
pub mod chrome;
pub mod focus_assist;
pub mod jumplist;
pub mod network;
pub mod power;
pub mod smtc;
pub mod thumbbar;
pub mod toast;
/// Dispatch a DOM `CustomEvent` to the web client (mirrors `forward_deeplink` in
/// lib.rs) so native modules can push data to the frontend without pulling in
/// `@tauri-apps/api` on the web side. `detail_json` MUST be valid JSON (use
/// `serde_json::to_string`). `event` is a static, trusted name.
#[allow(dead_code)]
pub fn emit_to_web(app: &AppHandle, event: &str, detail_json: &str) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.eval(&format!(
"window.dispatchEvent(new CustomEvent('{event}',{{detail:{detail_json}}}))"
));
}
}
/// Called once from lib.rs `.setup()`. Feature modules that need to register OS
/// listeners or managed state get initialized here. (power/jumplist/chrome are
/// command-only and need no setup.)
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
thumbbar::setup(app)?;
smtc::setup(app)?;
network::setup(app)?;
focus_assist::setup(app)?;
Ok(())
}
-109
View File
@@ -1,109 +0,0 @@
//! P5-49 — Network awareness (Windows connectivity / NCSI).
//!
//! Proactively detects when the machine gains or loses internet connectivity so
//! the web client can surface an offline state and, more importantly, nudge the
//! matrix client to retry its backed-off `/sync` the instant the network comes
//! back instead of waiting out the sync-loop backoff timer.
//!
//! Windows: a lightweight background thread polls the Network List Manager
//! (`INetworkListManager::IsConnectedToInternet`, the same NCSI signal the shell
//! uses) every ~3 seconds. We prefer a robust poll over a COM event sink
//! (`INetworkEvents`) — the poll is far simpler to reason about, needs no
//! connection-point plumbing, and a 3s cadence is more than responsive enough
//! for a "retry sync now" hint. We emit **only on a state transition**, so the
//! web side gets one event per change rather than a steady heartbeat.
//!
//! Other platforms are a no-op: the browser already fires `online`/`offline`
//! events, and the desktop shells (macOS/Linux) can adopt their own reachability
//! APIs later; the web hook stays unconditional so nothing there needs guarding.
use tauri::AppHandle;
/// Payload for the `network-changed` DOM event (`{ online: bool }`).
#[cfg(target_os = "windows")]
#[derive(serde::Serialize)]
struct NetworkState {
online: bool,
}
/// Called once from lib.rs `native::setup()`. On Windows, spawns the poll
/// thread; elsewhere it does nothing.
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
#[cfg(target_os = "windows")]
{
// Own a handle inside the thread so the poll outlives this call and runs
// for the lifetime of the app.
let app = app.clone();
std::thread::spawn(move || watch_network(app));
}
#[cfg(not(target_os = "windows"))]
{
// No-op on non-Windows platforms (see module docs). Bind the arg so the
// signature stays identical cross-platform with no unused warning.
let _ = app;
}
Ok(())
}
/// Poll loop, runs on its own thread for the app's lifetime.
#[cfg(target_os = "windows")]
fn watch_network(app: AppHandle) {
use std::time::Duration;
use windows::Win32::Networking::NetworkListManager::{INetworkListManager, NetworkListManager};
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER,
COINIT_MULTITHREADED,
};
// Initialize COM for this thread in the multithreaded apartment. This is a
// dedicated thread, so it should be the first to init and succeed (S_FALSE —
// "already initialized, same mode" — also counts as success). If it fails
// outright (e.g. RPC_E_CHANGED_MODE) we can't proceed, so bail.
// Safety: FFI call; `None` reserved param per the API contract.
if unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_err() {
return;
}
// Create the Network List Manager COM object (mirrors the `CoCreateInstance`
// idiom in lib.rs `set_badge_count`). If it's unavailable, tear COM back down
// and stop the thread cleanly.
// Safety: standard COM instantiation; type is inferred from the annotation.
let manager: INetworkListManager =
match unsafe { CoCreateInstance(&NetworkListManager, None, CLSCTX_INPROC_SERVER) } {
Ok(manager) => manager,
Err(_) => {
// Safety: balances the successful CoInitializeEx above.
unsafe { CoUninitialize() };
return;
}
};
// `None` = unknown; the first successful read is treated as a transition so
// the web side always learns the initial connectivity state.
let mut last: Option<bool> = None;
loop {
// `IsConnectedToInternet` yields a VARIANT_BOOL (VARIANT_TRUE == -1 when
// connected). Skip transient read errors without emitting.
// Safety: FFI call on a live COM interface owned by this thread.
if let Ok(connected) = unsafe { manager.IsConnectedToInternet() } {
let online = connected.as_bool();
if last != Some(online) {
last = Some(online);
super::emit_to_web(
&app,
"network-changed",
&serde_json::to_string(&NetworkState { online }).unwrap_or_default(),
);
}
}
std::thread::sleep(Duration::from_secs(3));
}
// Note: the loop never returns, so we intentionally don't call
// `CoUninitialize` here — COM stays initialized for this thread until the
// process exits, which is exactly the desired lifetime.
}
-48
View File
@@ -1,48 +0,0 @@
//! P5-46 — System power management (call continuity).
//!
//! Prevents the system from sleeping / turning off the display while a voice or
//! video call is active, then releases the request when the call ends. The web
//! client calls `set_call_active(true|false)` from `useTauriCallPower` as the
//! call-embed atom transitions.
//!
//! Windows: `SetThreadExecutionState`. The request is per-thread and persists
//! until cleared, so we run every set/clear on the **main thread** (via
//! `run_on_main_thread`) to guarantee the clear cancels the matching set even
//! though Tauri commands otherwise run on a pool thread.
//!
//! Other platforms are a no-op for now (macOS would use `IOPMAssertionCreate`,
//! Linux `org.freedesktop.ScreenSaver`/`login1` inhibit) — tracked as a future
//! extension; the command stays cross-platform so the web side is unconditional.
use tauri::AppHandle;
#[tauri::command]
pub fn set_call_active(app: AppHandle, active: bool) {
#[cfg(target_os = "windows")]
{
let _ = app.run_on_main_thread(move || {
use windows::Win32::System::Power::{
SetThreadExecutionState, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED,
};
let flags = if active {
ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
} else {
// Clearing to ES_CONTINUOUS alone releases the sleep/display
// requirement while leaving no lingering per-thread state.
ES_CONTINUOUS
};
// Safety: FFI call with no pointers; returns the previous state,
// which we don't need.
unsafe {
SetThreadExecutionState(flags);
}
});
}
#[cfg(not(target_os = "windows"))]
{
// No-op on non-Windows platforms (see module docs). Bind the args so the
// signature stays identical cross-platform and no unused warnings fire.
let _ = (&app, active);
}
}
-190
View File
@@ -1,190 +0,0 @@
//! P5-43 — System Media Transport Controls (SMTC) call surface (Windows).
//!
//! Surfaces the active voice/video call to the Windows media overlay (the
//! volume flyout / SMTC card) so the user can mute or hang up from the OS
//! media controls. We create the SMTC via the WinRT interop factory
//! (`ISystemMediaTransportControlsInterop::GetForWindow`), keep the object in
//! managed state, and let the web client drive its state through
//! `set_smtc_call_state` as the call-embed atom / mic state changes.
//!
//! Button mapping: **Play/Pause → mute toggle**, **Stop → end call**. Presses
//! are forwarded to the web client as a `smtc-action` DOM CustomEvent (see
//! `super::emit_to_web`) with `action` in `"mute" | "end"`; the web hook
//! (`useTauriSmtc`) translates them into `CallControl.toggleMicrophone()` /
//! `CallEmbed.hangup()`.
//!
//! RUNTIME NOTE: SMTC is designed for real media apps. For a non-media app the
//! card may not actually appear unless the process owns an active audio session
//! recognised by the system. This module prioritises a clean compile and
//! correct WinRT API usage; visibility of the overlay at runtime is uncertain
//! and may depend on the embedded Element Call iframe holding an audio session.
//!
//! Other platforms are a no-op (SMTC is Windows-only); the command keeps an
//! identical cross-platform signature so the web side stays unconditional.
use tauri::AppHandle;
/// Payload for the `smtc-action` DOM event forwarded to the web client.
#[cfg(target_os = "windows")]
#[derive(serde::Serialize)]
struct Ev {
action: String,
}
/// Holds the SMTC object (and its `ButtonPressed` registration token) in Tauri
/// managed state so `set_smtc_call_state` can update it at runtime. Mirrors the
/// `TrayUnreadState` managed-state pattern in lib.rs.
#[cfg(target_os = "windows")]
struct SmtcState {
controls: std::sync::Mutex<Option<windows::Media::SystemMediaTransportControls>>,
// Kept alive so the ButtonPressed handler stays registered for the app's
// lifetime; never unregistered. (windows 0.61 event registrations return a
// plain i64 token — the EventRegistrationToken newtype is gone.)
_token: i64,
}
/// Called once from `native::setup()`. Creates and configures the SMTC on
/// Windows; no-op elsewhere. SMTC init failures are logged and swallowed so a
/// missing/unsupported overlay never blocks app startup.
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
#[cfg(target_os = "windows")]
{
if let Err(err) = init_smtc(app) {
eprintln!("smtc: failed to initialize System Media Transport Controls: {err:?}");
}
}
#[cfg(not(target_os = "windows"))]
{
let _ = app;
}
Ok(())
}
#[cfg(target_os = "windows")]
fn init_smtc(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
use tauri::Manager;
use windows::core::{factory, HSTRING};
use windows::Foundation::TypedEventHandler;
use windows::Media::{
MediaPlaybackStatus, MediaPlaybackType, SystemMediaTransportControls,
SystemMediaTransportControlsButton, SystemMediaTransportControlsButtonPressedEventArgs,
};
use windows::Win32::Foundation::HWND;
use windows::Win32::System::WinRT::ISystemMediaTransportControlsInterop;
let window = app
.get_webview_window("main")
.ok_or("smtc: main window not found")?;
// Match the HWND conversion used by set_badge_count in lib.rs.
let hwnd = HWND(window.hwnd()?.0 as _);
// SMTC has no WinRT constructor; it's obtained per-window via the interop
// factory. `factory::<C, I>()` fetches the activation factory for the
// runtime class `C` cast to the classic COM interop interface `I`.
let interop =
factory::<SystemMediaTransportControls, ISystemMediaTransportControlsInterop>()?;
let controls: SystemMediaTransportControls = unsafe { interop.GetForWindow(hwnd)? };
controls.SetIsEnabled(true)?;
controls.SetIsPlayEnabled(true)?;
controls.SetIsPauseEnabled(true)?;
controls.SetIsStopEnabled(true)?;
// Configure the card metadata once ("In call"); the web side only toggles
// playback status afterwards.
let updater = controls.DisplayUpdater()?;
updater.SetType(MediaPlaybackType::Music)?;
let music = updater.MusicProperties()?;
music.SetTitle(&HSTRING::from("In call"))?;
updater.Update()?;
// Idle until a call becomes active (set_smtc_call_state flips this).
controls.SetPlaybackStatus(MediaPlaybackStatus::Closed)?;
// ButtonPressed → forward a normalized action to the web client.
let app_for_handler = app.clone();
let handler = TypedEventHandler::<
SystemMediaTransportControls,
SystemMediaTransportControlsButtonPressedEventArgs,
>::new(move |_sender, args| {
if let Some(args) = args.as_ref() {
let button = args.Button()?;
let action = if button == SystemMediaTransportControlsButton::Play
|| button == SystemMediaTransportControlsButton::Pause
{
Some("mute")
} else if button == SystemMediaTransportControlsButton::Stop {
Some("end")
} else {
None
};
if let Some(action) = action {
let payload = serde_json::to_string(&Ev {
action: action.to_string(),
})
.unwrap_or_default();
super::emit_to_web(&app_for_handler, "smtc-action", &payload);
}
}
Ok(())
});
let token = controls.ButtonPressed(&handler)?;
app.manage(SmtcState {
controls: std::sync::Mutex::new(Some(controls)),
_token: token,
});
Ok(())
}
/// Reflect the call state onto the SMTC. When `active`, enable the controls and
/// set playback status to Playing (unmuted) / Paused (muted); when inactive,
/// mark the card Closed and disable it. Windows-only; no-op elsewhere.
#[tauri::command]
pub fn set_smtc_call_state(app: AppHandle, active: bool, muted: bool) {
#[cfg(target_os = "windows")]
{
use tauri::Manager;
if let Some(state) = app.try_state::<SmtcState>() {
if let Ok(guard) = state.controls.lock() {
if let Some(controls) = guard.as_ref() {
let _ = apply_call_state(controls, active, muted);
}
}
}
}
#[cfg(not(target_os = "windows"))]
{
// No-op off Windows; bind args so the signature is identical everywhere
// and no unused warnings fire.
let _ = (&app, active, muted);
}
}
#[cfg(target_os = "windows")]
fn apply_call_state(
controls: &windows::Media::SystemMediaTransportControls,
active: bool,
muted: bool,
) -> windows::core::Result<()> {
use windows::Media::MediaPlaybackStatus;
if active {
controls.SetIsEnabled(true)?;
let status = if muted {
MediaPlaybackStatus::Paused
} else {
MediaPlaybackStatus::Playing
};
controls.SetPlaybackStatus(status)?;
} else {
controls.SetPlaybackStatus(MediaPlaybackStatus::Closed)?;
controls.SetIsEnabled(false)?;
}
Ok(())
}
-382
View File
@@ -1,382 +0,0 @@
//! P5-44 — Taskbar thumbnail toolbar (call controls).
//!
//! While a voice/video call is active the web client calls `set_thumbbar` from
//! `useTauriThumbbar`, which mirrors the call-embed atom + mic/sound state onto
//! three buttons on the taskbar thumbnail toolbar: **Mute/Unmute**,
//! **Deafen/Undeafen** and **End Call**. Clicking a button pushes a
//! `thumbbar-action` DOM event back to the web side (`"mute" | "deafen" | "end"`)
//! which drives the real call controls.
//!
//! Windows: `ITaskbarList3::ThumbBarAddButtons` (first call for the window) then
//! `ThumbBarUpdateButtons` (subsequent calls) — mirrors the COM + GDI/HICON idiom
//! in `set_badge_count`. Thumb-button clicks arrive as `WM_COMMAND` with
//! `HIWORD(wParam) == THBN_CLICKED`, so we subclass the main window (installed
//! once in `setup`) to catch them. The main window HWND comes from the "main"
//! webview window; the "buttons added" flag lives in managed `ThumbbarState`
//! (like lib.rs's `TrayUnreadState`) so add-vs-update works across calls.
//!
//! Other platforms are a no-op — the command stays cross-platform so the web
//! side is unconditional.
use tauri::{AppHandle, Manager};
/// Managed state shared with lib.rs (registered in `setup`). Only the Windows
/// path reads `added`; kept cross-platform so `set_thumbbar` can inject it.
#[derive(Default)]
pub struct ThumbbarState {
#[allow(dead_code)]
added: std::sync::atomic::AtomicBool,
}
// Thumb-button ids (LOWORD of wParam on WM_COMMAND / THBN_CLICKED).
#[cfg(target_os = "windows")]
const BTN_MUTE: u32 = 1;
#[cfg(target_os = "windows")]
const BTN_DEAFEN: u32 = 2;
#[cfg(target_os = "windows")]
const BTN_END: u32 = 3;
/// HIWORD(wParam) value on a thumb-button click (CommCtrl `THBN_CLICKED`).
#[cfg(target_os = "windows")]
const THBN_CLICKED: u16 = 0x1800;
/// uIdSubclass passed to SetWindowSubclass — identifies our subclass instance.
#[cfg(target_os = "windows")]
const SUBCLASS_ID: usize = 1;
/// Payload emitted to the web on a thumb-button click.
#[cfg(target_os = "windows")]
#[derive(serde::Serialize)]
struct Action<'a> {
action: &'a str,
}
/// Build a single THUMBBUTTON, attaching an icon (and the THB_ICON mask) when one
/// was created. Always carries a tooltip and enabled/hidden flags.
#[cfg(target_os = "windows")]
fn thumb_button(
id: u32,
hidden: bool,
tip: &str,
icon: Option<windows::Win32::UI::WindowsAndMessaging::HICON>,
) -> windows::Win32::UI::Shell::THUMBBUTTON {
use windows::Win32::UI::Shell::{
THUMBBUTTON, THUMBBUTTONFLAGS, THUMBBUTTONMASK, THBF_ENABLED, THBF_HIDDEN, THB_FLAGS,
THB_ICON, THB_TOOLTIP,
};
use windows::Win32::UI::WindowsAndMessaging::HICON;
let mut mask: THUMBBUTTONMASK = THB_TOOLTIP | THB_FLAGS;
let flags: THUMBBUTTONFLAGS = if hidden { THBF_HIDDEN } else { THBF_ENABLED };
let mut hicon = HICON::default();
if let Some(i) = icon {
mask = mask | THB_ICON;
hicon = i;
}
let mut sz_tip = [0u16; 260];
for (dst, ch) in sz_tip.iter_mut().zip(tip.encode_utf16().take(259)) {
*dst = ch;
}
THUMBBUTTON {
dwMask: mask,
iId: id,
iBitmap: 0,
hIcon: hicon,
szTip: sz_tip,
dwFlags: flags,
}
}
/// Update (or hide) the three thumb-toolbar buttons for the given call state.
#[tauri::command]
pub fn set_thumbbar(
app: AppHandle,
state: tauri::State<'_, ThumbbarState>,
active: bool,
muted: bool,
deafened: bool,
) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
use std::sync::atomic::Ordering;
use windows::Win32::{
Foundation::HWND,
System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
UI::{
Shell::{ITaskbarList3, TaskbarList},
WindowsAndMessaging::DestroyIcon,
},
};
// Nothing to do (and nothing to hide) if a toolbar was never added.
if !active && !state.added.load(Ordering::SeqCst) {
return Ok(());
}
let window = app
.get_webview_window("main")
.ok_or_else(|| "no main window".to_string())?;
let hwnd = HWND(window.hwnd().map_err(|e| e.to_string())?.0 as _);
let mic_icon = make_icon(Glyph::Mic, muted);
let deaf_icon = make_icon(Glyph::Head, deafened);
let end_icon = make_icon(Glyph::End, false);
let buttons = [
thumb_button(BTN_MUTE, !active, if muted { "Unmute" } else { "Mute" }, mic_icon),
thumb_button(
BTN_DEAFEN,
!active,
if deafened { "Undeafen" } else { "Deafen" },
deaf_icon,
),
thumb_button(BTN_END, !active, "End Call", end_icon),
];
let result = unsafe {
let taskbar: ITaskbarList3 = CoCreateInstance(&TaskbarList, None, CLSCTX_INPROC_SERVER)
.map_err(|e| e.to_string())?;
taskbar.HrInit().map_err(|e| e.to_string())?;
let r = if state.added.load(Ordering::SeqCst) {
taskbar.ThumbBarUpdateButtons(hwnd, &buttons)
} else {
let r = taskbar.ThumbBarAddButtons(hwnd, &buttons);
if r.is_ok() {
state.added.store(true, Ordering::SeqCst);
}
r
};
r.map_err(|e| e.to_string())
};
// The shell copies the icons on add/update, so release ours (mirrors the
// DestroyIcon after SetOverlayIcon in set_badge_count).
for icon in [mic_icon, deaf_icon, end_icon].into_iter().flatten() {
unsafe {
let _ = DestroyIcon(icon);
}
}
result?;
}
#[cfg(not(target_os = "windows"))]
{
// No-op elsewhere; bind args so the signature stays identical and no
// unused warnings fire.
let _ = (&app, &state, active, muted, deafened);
}
Ok(())
}
/// Which glyph a thumb-button icon draws.
#[cfg(target_os = "windows")]
#[derive(Clone, Copy)]
enum Glyph {
Mic,
Head,
End,
}
/// Draw a simple white monochrome glyph onto a 32x32 32-bpp DIB and wrap it in an
/// `HICON`. Mirrors the CreateDIBSection → alpha-fixup → CreateIconIndirect idiom
/// in `set_badge_count`. Returns `None` on any GDI failure (the button is then
/// added tooltip-only). `slashed` overlays a transparent diagonal cut to signal
/// the muted / deafened state.
#[cfg(target_os = "windows")]
fn make_icon(
glyph: Glyph,
slashed: bool,
) -> Option<windows::Win32::UI::WindowsAndMessaging::HICON> {
use windows::core::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Graphics::Gdi::{
Arc, CreateBitmap, CreateCompatibleDC, CreateDIBSection, CreatePen, CreateSolidBrush,
DeleteDC, DeleteObject, Ellipse, GetDC, LineTo, MoveToEx, ReleaseDC, RoundRect,
SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, PS_SOLID,
};
use windows::Win32::UI::WindowsAndMessaging::{CreateIconIndirect, ICONINFO};
unsafe {
let size = 32i32;
let hdc_screen = GetDC(None);
let hdc = CreateCompatibleDC(Some(hdc_screen));
let bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: size,
biHeight: -size,
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
..Default::default()
},
..Default::default()
};
let mut bits: *mut std::ffi::c_void = std::ptr::null_mut();
let hbm_color = CreateDIBSection(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0).ok()?;
if !bits.is_null() {
std::ptr::write_bytes(bits as *mut u8, 0, (size * size * 4) as usize);
}
let old_bm = SelectObject(hdc, hbm_color.into());
let white = COLORREF(0x00FF_FFFF);
let hbrush = CreateSolidBrush(white);
let old_brush = SelectObject(hdc, hbrush.into());
let hpen = CreatePen(PS_SOLID, 2, white);
let old_pen = SelectObject(hdc, hpen.into());
match glyph {
Glyph::Mic => {
// Capsule mic head + stand.
let _ = RoundRect(hdc, 13, 5, 19, 19, 6, 6);
let _ = MoveToEx(hdc, 16, 19, None);
let _ = LineTo(hdc, 16, 25);
let _ = MoveToEx(hdc, 11, 25, None);
let _ = LineTo(hdc, 21, 25);
}
Glyph::Head => {
// Headphone band + two ear cups.
let _ = Arc(hdc, 6, 7, 26, 27, 6, 17, 26, 17);
let _ = RoundRect(hdc, 6, 16, 11, 26, 2, 2);
let _ = RoundRect(hdc, 21, 16, 26, 26, 2, 2);
}
Glyph::End => {
// Filled disc (end-call button).
let _ = Ellipse(hdc, 6, 6, 26, 26);
}
}
if slashed {
// Draw the slash in black (pixel 0), which the alpha fixup below
// leaves fully transparent — carving a visible diagonal gap.
let black_pen = CreatePen(PS_SOLID, 4, COLORREF(0));
let prev = SelectObject(hdc, black_pen.into());
let _ = MoveToEx(hdc, 6, 6, None);
let _ = LineTo(hdc, 26, 26);
SelectObject(hdc, prev);
let _ = DeleteObject(black_pen.into());
}
SelectObject(hdc, old_brush);
SelectObject(hdc, old_pen);
SelectObject(hdc, old_bm);
let _ = DeleteObject(hbrush.into());
let _ = DeleteObject(hpen.into());
// GDI leaves alpha at 0; mark every painted pixel opaque so Windows uses
// per-pixel alpha instead of the opaque mask (same fix as set_badge_count).
let pixel_count = (size * size) as usize;
let pixels = std::slice::from_raw_parts_mut(bits as *mut u32, pixel_count);
for pixel in pixels.iter_mut() {
if *pixel != 0 {
*pixel |= 0xFF00_0000u32;
}
}
let hbm_mask = CreateBitmap(size, size, 1, 1, None);
if hbm_mask.0 as usize == 0 {
let _ = DeleteObject(hbm_color.into());
let _ = DeleteDC(hdc);
let _ = ReleaseDC(None, hdc_screen);
return None;
}
let icon_info = ICONINFO {
fIcon: BOOL(1),
xHotspot: 0,
yHotspot: 0,
hbmMask: hbm_mask,
hbmColor: hbm_color,
};
let hicon = CreateIconIndirect(&icon_info).ok();
let _ = DeleteObject(hbm_color.into());
let _ = DeleteObject(hbm_mask.into());
let _ = DeleteDC(hdc);
let _ = ReleaseDC(None, hdc_screen);
hicon
}
}
/// Window subclass proc: catches thumb-button clicks (`WM_COMMAND` /
/// `THBN_CLICKED`) and forwards them to the web as `thumbbar-action`. `dwrefdata`
/// is a leaked `Box<AppHandle>` installed by `setup`; it is reclaimed on
/// `WM_NCDESTROY`.
#[cfg(target_os = "windows")]
unsafe extern "system" fn subclass_proc(
hwnd: windows::Win32::Foundation::HWND,
umsg: u32,
wparam: windows::Win32::Foundation::WPARAM,
lparam: windows::Win32::Foundation::LPARAM,
_uidsubclass: usize,
dwrefdata: usize,
) -> windows::Win32::Foundation::LRESULT {
use windows::Win32::Foundation::LRESULT;
use windows::Win32::UI::Shell::{DefSubclassProc, RemoveWindowSubclass};
use windows::Win32::UI::WindowsAndMessaging::{WM_COMMAND, WM_NCDESTROY};
match umsg {
WM_COMMAND => {
let w = wparam.0;
let notif = ((w >> 16) & 0xFFFF) as u16;
let id = (w & 0xFFFF) as u32;
if notif == THBN_CLICKED {
let action = match id {
BTN_MUTE => Some("mute"),
BTN_DEAFEN => Some("deafen"),
BTN_END => Some("end"),
_ => None,
};
if let Some(action) = action {
if dwrefdata != 0 {
// Borrow (do not take ownership of) the leaked AppHandle.
let app = &*(dwrefdata as *const AppHandle);
let detail =
serde_json::to_string(&Action { action }).unwrap_or_default();
super::emit_to_web(app, "thumbbar-action", &detail);
}
return LRESULT(0);
}
}
DefSubclassProc(hwnd, umsg, wparam, lparam)
}
WM_NCDESTROY => {
let _ = RemoveWindowSubclass(hwnd, Some(subclass_proc), SUBCLASS_ID);
if dwrefdata != 0 {
drop(Box::from_raw(dwrefdata as *mut AppHandle));
}
DefSubclassProc(hwnd, umsg, wparam, lparam)
}
_ => DefSubclassProc(hwnd, umsg, wparam, lparam),
}
}
/// Called once from `native::setup`. Registers `ThumbbarState` and, on Windows,
/// subclasses the main window so thumb-button clicks reach the web client.
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
app.manage(ThumbbarState::default());
#[cfg(target_os = "windows")]
{
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Shell::SetWindowSubclass;
if let Some(window) = app.get_webview_window("main") {
if let Ok(handle) = window.hwnd() {
let hwnd = HWND(handle.0 as _);
// Leak an AppHandle for the proc; reclaimed on WM_NCDESTROY.
let refdata = Box::into_raw(Box::new(app.clone())) as usize;
unsafe {
let _ = SetWindowSubclass(hwnd, Some(subclass_proc), SUBCLASS_ID, refdata);
}
}
}
}
Ok(())
}
-245
View File
@@ -1,245 +0,0 @@
//! P5-41 — Native WinRT toast notifications (+ P5-35 click-opens-room, P5-41 quick reply).
//!
//! The web notification bridge calls `show_rich_toast` (see lib.rs
//! `NOTIFICATION_BRIDGE`) instead of the basic plugin notification so desktop
//! notifications gain a text reply box and a body-click that reopens the room.
//!
//! Windows: we build a `Windows.UI.Notifications.ToastNotification` from a toast
//! XML document (`Windows.Data.Xml.Dom.XmlDocument`) carrying the title + body,
//! an inline `<input id="reply" type="text"/>` and a Send `<action>`. Because the
//! app lives in the tray (always alive) we subscribe to the toast's **in-process**
//! `Activated` event rather than relying on COM activation: the handler downcasts
//! the event args to `ToastActivatedEventArgs`, reads the reply text from
//! `UserInput()` (keyed `"reply"`) and forwards it to the web client. A body click
//! (no reply text) forwards the launch `path` so the web side can route to the
//! room. Live `ToastNotification` objects are parked in a process-global `Vec`
//! (behind a `Mutex`) so their handlers survive until the toast is dismissed.
//!
//! If ANY WinRT step fails (most importantly: no registered AppUserModelID — see
//! the runtime note below), we fall back to the plain `tauri-plugin-notification`
//! notification so notifications always work.
//!
//! Other platforms always take the fallback path; the command keeps an identical
//! cross-platform signature so the web bridge stays unconditional.
//!
//! RUNTIME NOTE (AppUserModelID): WinRT toasts require the process to run under an
//! AppUserModelID that maps to a Start-menu shortcut. The installed app's bundle
//! id is `org.lotusguild.lotus-chat`; if no matching shortcut/AUMID is registered,
//! `CreateToastNotifier()` / `Show()` will error and we silently fall back. Wiring
//! `SetCurrentProcessExplicitAppUserModelID` (+ shortcut install) is handled
//! separately.
use tauri::AppHandle;
/// Show a rich desktop notification. On Windows this is a WinRT toast with a
/// reply box and click-to-open; elsewhere (or on any WinRT error) it degrades to
/// a basic plugin notification. `room_id` is the raw Matrix room id used for the
/// reply payload; `path` is the web hash route used for a body click.
#[tauri::command]
pub fn show_rich_toast(
app: AppHandle,
title: String,
body: Option<String>,
room_id: Option<String>,
path: Option<String>,
) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
match show_windows_toast(
&app,
&title,
body.as_deref(),
room_id.as_deref(),
path.as_deref(),
) {
Ok(()) => return Ok(()),
Err(err) => {
// Most commonly a missing AppUserModelID (see module note). Fall
// through to the plugin notification so the user still sees it.
eprintln!("toast: WinRT toast failed, falling back to plugin: {err:?}");
}
}
}
// Bind the routing args so the signature is identical cross-platform and no
// unused warnings fire on the fallback (non-Windows) path.
let _ = (&room_id, &path);
show_fallback(&app, &title, body.as_deref())
}
/// Cross-platform fallback: a basic notification via `tauri-plugin-notification`
/// (mirrors `send_notification` in lib.rs). Used off Windows and whenever the
/// WinRT toast path errors.
fn show_fallback(app: &AppHandle, title: &str, body: Option<&str>) -> Result<(), String> {
use tauri_plugin_notification::NotificationExt;
let mut builder = app.notification().builder().title(title);
if let Some(b) = body {
builder = builder.body(b);
}
builder.show().map_err(|e| e.to_string())
}
/// Process-global store keeping live `ToastNotification` objects (and therefore
/// their `Activated`/`Dismissed` handler registrations) alive until dismissed.
/// Lazily initialized so no `native::setup()` wiring is required.
#[cfg(target_os = "windows")]
fn toast_store() -> &'static std::sync::Mutex<Vec<windows::UI::Notifications::ToastNotification>> {
static STORE: std::sync::OnceLock<
std::sync::Mutex<Vec<windows::UI::Notifications::ToastNotification>>,
> = std::sync::OnceLock::new();
STORE.get_or_init(|| std::sync::Mutex::new(Vec::new()))
}
/// Escape text for inclusion in the toast XML (attribute or element content).
#[cfg(target_os = "windows")]
fn xml_escape(input: &str) -> String {
input
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
#[cfg(target_os = "windows")]
fn show_windows_toast(
app: &AppHandle,
title: &str,
body: Option<&str>,
room_id: Option<&str>,
path: Option<&str>,
) -> windows::core::Result<()> {
use windows::core::{HSTRING, IInspectable, Interface};
use windows::Data::Xml::Dom::XmlDocument;
use windows::Foundation::TypedEventHandler;
use windows::UI::Notifications::{
ToastActivatedEventArgs, ToastDismissedEventArgs, ToastNotification,
ToastNotificationManager,
};
// A body click carries the launch arguments back to us; prefer the web hash
// route (`path`), falling back to the raw room id so clicks are never inert.
let launch = path.or(room_id).unwrap_or_default();
let body_line = match body {
Some(b) if !b.is_empty() => format!("<text>{}</text>", xml_escape(b)),
_ => String::new(),
};
// ToastGeneric visual + an inline reply input and a foreground Send action.
// `hint-inputId="reply"` binds the Send button to the text box so the reply
// text arrives in `UserInput()` keyed "reply".
let xml = format!(
r#"<toast activationType="foreground" launch="{launch}">
<visual>
<binding template="ToastGeneric">
<text>{title}</text>
{body_line}
</binding>
</visual>
<actions>
<input id="reply" type="text" placeHolder="Reply..."/>
<action content="Send" arguments="reply" activationType="foreground" hint-inputId="reply"/>
</actions>
</toast>"#,
launch = xml_escape(launch),
title = xml_escape(title),
body_line = body_line,
);
let doc = XmlDocument::new()?;
doc.LoadXml(&HSTRING::from(xml))?;
let toast = ToastNotification::CreateToastNotification(&doc)?;
// In-process activation: the app is always alive in the tray, so we handle
// clicks/replies directly instead of via COM activation.
let app_activated = app.clone();
let room_id_owned = room_id.map(|s| s.to_string());
let path_owned = path.map(|s| s.to_string());
let activated = TypedEventHandler::<ToastNotification, IInspectable>::new(
move |_sender, args| {
let Some(args) = args.as_ref() else {
return Ok(());
};
let Ok(activated_args) = args.cast::<ToastActivatedEventArgs>() else {
return Ok(());
};
// Extract the reply text (if the Send action / input was used).
let reply = read_reply(&activated_args).unwrap_or_default();
if !reply.is_empty() {
// Quick reply: forward the room id + text to the web client.
let payload = serde_json::json!({
"roomId": room_id_owned.as_deref(),
"text": reply,
})
.to_string();
super::emit_to_web(&app_activated, "lotus-notification-reply", &payload);
} else {
// Plain body click: forward the launch path so the web routes to it.
let payload = serde_json::json!({
"path": path_owned.as_deref(),
})
.to_string();
super::emit_to_web(&app_activated, "lotus-notification-activate", &payload);
}
Ok(())
},
);
let _ = toast.Activated(&activated)?;
// Prune the store once the toast leaves the action center so we don't leak
// handler registrations for the app's lifetime.
let dismissed = TypedEventHandler::<ToastNotification, ToastDismissedEventArgs>::new(
move |sender, _args| {
if let Some(sender) = sender.as_ref() {
if let Ok(mut store) = toast_store().lock() {
store.retain(|t| t != sender);
}
}
Ok(())
},
);
let _ = toast.Dismissed(&dismissed)?;
// Keep the toast (and its handlers) alive until dismissed.
if let Ok(mut store) = toast_store().lock() {
store.push(toast.clone());
}
// No AUMID argument: relies on the process AppUserModelID (see module note).
let notifier = ToastNotificationManager::CreateToastNotifier()?;
notifier.Show(&toast)?;
Ok(())
}
/// Read the quick-reply text from a toast activation. Returns `None` when the
/// toast was activated without submitting the "reply" input (a plain click).
#[cfg(target_os = "windows")]
fn read_reply(
args: &windows::UI::Notifications::ToastActivatedEventArgs,
) -> Option<String> {
use windows::core::{HSTRING, Interface};
use windows::Foundation::IReference;
// UserInput() returns a ValueSet; windows 0.61 exposes its IMap methods
// (HasKey/Lookup) directly on the class (the generic IMap interface itself
// moved to the separate windows-collections crate). The text input value is
// boxed as an IReference<HSTRING>.
let inputs = args.UserInput().ok()?;
let key = HSTRING::from("reply");
if !inputs.HasKey(&key).ok()? {
return None;
}
let value = inputs.Lookup(&key).ok()?;
let reference: IReference<HSTRING> = value.cast().ok()?;
let text = reference.Value().ok()?.to_string();
if text.is_empty() {
None
} else {
Some(text)
}
}
+42 -41
View File
@@ -1,22 +1,19 @@
{
"package": {
"productName": "Cinny",
"version": "4.10.5"
},
"build": {
"distDir": "../cinny/dist",
"devPath": "http://localhost:8080",
"beforeDevCommand": "cd cinny && npm start",
"beforeBuildCommand": "cd cinny && npm run build"
},
"tauri": {
"bundle": {
"active": true,
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"type": "downloadBootstrapper"
},
"nsis": {
"installMode": "currentUser"
},
"wix": {
"bannerPath": "wix/banner.bmp",
"dialogImagePath": "wix/dialogImage.bmp"
}
},
"identifier": "in.cinny.app",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -30,6 +27,9 @@
"category": "SocialNetworking",
"shortDescription": "Yet another matrix client",
"longDescription": "",
"deb": {
"depends": []
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
@@ -38,39 +38,40 @@
"providerShortName": null,
"entitlements": null
},
"linux": {
"deb": {
"depends": []
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"wix": {
"bannerPath": "wix/banner.bmp",
"dialogImagePath": "wix/dialogImage.bmp"
}
}
},
"createUpdaterArtifacts": "v1Compatible"
},
"build": {
"beforeBuildCommand": "cd cinny && npm run build",
"frontendDist": "../cinny/dist",
"beforeDevCommand": "cd cinny && npm start",
"devUrl": "http://localhost:8080"
},
"productName": "Lotus Chat",
"mainBinaryName": "cinny",
"version": "4.12.2",
"identifier": "org.lotusguild.lotus-chat",
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM1N0Y0RThCQTJEQzY1NTkKUldSWlpkeWlpMDUvTlVjejMzN0E1U0FiaVpLK05QVkRXdWlMMm1NNUprMXAvTGZSbU5maVovNmwK",
"active": true,
"endpoints": [
"https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest/release.json"
]
"https://github.com/cinnyapp/cinny-desktop/releases/download/tauri/release.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE2NDc3NDBGMTAzNTk1NUYKUldSZmxUVVFEM1JIRnRuMjVRTkFOQ21lUFI5KzRMU0s4OWtBS1RNRUVCNE9LcE9GcExNZ2M2NHoK"
},
"deep-link": {
"desktop": {
"schemes": ["matrix"]
}
}
"allowlist": {
"all": true
},
"app": {
"windows": [
{
"title": "Cinny",
"width": 1280,
"height": 905,
"center": true,
"resizable": true,
"fullscreen": false,
"fileDropEnabled": false
}
],
"security": {
"csp": "default-src 'self' blob: data: filesystem: ws: wss: http: https: tauri:; script-src 'self' 'unsafe-eval' 'unsafe-inline' blob: data: filesystem: ws: wss: http: https: tauri:; style-src 'self' 'unsafe-inline' blob: data: filesystem: http: https:; img-src 'self' data: blob: filesystem: http: https:; media-src 'self' blob: data: mediastream:; connect-src 'self' blob: ipc: ws: wss: http: https: http://ipc.localhost"
"csp": "script-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
}
}
}
Binary file not shown.
-53
View File
@@ -1,53 +0,0 @@
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
int main(int argc, char **argv) {
const char *appdir = "/root/linuxdeploy-root";
char apprun[256], ldbin[256], new_path[4096], new_ldpath[4096];
struct stat st;
snprintf(apprun, sizeof(apprun), "%s/AppRun", appdir);
snprintf(ldbin, sizeof(ldbin), "%s/usr/bin/linuxdeploy", appdir);
/* Strip --appimage-extract-and-run: AppImage runtime flag, not a linuxdeploy flag */
char **new_argv = malloc((argc + 1) * sizeof(char *));
int new_argc = 0;
new_argv[new_argc++] = argv[0];
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--appimage-extract-and-run") != 0)
new_argv[new_argc++] = argv[i];
}
new_argv[new_argc] = NULL;
setenv("APPDIR", appdir, 1);
char *old_path = getenv("PATH");
snprintf(new_path, sizeof(new_path), "%s/usr/bin:%s", appdir, old_path ? old_path : "");
setenv("PATH", new_path, 1);
char *old_ldpath = getenv("LD_LIBRARY_PATH");
snprintf(new_ldpath, sizeof(new_ldpath), "%s/usr/lib:%s/usr/lib/x86_64-linux-gnu:%s",
appdir, appdir, old_ldpath ? old_ldpath : "");
setenv("LD_LIBRARY_PATH", new_ldpath, 1);
/* Write diagnostic log visible in the always() post-step */
FILE *log = fopen("/tmp/ld-wrapper.log", "w");
if (log) {
fprintf(log, "APPDIR=%s\n", appdir);
fprintf(log, "AppRun exists: %s\n", stat(apprun, &st) == 0 ? "yes" : "NO");
fprintf(log, "linuxdeploy exists: %s\n", stat(ldbin, &st) == 0 ? "yes" : "NO");
fprintf(log, "argc=%d new_argc=%d\n", argc, new_argc);
fprintf(log, "args:");
for (int i = 0; i < new_argc; i++) fprintf(log, " [%s]", new_argv[i]);
fprintf(log, "\nPATH=%s\n", getenv("PATH") ? getenv("PATH") : "(null)");
fclose(log);
}
if (stat(ldbin, &st) == 0)
execv(ldbin, new_argv);
execv(apprun, new_argv);
return 1;
}