mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-06-30 02:52:05 +08:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0681f6540d | |||
| d843901550 | |||
| b41630ebb4 | |||
| a815eecc73 | |||
| 7e0984fe9c | |||
| 9c28cd2ab2 | |||
| 3c4000bdbb | |||
| d8e9fa5bba | |||
| e026491b9f | |||
| 009699b375 | |||
| 3677588a3d | |||
| 3d280053a7 | |||
| fbde3f6a47 | |||
| 1c1a33fdce | |||
| fea238722d | |||
| 178d958c08 | |||
| f9633f366b | |||
| 7a81f3e767 | |||
| bbbbbf7927 | |||
| 5735f84008 | |||
| fe0cf42e5d | |||
| 04100584ce | |||
| 9d3a422916 | |||
| 65d8284fb8 | |||
| eea107db66 | |||
| 67812957db | |||
| 69d77e59cc | |||
| efcebfd82c | |||
| 32345f93bf | |||
| 193e4bd5bf | |||
| 53edf3d57e | |||
| 895e297771 |
+146
-22
@@ -7,6 +7,11 @@ on:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
patch:
|
||||
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
|
||||
required: false
|
||||
default: "0"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -37,21 +42,53 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
VERSION_NUM="${VERSION#v}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
BUILD_DATE_OVERRIDE=""
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set legal Debian version
|
||||
shell: bash
|
||||
run: |
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
BUILD_DATE="${BUILD_DATE_OVERRIDE}"
|
||||
if [[ -z "${BUILD_DATE}" ]]; then
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
fi
|
||||
|
||||
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
|
||||
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
|
||||
VERSION_BASE="0.0.0-${VERSION_BASE}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
|
||||
@@ -67,6 +104,7 @@ jobs:
|
||||
CUDA_PATH: /usr/local/cuda
|
||||
XMAKE_GLOBALDIR: /data
|
||||
run: |
|
||||
apt install -y libxft-dev
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
|
||||
xmake b -vy --root crossdesk
|
||||
|
||||
@@ -102,14 +140,43 @@ jobs:
|
||||
steps:
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
|
||||
echo "VERSION_NUM=${VERSION_NUM}"
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
@@ -163,10 +230,38 @@ jobs:
|
||||
$version = $ref -replace '^refs/(tags|heads)/', ''
|
||||
$version = $version -replace '^v', ''
|
||||
$version = $version -replace '/', '-'
|
||||
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
|
||||
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
|
||||
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV
|
||||
$PATCH_NUMBER = "${{ github.event.inputs.patch }}"
|
||||
|
||||
if ($PATCH_NUMBER -notmatch '^[0-9]+$') {
|
||||
$PATCH_NUMBER = "0"
|
||||
}
|
||||
|
||||
if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
$BUILD_DATE = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
$PATCH_NUMBER = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
}
|
||||
|
||||
if ($PATCH_NUMBER -ne "0") {
|
||||
$VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE"
|
||||
} else {
|
||||
$VERSION_NUM = "v$version-$BUILD_DATE"
|
||||
}
|
||||
|
||||
echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV
|
||||
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
|
||||
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
@@ -239,8 +334,7 @@ jobs:
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd "${{ github.workspace }}\scripts\windows"
|
||||
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
|
||||
& "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
|
||||
- name: Build Portable CrossDesk
|
||||
run: |
|
||||
@@ -310,19 +404,47 @@ jobs:
|
||||
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d)
|
||||
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}"
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
VERSION_WITH_V="v${VERSION_NUM}"
|
||||
VERSION_ONLY="${VERSION#v}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
@@ -380,8 +502,10 @@ jobs:
|
||||
run: |
|
||||
cat > version.json << EOF
|
||||
{
|
||||
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
|
||||
"latest_version": "${{ steps.version.outputs.VERSION_NUM }}",
|
||||
"version": "${{ steps.version.outputs.VERSION_NUM }}",
|
||||
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
|
||||
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
|
||||
"releaseName": "",
|
||||
"releaseNotes": "",
|
||||
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
schedule:
|
||||
# run every day at midnight
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
@@ -15,19 +16,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check inactive issues and close them
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
script: |
|
||||
const { data: issues } = await github.rest.issues.listForRepo({
|
||||
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const now = Date.now();
|
||||
|
||||
// paginate through all open issues (listForRepo also returns PRs)
|
||||
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const now = new Date().getTime();
|
||||
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
|
||||
for (const issue of issues) {
|
||||
// skip pull requests (they are also returned by listForRepo)
|
||||
if (issue.pull_request) continue;
|
||||
@@ -38,26 +41,14 @@ jobs:
|
||||
continue;
|
||||
}
|
||||
|
||||
// fetch comments for this issue
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
// determine the "last activity" time
|
||||
let lastActivityTime;
|
||||
if (comments.length > 0) {
|
||||
const lastComment = comments[comments.length - 1];
|
||||
lastActivityTime = new Date(lastComment.updated_at).getTime();
|
||||
} else {
|
||||
lastActivityTime = new Date(issue.created_at).getTime();
|
||||
}
|
||||
// last activity time = the issue's own updated_at, which is
|
||||
// refreshed on comments, labels, etc. This avoids relying on
|
||||
// fetching comments and is accurate even when comments are edited.
|
||||
const lastActivityTime = new Date(issue.updated_at).getTime();
|
||||
|
||||
// check inactivity
|
||||
if (now - lastActivityTime > inactivePeriod) {
|
||||
console.log(`Closing inactive issue: #${issue.number} (No recent replies for 7 days)`);
|
||||
console.log(`Closing inactive issue: #${issue.number} (No activity for 7 days)`);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
@@ -76,5 +67,3 @@ jobs:
|
||||
console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
|
||||
}
|
||||
}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -20,21 +20,39 @@ jobs:
|
||||
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
TAG_NAME="${{ github.event.release.tag_name }}"
|
||||
VERSION_ONLY="${TAG_NAME#v}"
|
||||
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
|
||||
TAG_VERSION="${TAG_NAME#v}"
|
||||
VERSION_FULL="${TAG_VERSION}"
|
||||
VERSION_BASE="${TAG_VERSION}"
|
||||
PATCH_NUMBER=0
|
||||
|
||||
# Extract date from tag if available (format: v1.2.3-20251113-abc)
|
||||
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then
|
||||
DATE_STR="${BASH_REMATCH[1]}"
|
||||
# Extract date and patch from tags such as v1.2.3-1-20251113.
|
||||
if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
DATE_STR="${BASH_REMATCH[4]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
DATE_STR="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
DATE_STR="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
else
|
||||
# Use release published date
|
||||
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
|
||||
fi
|
||||
|
||||
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
@@ -122,8 +140,10 @@ jobs:
|
||||
# Generate version.json using cat and heredoc
|
||||
cat > version.json << EOF
|
||||
{
|
||||
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
|
||||
"latest_version": "${{ steps.version.outputs.VERSION_FULL }}",
|
||||
"version": "${{ steps.version.outputs.VERSION_FULL }}",
|
||||
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
|
||||
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
|
||||
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
|
||||
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
|
||||
"tagName": "${{ steps.version.outputs.TAG_NAME }}",
|
||||
|
||||
@@ -4,13 +4,31 @@ set -e
|
||||
PKG_NAME="crossdesk"
|
||||
APP_NAME="CrossDesk"
|
||||
|
||||
APP_VERSION="$1"
|
||||
ARCHITECTURE="amd64"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
|
||||
|
||||
@@ -4,13 +4,31 @@ set -e
|
||||
PKG_NAME="crossdesk"
|
||||
APP_NAME="CrossDesk"
|
||||
|
||||
APP_VERSION="$1"
|
||||
ARCHITECTURE="arm64"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
|
||||
|
||||
@@ -4,13 +4,31 @@ set -e
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
|
||||
APP_VERSION="$1"
|
||||
PLATFORM="macos"
|
||||
ARCH="arm64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="10.12"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
APP_BUNDLE="${APP_NAME_UPPER}.app"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
|
||||
@@ -4,13 +4,31 @@ set -e
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
|
||||
APP_VERSION="$1"
|
||||
PLATFORM="macos"
|
||||
ARCH="x64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="10.12"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
APP_BUNDLE="${APP_NAME_UPPER}.app"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<description>CrossDesk Portable Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -0,0 +1,8 @@
|
||||
// Portable build resource. The app itself runs as the current user; service
|
||||
// installation raises a separate UAC prompt only when the user chooses it.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||
@@ -0,0 +1,40 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Normalize-AppVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InputVersion
|
||||
)
|
||||
|
||||
$prefix = ""
|
||||
$body = $InputVersion
|
||||
|
||||
if ($body.StartsWith("v")) {
|
||||
$prefix = "v"
|
||||
$body = $body.Substring(1)
|
||||
}
|
||||
|
||||
if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||
return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])"
|
||||
}
|
||||
|
||||
return $InputVersion
|
||||
}
|
||||
|
||||
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
Push-Location $scriptDir
|
||||
try {
|
||||
& makensis "/DVERSION=$normalizedVersion" "nsis_script.nsi"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
+5
-1
@@ -128,7 +128,11 @@ bool Daemon::start(MainLoopFunc loop) {
|
||||
if (pid > 0) _exit(0);
|
||||
|
||||
umask(0);
|
||||
chdir("/");
|
||||
if (chdir("/") != 0) {
|
||||
std::cerr << "Failed to change daemon working directory to /: "
|
||||
<< std::strerror(errno) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// redirect file descriptors: keep stdout/stderr if from terminal, else
|
||||
// redirect to /dev/null
|
||||
|
||||
@@ -79,6 +79,9 @@ int ConfigCenter::Load() {
|
||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
enable_minimize_to_tray_ = ini_.GetBoolValue(
|
||||
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
|
||||
portable_service_prompt_suppressed_ =
|
||||
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
const char* file_transfer_save_path_value =
|
||||
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
|
||||
@@ -118,6 +121,8 @@ int ConfigCenter::Save() {
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
||||
enable_minimize_to_tray_);
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
@@ -325,6 +330,18 @@ int ConfigCenter::SetDaemon(bool enable_daemon) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
|
||||
portable_service_prompt_suppressed_ = suppressed;
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// getters
|
||||
|
||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
||||
@@ -377,6 +394,10 @@ bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||
|
||||
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
|
||||
return portable_service_prompt_suppressed_;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
|
||||
file_transfer_save_path_ = path;
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
|
||||
@@ -39,6 +39,7 @@ class ConfigCenter {
|
||||
int SetMinimizeToTray(bool enable_minimize_to_tray);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||
int SetFileTransferSavePath(const std::string& path);
|
||||
|
||||
// read config
|
||||
@@ -60,6 +61,7 @@ class ConfigCenter {
|
||||
bool IsMinimizeToTray() const;
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
bool IsPortableServicePromptSuppressed() const;
|
||||
std::string GetFileTransferSavePath() const;
|
||||
|
||||
int Load();
|
||||
@@ -87,6 +89,7 @@ class ConfigCenter {
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::string file_transfer_save_path_ = "";
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -21,12 +21,13 @@ namespace crossdesk {
|
||||
|
||||
typedef enum {
|
||||
mouse = 0,
|
||||
keyboard,
|
||||
audio_capture,
|
||||
host_infomation,
|
||||
display_id,
|
||||
service_status,
|
||||
service_command,
|
||||
keyboard = 1,
|
||||
audio_capture = 2,
|
||||
host_infomation = 3,
|
||||
display_id = 4,
|
||||
service_status = 5,
|
||||
service_command = 6,
|
||||
keyboard_state = 7,
|
||||
} ControlType;
|
||||
typedef enum {
|
||||
move = 0,
|
||||
@@ -55,6 +56,20 @@ typedef struct {
|
||||
KeyFlag flag;
|
||||
} Key;
|
||||
|
||||
inline constexpr size_t kMaxKeyboardStateKeys = 32;
|
||||
|
||||
typedef struct {
|
||||
size_t key_value;
|
||||
uint32_t scan_code;
|
||||
bool extended;
|
||||
} KeyboardStateKey;
|
||||
|
||||
typedef struct {
|
||||
uint32_t seq;
|
||||
size_t pressed_count;
|
||||
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
||||
} KeyboardState;
|
||||
|
||||
typedef struct {
|
||||
char host_name[64];
|
||||
size_t host_name_size;
|
||||
@@ -80,6 +95,7 @@ struct RemoteAction {
|
||||
union {
|
||||
Mouse m;
|
||||
Key k;
|
||||
KeyboardState ks;
|
||||
HostInfo i;
|
||||
bool a;
|
||||
int d;
|
||||
@@ -111,6 +127,20 @@ struct RemoteAction {
|
||||
{"extended", a.k.extended},
|
||||
{"flag", a.k.flag}};
|
||||
break;
|
||||
case ControlType::keyboard_state: {
|
||||
json keys = json::array();
|
||||
const size_t pressed_count =
|
||||
a.ks.pressed_count < kMaxKeyboardStateKeys
|
||||
? a.ks.pressed_count
|
||||
: kMaxKeyboardStateKeys;
|
||||
for (size_t idx = 0; idx < pressed_count; ++idx) {
|
||||
keys.push_back({{"key_value", a.ks.pressed_keys[idx].key_value},
|
||||
{"scan_code", a.ks.pressed_keys[idx].scan_code},
|
||||
{"extended", a.ks.pressed_keys[idx].extended}});
|
||||
}
|
||||
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
|
||||
break;
|
||||
}
|
||||
case ControlType::audio_capture:
|
||||
j["audio_capture"] = a.a;
|
||||
break;
|
||||
@@ -162,6 +192,33 @@ struct RemoteAction {
|
||||
out.k.extended = j.at("keyboard").value("extended", false);
|
||||
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
|
||||
break;
|
||||
case ControlType::keyboard_state: {
|
||||
const auto& keyboard_state_json = j.at("keyboard_state");
|
||||
out.ks.seq = keyboard_state_json.value("seq", 0u);
|
||||
out.ks.pressed_count = 0;
|
||||
|
||||
const auto keys_json =
|
||||
keyboard_state_json.value("pressed_keys", json::array());
|
||||
if (!keys_json.is_array()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t count =
|
||||
keys_json.size() < kMaxKeyboardStateKeys
|
||||
? keys_json.size()
|
||||
: kMaxKeyboardStateKeys;
|
||||
for (size_t idx = 0; idx < count; ++idx) {
|
||||
const auto& key_json = keys_json[idx];
|
||||
out.ks.pressed_keys[idx].key_value =
|
||||
key_json.at("key_value").get<size_t>();
|
||||
out.ks.pressed_keys[idx].scan_code =
|
||||
key_json.value("scan_code", static_cast<uint32_t>(0));
|
||||
out.ks.pressed_keys[idx].extended =
|
||||
key_json.value("extended", false);
|
||||
}
|
||||
out.ks.pressed_count = count;
|
||||
break;
|
||||
}
|
||||
case ControlType::audio_capture:
|
||||
out.a = j.at("audio_capture").get<bool>();
|
||||
break;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <dbus/dbus.h>
|
||||
#endif
|
||||
|
||||
#include "linux_evdev_keycode.h"
|
||||
#include "rd_log.h"
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
@@ -579,33 +580,46 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
if (!dbus_connection_ || wayland_session_handle_.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it == vkCodeToX11KeySym.end()) {
|
||||
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
||||
|
||||
const int evdev_keycode =
|
||||
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
|
||||
if (evdev_keycode >= 0 &&
|
||||
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
||||
const int keysym = key_it->second;
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it == vkCodeToX11KeySym.end()) {
|
||||
if (evdev_keycode >= 0) {
|
||||
LOG_ERROR(
|
||||
"Failed to send Wayland keyboard keycode event, vk_code={}, "
|
||||
"evdev_keycode={}, is_down={}",
|
||||
key_code, evdev_keycode, is_down);
|
||||
return -3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Prefer keycode injection to preserve physical-key semantics and avoid
|
||||
// implicit Shift interpretation for uppercase keysyms.
|
||||
if (display_) {
|
||||
const int keysym = key_it->second;
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
||||
if (x11_keycode > 8) {
|
||||
const int evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
||||
if (NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
||||
const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
||||
if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int keysym = key_it->second;
|
||||
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
|
||||
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
|
||||
return 0;
|
||||
|
||||
@@ -310,6 +310,10 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
if (IsFunctionKey(cg_key_code) && !is_down) {
|
||||
CGEventRef fn_release_event =
|
||||
CGEventCreateKeyboardEvent(NULL, fn_key_code_, false);
|
||||
if (!fn_release_event) {
|
||||
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
|
||||
return -1;
|
||||
}
|
||||
CGEventPost(kCGHIDEventTap, fn_release_event);
|
||||
CFRelease(fn_release_event);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
#ifndef _LINUX_EVDEV_KEYCODE_H_
|
||||
#define _LINUX_EVDEV_KEYCODE_H_
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline int LinuxEvdevKeycodeFromWindowsScanCode(uint32_t scan_code,
|
||||
bool extended) {
|
||||
const uint32_t base_scan_code = scan_code & 0xFFu;
|
||||
if (base_scan_code == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (extended) {
|
||||
switch (base_scan_code) {
|
||||
case 0x1C:
|
||||
return KEY_KPENTER;
|
||||
case 0x1D:
|
||||
return KEY_RIGHTCTRL;
|
||||
case 0x35:
|
||||
return KEY_KPSLASH;
|
||||
case 0x38:
|
||||
return KEY_RIGHTALT;
|
||||
case 0x47:
|
||||
return KEY_HOME;
|
||||
case 0x48:
|
||||
return KEY_UP;
|
||||
case 0x49:
|
||||
return KEY_PAGEUP;
|
||||
case 0x4B:
|
||||
return KEY_LEFT;
|
||||
case 0x4D:
|
||||
return KEY_RIGHT;
|
||||
case 0x4F:
|
||||
return KEY_END;
|
||||
case 0x50:
|
||||
return KEY_DOWN;
|
||||
case 0x51:
|
||||
return KEY_PAGEDOWN;
|
||||
case 0x52:
|
||||
return KEY_INSERT;
|
||||
case 0x53:
|
||||
return KEY_DELETE;
|
||||
case 0x5B:
|
||||
return KEY_LEFTMETA;
|
||||
case 0x5C:
|
||||
return KEY_RIGHTMETA;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// For the common PC set-1 keys, Linux evdev key codes intentionally line up
|
||||
// with the low byte of the Windows scan code.
|
||||
if ((base_scan_code >= 0x01 && base_scan_code <= 0x53) ||
|
||||
base_scan_code == 0x56 || base_scan_code == 0x57 ||
|
||||
base_scan_code == 0x58) {
|
||||
return static_cast<int>(base_scan_code);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline int LinuxEvdevKeycodeFromWindowsVk(int key_code) {
|
||||
switch (key_code) {
|
||||
case 0x08:
|
||||
return KEY_BACKSPACE;
|
||||
case 0x09:
|
||||
return KEY_TAB;
|
||||
case 0x0D:
|
||||
return KEY_ENTER;
|
||||
case 0x10:
|
||||
case 0xA0:
|
||||
return KEY_LEFTSHIFT;
|
||||
case 0x11:
|
||||
case 0xA2:
|
||||
return KEY_LEFTCTRL;
|
||||
case 0x12:
|
||||
case 0xA4:
|
||||
return KEY_LEFTALT;
|
||||
case 0x13:
|
||||
return KEY_PAUSE;
|
||||
case 0x14:
|
||||
return KEY_CAPSLOCK;
|
||||
case 0x1B:
|
||||
return KEY_ESC;
|
||||
case 0x20:
|
||||
return KEY_SPACE;
|
||||
case 0x21:
|
||||
return KEY_PAGEUP;
|
||||
case 0x22:
|
||||
return KEY_PAGEDOWN;
|
||||
case 0x23:
|
||||
return KEY_END;
|
||||
case 0x24:
|
||||
return KEY_HOME;
|
||||
case 0x25:
|
||||
return KEY_LEFT;
|
||||
case 0x26:
|
||||
return KEY_UP;
|
||||
case 0x27:
|
||||
return KEY_RIGHT;
|
||||
case 0x28:
|
||||
return KEY_DOWN;
|
||||
case 0x2C:
|
||||
return KEY_SYSRQ;
|
||||
case 0x2D:
|
||||
return KEY_INSERT;
|
||||
case 0x2E:
|
||||
return KEY_DELETE;
|
||||
case 0x30:
|
||||
return KEY_0;
|
||||
case 0x31:
|
||||
return KEY_1;
|
||||
case 0x32:
|
||||
return KEY_2;
|
||||
case 0x33:
|
||||
return KEY_3;
|
||||
case 0x34:
|
||||
return KEY_4;
|
||||
case 0x35:
|
||||
return KEY_5;
|
||||
case 0x36:
|
||||
return KEY_6;
|
||||
case 0x37:
|
||||
return KEY_7;
|
||||
case 0x38:
|
||||
return KEY_8;
|
||||
case 0x39:
|
||||
return KEY_9;
|
||||
case 0x41:
|
||||
return KEY_A;
|
||||
case 0x42:
|
||||
return KEY_B;
|
||||
case 0x43:
|
||||
return KEY_C;
|
||||
case 0x44:
|
||||
return KEY_D;
|
||||
case 0x45:
|
||||
return KEY_E;
|
||||
case 0x46:
|
||||
return KEY_F;
|
||||
case 0x47:
|
||||
return KEY_G;
|
||||
case 0x48:
|
||||
return KEY_H;
|
||||
case 0x49:
|
||||
return KEY_I;
|
||||
case 0x4A:
|
||||
return KEY_J;
|
||||
case 0x4B:
|
||||
return KEY_K;
|
||||
case 0x4C:
|
||||
return KEY_L;
|
||||
case 0x4D:
|
||||
return KEY_M;
|
||||
case 0x4E:
|
||||
return KEY_N;
|
||||
case 0x4F:
|
||||
return KEY_O;
|
||||
case 0x50:
|
||||
return KEY_P;
|
||||
case 0x51:
|
||||
return KEY_Q;
|
||||
case 0x52:
|
||||
return KEY_R;
|
||||
case 0x53:
|
||||
return KEY_S;
|
||||
case 0x54:
|
||||
return KEY_T;
|
||||
case 0x55:
|
||||
return KEY_U;
|
||||
case 0x56:
|
||||
return KEY_V;
|
||||
case 0x57:
|
||||
return KEY_W;
|
||||
case 0x58:
|
||||
return KEY_X;
|
||||
case 0x59:
|
||||
return KEY_Y;
|
||||
case 0x5A:
|
||||
return KEY_Z;
|
||||
case 0x5B:
|
||||
return KEY_LEFTMETA;
|
||||
case 0x5C:
|
||||
return KEY_RIGHTMETA;
|
||||
case 0x60:
|
||||
return KEY_KP0;
|
||||
case 0x61:
|
||||
return KEY_KP1;
|
||||
case 0x62:
|
||||
return KEY_KP2;
|
||||
case 0x63:
|
||||
return KEY_KP3;
|
||||
case 0x64:
|
||||
return KEY_KP4;
|
||||
case 0x65:
|
||||
return KEY_KP5;
|
||||
case 0x66:
|
||||
return KEY_KP6;
|
||||
case 0x67:
|
||||
return KEY_KP7;
|
||||
case 0x68:
|
||||
return KEY_KP8;
|
||||
case 0x69:
|
||||
return KEY_KP9;
|
||||
case 0x6A:
|
||||
return KEY_KPASTERISK;
|
||||
case 0x6B:
|
||||
return KEY_KPPLUS;
|
||||
case 0x6D:
|
||||
return KEY_KPMINUS;
|
||||
case 0x6E:
|
||||
return KEY_KPDOT;
|
||||
case 0x6F:
|
||||
return KEY_KPSLASH;
|
||||
case 0x70:
|
||||
return KEY_F1;
|
||||
case 0x71:
|
||||
return KEY_F2;
|
||||
case 0x72:
|
||||
return KEY_F3;
|
||||
case 0x73:
|
||||
return KEY_F4;
|
||||
case 0x74:
|
||||
return KEY_F5;
|
||||
case 0x75:
|
||||
return KEY_F6;
|
||||
case 0x76:
|
||||
return KEY_F7;
|
||||
case 0x77:
|
||||
return KEY_F8;
|
||||
case 0x78:
|
||||
return KEY_F9;
|
||||
case 0x79:
|
||||
return KEY_F10;
|
||||
case 0x7A:
|
||||
return KEY_F11;
|
||||
case 0x7B:
|
||||
return KEY_F12;
|
||||
case 0x90:
|
||||
return KEY_NUMLOCK;
|
||||
case 0x91:
|
||||
return KEY_SCROLLLOCK;
|
||||
case 0xA1:
|
||||
return KEY_RIGHTSHIFT;
|
||||
case 0xA3:
|
||||
return KEY_RIGHTCTRL;
|
||||
case 0xA5:
|
||||
return KEY_RIGHTALT;
|
||||
case 0xBA:
|
||||
return KEY_SEMICOLON;
|
||||
case 0xBB:
|
||||
return KEY_EQUAL;
|
||||
case 0xBC:
|
||||
return KEY_COMMA;
|
||||
case 0xBD:
|
||||
return KEY_MINUS;
|
||||
case 0xBE:
|
||||
return KEY_DOT;
|
||||
case 0xBF:
|
||||
return KEY_SLASH;
|
||||
case 0xC0:
|
||||
return KEY_GRAVE;
|
||||
case 0xDB:
|
||||
return KEY_LEFTBRACE;
|
||||
case 0xDC:
|
||||
return KEY_BACKSLASH;
|
||||
case 0xDD:
|
||||
return KEY_RIGHTBRACE;
|
||||
case 0xDE:
|
||||
return KEY_APOSTROPHE;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
inline int ResolveLinuxEvdevKeycodeFromWindowsKey(int key_code,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
const int scan_keycode =
|
||||
LinuxEvdevKeycodeFromWindowsScanCode(scan_code, extended);
|
||||
if (scan_keycode >= 0) {
|
||||
return scan_keycode;
|
||||
}
|
||||
|
||||
return LinuxEvdevKeycodeFromWindowsVk(key_code);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -2,9 +2,35 @@
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDoubleClickInterval = std::chrono::milliseconds(500);
|
||||
constexpr int kDoubleClickMaxDistance = 8;
|
||||
constexpr int kMaxClickState = 3;
|
||||
|
||||
bool IsWithinClickDistance(int x1, int y1, int x2, int y2) {
|
||||
return std::abs(x1 - x2) <= kDoubleClickMaxDistance &&
|
||||
std::abs(y1 - y2) <= kDoubleClickMaxDistance;
|
||||
}
|
||||
|
||||
void SetClickState(CGEventRef event, int click_state) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGEventSetIntegerValueField(
|
||||
event, kCGMouseEventClickState,
|
||||
std::max(1, std::min(click_state, kMaxClickState)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MouseController::MouseController() {}
|
||||
|
||||
@@ -18,87 +44,144 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
|
||||
|
||||
int MouseController::Destroy() { return 0; }
|
||||
|
||||
int MouseController::BeginClick(ClickTracker& tracker, int x, int y) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const bool continues_previous_click =
|
||||
tracker.has_last_down &&
|
||||
now - tracker.last_down_time <= kDoubleClickInterval &&
|
||||
IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y);
|
||||
|
||||
tracker.click_state = continues_previous_click
|
||||
? std::min(tracker.click_state + 1, kMaxClickState)
|
||||
: 1;
|
||||
tracker.active_click_state = tracker.click_state;
|
||||
tracker.has_last_down = true;
|
||||
tracker.last_down_time = now;
|
||||
tracker.last_down_x = x;
|
||||
tracker.last_down_y = y;
|
||||
|
||||
return tracker.active_click_state;
|
||||
}
|
||||
|
||||
int MouseController::EndClick(ClickTracker& tracker, int x, int y) {
|
||||
const int click_state = tracker.active_click_state;
|
||||
if (!IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y)) {
|
||||
tracker.has_last_down = false;
|
||||
tracker.click_state = 0;
|
||||
tracker.active_click_state = 1;
|
||||
}
|
||||
|
||||
return click_state;
|
||||
}
|
||||
|
||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
int mouse_pos_x =
|
||||
remote_action.m.x * display_info_list_[display_index].width +
|
||||
display_info_list_[display_index].left;
|
||||
int mouse_pos_y =
|
||||
remote_action.m.y * display_info_list_[display_index].height +
|
||||
display_info_list_[display_index].top;
|
||||
if (remote_action.type != ControlType::mouse) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
CGEventRef mouse_event = nullptr;
|
||||
CGEventType mouse_type;
|
||||
CGMouseButton mouse_button;
|
||||
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
|
||||
display_index, display_info_list_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
mouse_type = kCGEventLeftMouseDown;
|
||||
left_dragging_ = true;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
mouse_type = kCGEventLeftMouseUp;
|
||||
left_dragging_ = false;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
mouse_type = kCGEventRightMouseDown;
|
||||
right_dragging_ = true;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
mouse_type = kCGEventRightMouseUp;
|
||||
right_dragging_ = false;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
mouse_type = kCGEventOtherMouseDown;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
mouse_type = kCGEventOtherMouseUp;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(
|
||||
NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0);
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(
|
||||
NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s);
|
||||
break;
|
||||
default:
|
||||
if (left_dragging_) {
|
||||
mouse_type = kCGEventLeftMouseDragged;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
} else if (right_dragging_) {
|
||||
mouse_type = kCGEventRightMouseDragged;
|
||||
mouse_button = kCGMouseButtonRight;
|
||||
} else {
|
||||
mouse_type = kCGEventMouseMoved;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
}
|
||||
const DisplayInfo& display_info = display_info_list_[display_index];
|
||||
if (display_info.width <= 0 || display_info.height <= 0) {
|
||||
LOG_WARN("Mouse command skipped, invalid display geometry: {}x{}",
|
||||
display_info.width, display_info.height);
|
||||
return -1;
|
||||
}
|
||||
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
mouse_button);
|
||||
break;
|
||||
}
|
||||
const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
int mouse_pos_x = normalized_x * display_info.width + display_info.left;
|
||||
int mouse_pos_y = normalized_y * display_info.height + display_info.top;
|
||||
|
||||
if (mouse_event) {
|
||||
CGEventPost(kCGHIDEventTap, mouse_event);
|
||||
CFRelease(mouse_event);
|
||||
}
|
||||
CGEventRef mouse_event = nullptr;
|
||||
CGEventType mouse_type;
|
||||
CGMouseButton mouse_button;
|
||||
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
|
||||
int click_state = 1;
|
||||
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
mouse_type = kCGEventLeftMouseDown;
|
||||
left_dragging_ = true;
|
||||
click_state = BeginClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
mouse_type = kCGEventLeftMouseUp;
|
||||
left_dragging_ = false;
|
||||
click_state = EndClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
mouse_type = kCGEventRightMouseDown;
|
||||
right_dragging_ = true;
|
||||
click_state = BeginClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
mouse_type = kCGEventRightMouseUp;
|
||||
right_dragging_ = false;
|
||||
click_state = EndClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
mouse_type = kCGEventOtherMouseDown;
|
||||
click_state = BeginClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
mouse_type = kCGEventOtherMouseUp;
|
||||
click_state = EndClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
|
||||
2, remote_action.m.s, 0);
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
|
||||
2, 0, remote_action.m.s);
|
||||
break;
|
||||
default:
|
||||
if (left_dragging_) {
|
||||
mouse_type = kCGEventLeftMouseDragged;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
} else if (right_dragging_) {
|
||||
mouse_type = kCGEventRightMouseDragged;
|
||||
mouse_button = kCGMouseButtonRight;
|
||||
} else {
|
||||
mouse_type = kCGEventMouseMoved;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
}
|
||||
|
||||
mouse_event =
|
||||
CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, mouse_button);
|
||||
break;
|
||||
}
|
||||
|
||||
if (mouse_event) {
|
||||
CGEventPost(kCGHIDEventTap, mouse_event);
|
||||
CFRelease(mouse_event);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#ifndef _MOUSE_CONTROLLER_H_
|
||||
#define _MOUSE_CONTROLLER_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
@@ -24,9 +25,24 @@ class MouseController : public DeviceController {
|
||||
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
|
||||
|
||||
private:
|
||||
struct ClickTracker {
|
||||
bool has_last_down = false;
|
||||
std::chrono::steady_clock::time_point last_down_time{};
|
||||
int last_down_x = 0;
|
||||
int last_down_y = 0;
|
||||
int click_state = 0;
|
||||
int active_click_state = 1;
|
||||
};
|
||||
|
||||
int BeginClick(ClickTracker& tracker, int x, int y);
|
||||
int EndClick(ClickTracker& tracker, int x, int y);
|
||||
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
bool left_dragging_ = false;
|
||||
bool right_dragging_ = false;
|
||||
ClickTracker left_click_tracker_;
|
||||
ClickTracker right_click_tracker_;
|
||||
ClickTracker middle_click_tracker_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -19,176 +19,217 @@ struct TranslationRow {
|
||||
};
|
||||
|
||||
// Single source of truth for all UI strings.
|
||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||
u8"Скопировано в буфер обмена") \
|
||||
X(password, u8"密码", "Password", u8"Пароль") \
|
||||
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
|
||||
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
||||
u8"Удаленный рабочий стол") \
|
||||
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
|
||||
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
||||
X(recent_connections, u8"近期连接", "Recent Connections", \
|
||||
u8"Недавние подключения") \
|
||||
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
||||
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
||||
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
||||
u8"Развернуть панель управления") \
|
||||
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
||||
u8"Свернуть панель управления") \
|
||||
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
|
||||
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
|
||||
u8"Показать статистику трафика") \
|
||||
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
|
||||
u8"Скрыть статистику трафика") \
|
||||
X(video, u8"视频", "Video", u8"Видео") \
|
||||
X(audio, u8"音频", "Audio", u8"Аудио") \
|
||||
X(data, u8"数据", "Data", u8"Данные") \
|
||||
X(total, u8"总计", "Total", u8"Итого") \
|
||||
X(in, u8"输入", "In", u8"Вход") \
|
||||
X(out, u8"输出", "Out", u8"Выход") \
|
||||
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
|
||||
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
|
||||
u8"Выйти из полноэкранного режима") \
|
||||
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
|
||||
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
|
||||
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
|
||||
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
||||
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
||||
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
||||
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
||||
X(remote_password_box_visible, u8"远端密码框已出现", \
|
||||
"Remote password box visible", u8"Окно ввода пароля видно") \
|
||||
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
||||
"Remote lock screen visible, send SAS", \
|
||||
u8"Видна блокировка, отправьте SAS") \
|
||||
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
||||
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
|
||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
||||
"Remote Windows service unavailable", \
|
||||
u8"Служба Windows на удаленной стороне недоступна") \
|
||||
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
||||
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
|
||||
X(windows_service_setup_message, \
|
||||
u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \
|
||||
"The portable version needs the local Windows service for full control on the lock screen, sign-in UI, and secure desktop. The service is not installed. Click Install and approve the system prompt.", \
|
||||
u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \
|
||||
X(install_windows_service, u8"安装", "Install", \
|
||||
u8"Установить службу") \
|
||||
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
|
||||
u8"Установка службы...") \
|
||||
X(windows_service_install_success, u8"服务已安装并启动", \
|
||||
"Service installed and started", u8"Служба установлена и запущена") \
|
||||
X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \
|
||||
"Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \
|
||||
u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \
|
||||
X(remote_unlock_requires_secure_desktop, \
|
||||
u8"当前仍需要安全桌面专用采集/输入", \
|
||||
"Secure desktop capture/input is still required", \
|
||||
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
|
||||
u8"стола") \
|
||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_frame_rate, u8"画面采集帧率:", \
|
||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||
X(video_quality_high, u8"高", "High", u8"Высокое") \
|
||||
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
|
||||
X(video_quality_low, u8"低", "Low", u8"Низкое") \
|
||||
X(video_encode_format, u8"视频编码格式:", \
|
||||
"Video Encode Format:", u8"Формат кодека видео:") \
|
||||
X(av1, u8"AV1", "AV1", "AV1") \
|
||||
X(h264, u8"H.264", "H.264", "H.264") \
|
||||
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
|
||||
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
|
||||
X(enable_turn, u8"启用中继服务:", \
|
||||
"Enable TURN Service:", u8"Включить TURN-сервис:") \
|
||||
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
|
||||
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
|
||||
u8"Конфигурация self-hosted") \
|
||||
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
|
||||
u8"Настройки self-hosted") \
|
||||
X(self_hosted_server_address, u8"服务器地址:", \
|
||||
"Server Address:", u8"Адрес сервера:") \
|
||||
X(self_hosted_server_port, u8"信令服务端口:", \
|
||||
"Signal Service Port:", u8"Порт сигнального сервиса:") \
|
||||
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
|
||||
"Relay Service Port:", u8"Порт реле-сервиса:") \
|
||||
X(ok, u8"确认", "OK", u8"ОК") \
|
||||
X(cancel, u8"取消", "Cancel", u8"Отмена") \
|
||||
X(new_password, u8"请输入六位密码:", \
|
||||
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
|
||||
X(input_password, u8"请输入密码:", \
|
||||
"Please input password:", u8"Введите пароль:") \
|
||||
X(validate_password, u8"验证密码中...", "Validate password ...", \
|
||||
u8"Проверка пароля...") \
|
||||
X(reinput_password, u8"请重新输入密码", "Please input password again", \
|
||||
u8"Повторно введите пароль") \
|
||||
X(remember_password, u8"记住密码", "Remember password", \
|
||||
u8"Запомнить пароль") \
|
||||
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
||||
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
||||
u8"Нет подключения к серверу") \
|
||||
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
||||
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
||||
u8"P2P отключено") \
|
||||
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
||||
u8"Подключение P2P...") \
|
||||
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
||||
u8"Получение изображения...") \
|
||||
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
||||
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
||||
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
||||
X(about, u8"关于", "About", u8"О программе") \
|
||||
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
||||
X(new_version_available, u8"新版本可用", "New Version Available", \
|
||||
u8"Доступна новая версия") \
|
||||
X(version, u8"版本", "Version", u8"Версия") \
|
||||
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
||||
X(access_website, u8"访问官网: ", \
|
||||
"Access Website: ", u8"Официальный сайт: ") \
|
||||
X(update, u8"更新", "Update", u8"Обновить") \
|
||||
X(confirm_delete_connection, u8"确认删除此连接", \
|
||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
|
||||
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
|
||||
u8"Вступит в силу после перезапуска") \
|
||||
X(select_file, u8"选择文件发送", "Select File to Send", \
|
||||
u8"Выбрать файл для отправки") \
|
||||
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
|
||||
u8"Прогресс передачи файлов") \
|
||||
X(queued, u8"队列中", "Queued", u8"В очереди") \
|
||||
X(sending, u8"正在传输", "Sending", u8"Передача") \
|
||||
X(completed, u8"已完成", "Completed", u8"Завершено") \
|
||||
X(failed, u8"失败", "Failed", u8"Ошибка") \
|
||||
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
|
||||
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
|
||||
X(connection_status, u8"连接状态:", \
|
||||
"Connection Status:", u8"Состояние соединения:") \
|
||||
X(file_transfer_save_path, u8"文件接收保存路径:", \
|
||||
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
|
||||
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
||||
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
|
||||
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
|
||||
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
||||
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
||||
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
||||
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
|
||||
X(online, u8"在线", "Online", u8"Онлайн") \
|
||||
X(offline, u8"离线", "Offline", u8"Офлайн") \
|
||||
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
|
||||
X(request_permissions, u8"权限请求", "Request Permissions", \
|
||||
u8"Запрос разрешений") \
|
||||
X(screen_recording_permission, u8"屏幕录制权限", \
|
||||
"Screen Recording Permission", u8"Разрешение на запись экрана") \
|
||||
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
|
||||
u8"Разрешение специальных возможностей") \
|
||||
X(permission_required_message, u8"该应用需要授权以下权限:", \
|
||||
"The application requires the following permissions:", \
|
||||
u8"Для работы приложения требуются следующие разрешения:") \
|
||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||
u8"Скопировано в буфер обмена") \
|
||||
X(password, u8"密码", "Password", u8"Пароль") \
|
||||
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
|
||||
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
||||
u8"Удаленный рабочий стол") \
|
||||
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
|
||||
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
||||
X(recent_connections, u8"近期连接", "Recent Connections", \
|
||||
u8"Недавние подключения") \
|
||||
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
||||
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
||||
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
||||
u8"Развернуть панель управления") \
|
||||
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
||||
u8"Свернуть панель управления") \
|
||||
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
|
||||
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
|
||||
u8"Показать статистику трафика") \
|
||||
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
|
||||
u8"Скрыть статистику трафика") \
|
||||
X(video, u8"视频", "Video", u8"Видео") \
|
||||
X(audio, u8"音频", "Audio", u8"Аудио") \
|
||||
X(data, u8"数据", "Data", u8"Данные") \
|
||||
X(total, u8"总计", "Total", u8"Итого") \
|
||||
X(in, u8"输入", "In", u8"Вход") \
|
||||
X(out, u8"输出", "Out", u8"Выход") \
|
||||
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
|
||||
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
|
||||
u8"Выйти из полноэкранного режима") \
|
||||
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
|
||||
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
|
||||
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
|
||||
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
||||
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
||||
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
||||
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
||||
X(remote_password_box_visible, u8"远端密码框已出现", \
|
||||
"Remote password box visible", u8"Окно ввода пароля видно") \
|
||||
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
||||
"Remote lock screen visible, send SAS", \
|
||||
u8"Видна блокировка, отправьте SAS") \
|
||||
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
||||
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
|
||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
||||
"Remote Windows service unavailable", \
|
||||
u8"Служба Windows на удаленной стороне недоступна") \
|
||||
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
||||
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
|
||||
X(windows_service_setup_message, \
|
||||
u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
|
||||
u8"Service。\n未安装该服务不影响 CrossDesk " \
|
||||
u8"正常使用,仅无法在锁屏状态下控制本机。", \
|
||||
"To support remote control of this device while it is locked, CrossDesk " \
|
||||
"Service must be installed with administrator permission.\nWithout this " \
|
||||
"service, CrossDesk still works normally; only lock-screen control of " \
|
||||
"this computer is unavailable.", \
|
||||
u8"Чтобы поддерживать удаленное управление этим устройством на экране " \
|
||||
u8"блокировки, необходимо установить CrossDesk Service с правами " \
|
||||
u8"администратора.\nБез этой службы CrossDesk продолжит работать " \
|
||||
u8"нормально; будет недоступно только управление этим компьютером на " \
|
||||
u8"экране блокировки.") \
|
||||
X(install_windows_service, u8"安装", "Install", u8"Установить") \
|
||||
X(windows_service_settings_label, u8"锁屏控制服务:", \
|
||||
"Lock Screen Service:", u8"Служба блокировки экрана:") \
|
||||
X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
|
||||
X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
|
||||
u8"Больше не напоминать") \
|
||||
X(windows_service_prompt_suppressed_message, \
|
||||
u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \
|
||||
"You will not be reminded again. To enable remote control while locked " \
|
||||
"later, click Install in Settings.", \
|
||||
u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \
|
||||
u8"экране блокировки, нажмите «Установить» в настройках.") \
|
||||
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
|
||||
u8"Установка службы...") \
|
||||
X(windows_service_install_success, u8"服务已安装并启动", \
|
||||
"Service installed and started", u8"Служба установлена и запущена") \
|
||||
X(windows_service_install_failed, \
|
||||
u8"服务安装失败。请确认 " \
|
||||
u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \
|
||||
u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \
|
||||
"Service installation failed. Make sure CrossDesk.exe, " \
|
||||
"crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \
|
||||
"portable folder, then approve the administrator prompt.", \
|
||||
u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \
|
||||
u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \
|
||||
u8"одной папке портативной версии, затем подтвердите запрос прав " \
|
||||
u8"администратора.") \
|
||||
X(remote_unlock_requires_secure_desktop, \
|
||||
u8"当前仍需要安全桌面专用采集/输入", \
|
||||
"Secure desktop capture/input is still required", \
|
||||
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
|
||||
u8"стола") \
|
||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_frame_rate, u8"画面采集帧率:", \
|
||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||
X(video_quality_high, u8"高", "High", u8"Высокое") \
|
||||
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
|
||||
X(video_quality_low, u8"低", "Low", u8"Низкое") \
|
||||
X(video_encode_format, u8"视频编码格式:", \
|
||||
"Video Encode Format:", u8"Формат кодека видео:") \
|
||||
X(av1, u8"AV1", "AV1", "AV1") \
|
||||
X(h264, u8"H.264", "H.264", "H.264") \
|
||||
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
|
||||
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
|
||||
X(enable_turn, u8"启用中继服务:", \
|
||||
"Enable TURN Service:", u8"Включить TURN-сервис:") \
|
||||
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
|
||||
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
|
||||
u8"Конфигурация self-hosted") \
|
||||
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
|
||||
u8"Настройки self-hosted") \
|
||||
X(self_hosted_server_address, u8"服务器地址:", \
|
||||
"Server Address:", u8"Адрес сервера:") \
|
||||
X(self_hosted_server_port, u8"信令服务端口:", \
|
||||
"Signal Service Port:", u8"Порт сигнального сервиса:") \
|
||||
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
|
||||
"Relay Service Port:", u8"Порт реле-сервиса:") \
|
||||
X(ok, u8"确认", "OK", u8"ОК") \
|
||||
X(cancel, u8"取消", "Cancel", u8"Отмена") \
|
||||
X(new_password, u8"请输入六位密码:", \
|
||||
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
|
||||
X(input_password, u8"请输入密码:", \
|
||||
"Please input password:", u8"Введите пароль:") \
|
||||
X(validate_password, u8"验证密码中...", "Validate password ...", \
|
||||
u8"Проверка пароля...") \
|
||||
X(reinput_password, u8"请重新输入密码", "Please input password again", \
|
||||
u8"Повторно введите пароль") \
|
||||
X(remember_password, u8"记住密码", "Remember password", \
|
||||
u8"Запомнить пароль") \
|
||||
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
||||
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
||||
u8"Нет подключения к серверу") \
|
||||
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
|
||||
"Certificate verification failed. Reinstall the self-hosted root " \
|
||||
"certificate.", \
|
||||
u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
|
||||
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
||||
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
||||
u8"P2P отключено") \
|
||||
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
||||
u8"Подключение P2P...") \
|
||||
X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
|
||||
u8"Сбор кандидатов...") \
|
||||
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
||||
u8"Получение изображения...") \
|
||||
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
||||
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
||||
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
||||
X(about, u8"关于", "About", u8"О программе") \
|
||||
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
||||
X(new_version_available, u8"新版本可用", "New Version Available", \
|
||||
u8"Доступна новая версия") \
|
||||
X(version, u8"版本", "Version", u8"Версия") \
|
||||
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
||||
X(access_website, u8"访问官网: ", \
|
||||
"Access Website: ", u8"Официальный сайт: ") \
|
||||
X(update, u8"更新", "Update", u8"Обновить") \
|
||||
X(connection_alias, u8"修改名称", "Edit Alias", \
|
||||
u8"Изменить имя подключения") \
|
||||
X(delete_connection, u8"删除连接", "Delete Connection", \
|
||||
u8"Удалить подключение") \
|
||||
X(connect_to_this_connection, u8"发起连接", "Connect to this connection", \
|
||||
u8"Подключиться") \
|
||||
X(input_connection_alias, u8"请输入连接名称:", \
|
||||
"Please input connection name:", u8"Введите имя подключения:") \
|
||||
X(confirm_delete_connection, u8"确认删除此连接", \
|
||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
|
||||
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
|
||||
u8"Вступит в силу после перезапуска") \
|
||||
X(select_file, u8"选择文件发送", "Select File to Send", \
|
||||
u8"Выбрать файл для отправки") \
|
||||
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
|
||||
u8"Прогресс передачи файлов") \
|
||||
X(queued, u8"队列中", "Queued", u8"В очереди") \
|
||||
X(sending, u8"正在传输", "Sending", u8"Передача") \
|
||||
X(completed, u8"已完成", "Completed", u8"Завершено") \
|
||||
X(failed, u8"失败", "Failed", u8"Ошибка") \
|
||||
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
|
||||
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
|
||||
X(connection_status, u8"连接状态:", \
|
||||
"Connection Status:", u8"Состояние соединения:") \
|
||||
X(file_transfer_save_path, u8"文件接收保存路径:", \
|
||||
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
|
||||
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
||||
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
|
||||
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
|
||||
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
||||
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
||||
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
||||
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
|
||||
X(online, u8"在线", "Online", u8"Онлайн") \
|
||||
X(offline, u8"离线", "Offline", u8"Офлайн") \
|
||||
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
|
||||
X(request_permissions, u8"权限请求", "Request Permissions", \
|
||||
u8"Запрос разрешений") \
|
||||
X(screen_recording_permission, u8"屏幕录制权限", \
|
||||
"Screen Recording Permission", u8"Разрешение на запись экрана") \
|
||||
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
|
||||
u8"Разрешение специальных возможностей") \
|
||||
X(permission_required_message, u8"该应用需要授权以下权限:", \
|
||||
"The application requires the following permissions:", \
|
||||
u8"Для работы приложения требуются следующие разрешения:") \
|
||||
X(exit_program, u8"退出", "Exit", u8"Выход")
|
||||
|
||||
inline constexpr TranslationRow kTranslationRows[] = {
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
std::string TrimConnectionAlias(const char* value) {
|
||||
std::string alias = value ? value : "";
|
||||
|
||||
auto not_space = [](unsigned char ch) { return !std::isspace(ch); };
|
||||
alias.erase(alias.begin(),
|
||||
std::find_if(alias.begin(), alias.end(), not_space));
|
||||
alias.erase(std::find_if(alias.rbegin(), alias.rend(), not_space).base(),
|
||||
alias.end());
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
void SetDarkTextTooltip(const char* text) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.05f, 0.05f, 0.05f, 1.0f));
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::Text("%s", text);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int Render::RecentConnectionsWindow() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
@@ -51,8 +79,9 @@ int Render::ShowRecentConnections() {
|
||||
float recent_connection_button_width = recent_connection_image_width * 0.15f;
|
||||
float recent_connection_button_height =
|
||||
recent_connection_image_height * 0.25f;
|
||||
float recent_connection_dummy_button_width =
|
||||
recent_connection_image_width - 2 * recent_connection_button_width;
|
||||
float recent_connection_footer_height =
|
||||
recent_connection_button_height * 1.18f;
|
||||
float recent_connection_name_width = recent_connection_image_width;
|
||||
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
|
||||
@@ -61,14 +90,16 @@ int Render::ShowRecentConnections() {
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg,
|
||||
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
|
||||
const ImGuiWindowFlags container_flags =
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoScrollWithMouse |
|
||||
(recent_connections_.empty()
|
||||
? ImGuiWindowFlags_None
|
||||
: ImGuiWindowFlags_AlwaysHorizontalScrollbar);
|
||||
ImGui::BeginChild(
|
||||
"RecentConnectionsContainer",
|
||||
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGuiChildFlags_Borders, container_flags);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
size_t recent_connections_count = recent_connections_.size();
|
||||
@@ -90,149 +121,311 @@ int Render::ShowRecentConnections() {
|
||||
// password length is 6
|
||||
// connection_info -> remote_id + 'Y' + host_name + '@' + password
|
||||
// -> remote_id + 'N' + host_name
|
||||
if ('Y' == connection_info[9] && connection_info.size() >= 16) {
|
||||
bool invalid_connection_info = false;
|
||||
if (connection_info.size() > 9 && 'Y' == connection_info[9] &&
|
||||
connection_info.size() >= 16) {
|
||||
size_t pos_y = connection_info.find('Y');
|
||||
size_t pos_at = connection_info.find('@');
|
||||
|
||||
if (pos_y == std::string::npos || pos_at == std::string::npos ||
|
||||
pos_y >= pos_at) {
|
||||
LOG_ERROR("Invalid filename");
|
||||
continue;
|
||||
invalid_connection_info = true;
|
||||
} else {
|
||||
it.second.remote_id = connection_info.substr(0, pos_y);
|
||||
it.second.remote_host_name =
|
||||
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
|
||||
it.second.password = connection_info.substr(pos_at + 1);
|
||||
it.second.remember_password = true;
|
||||
}
|
||||
|
||||
it.second.remote_id = connection_info.substr(0, pos_y);
|
||||
it.second.remote_host_name =
|
||||
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
|
||||
it.second.password = connection_info.substr(pos_at + 1);
|
||||
it.second.remember_password = true;
|
||||
} else if ('N' == connection_info[9] && connection_info.size() >= 10) {
|
||||
} else if (connection_info.size() > 9 && 'N' == connection_info[9] &&
|
||||
connection_info.size() >= 10) {
|
||||
size_t pos_n = connection_info.find('N');
|
||||
size_t pos_at = connection_info.find('@');
|
||||
|
||||
if (pos_n == std::string::npos) {
|
||||
LOG_ERROR("Invalid filename");
|
||||
continue;
|
||||
invalid_connection_info = true;
|
||||
} else {
|
||||
it.second.remote_id = connection_info.substr(0, pos_n);
|
||||
it.second.remote_host_name = connection_info.substr(pos_n + 1);
|
||||
it.second.password = "";
|
||||
it.second.remember_password = false;
|
||||
}
|
||||
|
||||
it.second.remote_id = connection_info.substr(0, pos_n);
|
||||
it.second.remote_host_name = connection_info.substr(pos_n + 1);
|
||||
it.second.password = "";
|
||||
it.second.remember_password = false;
|
||||
} else {
|
||||
it.second.remote_host_name = "unknown";
|
||||
invalid_connection_info = true;
|
||||
}
|
||||
|
||||
if (invalid_connection_info) {
|
||||
it.second.remote_id = connection_info.substr(
|
||||
0, std::min<size_t>(connection_info.size(), 9));
|
||||
it.second.remote_host_name = "unknown";
|
||||
it.second.password = "";
|
||||
it.second.remember_password = false;
|
||||
}
|
||||
|
||||
std::string display_name = GetRecentConnectionDisplayName(it.second);
|
||||
bool online = device_presence_.IsOnline(it.second.remote_id);
|
||||
|
||||
ImVec2 image_screen_pos = ImVec2(
|
||||
ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f,
|
||||
ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f);
|
||||
ImVec2 image_pos =
|
||||
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
|
||||
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
|
||||
|
||||
ImGui::SetCursorPos(image_pos);
|
||||
ImVec2 image_screen_pos = ImGui::GetCursorScreenPos();
|
||||
ImGui::Image(
|
||||
(ImTextureID)(intptr_t)it.second.texture,
|
||||
ImVec2(recent_connection_image_width, recent_connection_image_height));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
|
||||
const bool image_item_hovered = ImGui::IsItemHovered();
|
||||
|
||||
ImVec2 card_screen_min = image_screen_pos;
|
||||
ImVec2 card_screen_max =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width,
|
||||
image_screen_pos.y + recent_connection_image_height +
|
||||
recent_connection_footer_height);
|
||||
|
||||
const bool card_hovered =
|
||||
ImGui::IsMouseHoveringRect(card_screen_min, card_screen_max, true);
|
||||
|
||||
const float recent_connection_toolbar_width =
|
||||
3.0f * recent_connection_button_width;
|
||||
|
||||
const float recent_connection_toolbar_padding =
|
||||
recent_connection_image_width * 0.025f;
|
||||
|
||||
const ImVec2 toolbar_pos = ImVec2(
|
||||
image_pos.x + recent_connection_image_width -
|
||||
recent_connection_toolbar_width - recent_connection_toolbar_padding,
|
||||
image_pos.y + recent_connection_toolbar_padding);
|
||||
|
||||
const ImVec2 toolbar_screen_pos = ImVec2(
|
||||
image_screen_pos.x + recent_connection_image_width -
|
||||
recent_connection_toolbar_width - recent_connection_toolbar_padding,
|
||||
image_screen_pos.y + recent_connection_toolbar_padding);
|
||||
|
||||
const ImVec2 toolbar_screen_end =
|
||||
ImVec2(toolbar_screen_pos.x + recent_connection_toolbar_width,
|
||||
toolbar_screen_pos.y + recent_connection_button_height);
|
||||
|
||||
const bool toolbar_hovered =
|
||||
card_hovered && ImGui::IsMouseHoveringRect(toolbar_screen_pos,
|
||||
toolbar_screen_end, true);
|
||||
|
||||
const bool show_image_tooltip = image_item_hovered && !toolbar_hovered;
|
||||
|
||||
if (show_image_tooltip) {
|
||||
ImGui::BeginTooltip();
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
std::string display_host_name_with_presence =
|
||||
it.second.remote_host_name + " " +
|
||||
(online ? localization::online[localization_language_index_]
|
||||
: localization::offline[localization_language_index_]);
|
||||
ImGui::Text("%s", display_host_name_with_presence.c_str());
|
||||
|
||||
ImGui::Text("%s", display_name.c_str());
|
||||
|
||||
if (!it.second.remote_host_name.empty() &&
|
||||
it.second.remote_host_name != display_name) {
|
||||
ImGui::Text("%s", it.second.remote_host_name.c_str());
|
||||
}
|
||||
|
||||
ImGui::Text("%s: %s",
|
||||
localization::remote_id[localization_language_index_].c_str(),
|
||||
it.second.remote_id.c_str());
|
||||
|
||||
ImGui::Text("%s",
|
||||
(online ? localization::online[localization_language_index_]
|
||||
: localization::offline[localization_language_index_])
|
||||
.c_str());
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
ImVec2 circle_pos =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 0.07f,
|
||||
image_screen_pos.y + recent_connection_image_height * 0.12f);
|
||||
ImU32 fill_color =
|
||||
online ? IM_COL32(0, 255, 0, 255) : IM_COL32(140, 140, 140, 255);
|
||||
ImU32 border_color = IM_COL32(255, 255, 255, 255);
|
||||
float dot_radius = recent_connection_image_height * 0.06f;
|
||||
draw_list->AddCircleFilled(circle_pos, dot_radius * 1.25f, border_color,
|
||||
100);
|
||||
draw_list->AddCircleFilled(circle_pos, dot_radius, fill_color, 100);
|
||||
|
||||
// remote id display button
|
||||
// connection name footer
|
||||
{
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.2f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0.2f));
|
||||
|
||||
ImVec2 dummy_button_pos =
|
||||
ImVec2 footer_pos =
|
||||
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height);
|
||||
std::string dummy_button_name = "##DummyButton" + it.second.remote_id;
|
||||
ImGui::SetCursorPos(dummy_button_pos);
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
ImGui::Button(dummy_button_name.c_str(),
|
||||
ImVec2(recent_connection_dummy_button_width,
|
||||
recent_connection_button_height));
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetCursorPos(ImVec2(
|
||||
dummy_button_pos.x + recent_connection_dummy_button_width * 0.05f,
|
||||
dummy_button_pos.y + recent_connection_button_height * 0.05f));
|
||||
ImGui::SetWindowFontScale(0.65f);
|
||||
ImGui::Text("%s", it.second.remote_id.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImVec4(0.1f, 0.4f, 0.8f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 0.7f));
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
// trash button
|
||||
{
|
||||
ImVec2 trash_can_button_pos =
|
||||
ImVec2(image_pos.x + recent_connection_image_width -
|
||||
2 * recent_connection_button_width,
|
||||
image_pos.y + recent_connection_image_height);
|
||||
ImGui::SetCursorPos(trash_can_button_pos);
|
||||
std::string trash_can = ICON_FA_TRASH_CAN;
|
||||
std::string recent_connection_delete_button_name =
|
||||
trash_can + "##RecentConnectionDelete" +
|
||||
std::to_string(trash_can_button_pos.x);
|
||||
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
|
||||
ImVec2(recent_connection_button_width,
|
||||
recent_connection_button_height))) {
|
||||
show_confirm_delete_connection_ = true;
|
||||
delete_connection_name_ = it.first;
|
||||
}
|
||||
ImVec2 footer_screen_pos =
|
||||
ImVec2(image_screen_pos.x,
|
||||
image_screen_pos.y + recent_connection_image_height);
|
||||
|
||||
if (delete_connection_ && delete_connection_name_ == it.first) {
|
||||
if (!thumbnail_->DeleteThumbnail(it.first)) {
|
||||
reload_recent_connections_ = true;
|
||||
delete_connection_ = false;
|
||||
ImVec2 footer_screen_end =
|
||||
ImVec2(footer_screen_pos.x + recent_connection_name_width,
|
||||
footer_screen_pos.y + recent_connection_footer_height);
|
||||
|
||||
float footer_rounding = recent_connection_footer_height * 0.16f;
|
||||
|
||||
draw_list->AddRectFilled(footer_screen_pos, footer_screen_end,
|
||||
IM_COL32(0, 0, 0, 40), footer_rounding,
|
||||
ImDrawFlags_RoundCornersBottom);
|
||||
|
||||
const float status_left_margin = recent_connection_footer_height * 0.22f;
|
||||
const float status_gap = recent_connection_footer_height * 0.20f;
|
||||
const float dot_radius = recent_connection_footer_height * 0.18f;
|
||||
|
||||
const ImVec2 dot_center(
|
||||
footer_screen_pos.x + status_left_margin + dot_radius,
|
||||
footer_screen_pos.y + recent_connection_footer_height * 0.5f);
|
||||
const ImU32 dot_color =
|
||||
online ? IM_COL32(34, 197, 94, 255) : IM_COL32(156, 163, 175, 255);
|
||||
|
||||
// Layered halo simulates a radial gradient glow for the online state.
|
||||
if (online) {
|
||||
const float halo_radius = dot_radius * 2.2f;
|
||||
const int halo_layers = 8;
|
||||
for (int i = halo_layers; i > 0; --i) {
|
||||
const float t = static_cast<float>(i) / halo_layers;
|
||||
const float r = dot_radius + (halo_radius - dot_radius) * t;
|
||||
const int alpha = static_cast<int>(14.0f * (1.0f - t) + 4.0f);
|
||||
draw_list->AddCircleFilled(
|
||||
dot_center, r, IM_COL32(74, 222, 128, alpha));
|
||||
}
|
||||
}
|
||||
draw_list->AddCircleFilled(dot_center, dot_radius, dot_color);
|
||||
|
||||
const float status_block_end_x = dot_center.x + dot_radius;
|
||||
|
||||
ImVec2 text_min =
|
||||
ImVec2(status_block_end_x + status_gap, footer_screen_pos.y);
|
||||
|
||||
ImVec2 text_max =
|
||||
ImVec2(footer_screen_end.x - recent_connection_name_width * 0.05f,
|
||||
footer_screen_end.y);
|
||||
|
||||
ImGui::SetWindowFontScale(0.52f);
|
||||
|
||||
ImGui::RenderTextClipped(text_min, text_max, display_name.c_str(),
|
||||
nullptr, nullptr, ImVec2(0.0f, 0.5f));
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
}
|
||||
|
||||
// connect button
|
||||
{
|
||||
ImVec2 connect_button_pos =
|
||||
ImVec2(image_pos.x + recent_connection_image_width -
|
||||
recent_connection_button_width,
|
||||
image_pos.y + recent_connection_image_height);
|
||||
ImGui::SetCursorPos(connect_button_pos);
|
||||
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
|
||||
std::string connect_to_this_connection_button_name =
|
||||
connect + "##ConnectionTo" + it.first;
|
||||
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
|
||||
ImVec2(recent_connection_button_width,
|
||||
recent_connection_button_height))) {
|
||||
ConnectTo(it.second.remote_id, it.second.password.c_str(),
|
||||
it.second.remember_password);
|
||||
// toolbar / three buttons
|
||||
if (card_hovered) {
|
||||
float toolbar_rounding = recent_connection_button_height * 0.22f;
|
||||
|
||||
draw_list->AddRectFilled(
|
||||
ImVec2(toolbar_screen_pos.x, toolbar_screen_pos.y + 1.0f),
|
||||
ImVec2(toolbar_screen_end.x, toolbar_screen_end.y + 1.0f),
|
||||
IM_COL32(0, 0, 0, 70), toolbar_rounding);
|
||||
|
||||
draw_list->AddRectFilled(toolbar_screen_pos, toolbar_screen_end,
|
||||
IM_COL32(20, 24, 30, 170), toolbar_rounding);
|
||||
|
||||
draw_list->AddRect(toolbar_screen_pos, toolbar_screen_end,
|
||||
IM_COL32(255, 255, 255, 48), toolbar_rounding);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toolbar_rounding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 0.18f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(0.35f, 0.55f, 0.95f, 0.45f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 0.95f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
|
||||
// edit alias button
|
||||
{
|
||||
ImGui::SetCursorPos(toolbar_pos);
|
||||
|
||||
std::string edit = ICON_FA_PEN;
|
||||
std::string recent_connection_edit_button_name =
|
||||
edit + "##RecentConnectionAlias" + it.first;
|
||||
|
||||
if (ImGui::Button(recent_connection_edit_button_name.c_str(),
|
||||
ImVec2(recent_connection_button_width,
|
||||
recent_connection_button_height))) {
|
||||
BeginEditRecentConnectionAlias(it.second);
|
||||
}
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
SetDarkTextTooltip(
|
||||
localization::connection_alias[localization_language_index_]
|
||||
.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// trash button
|
||||
{
|
||||
ImVec2 trash_can_button_pos = ImVec2(
|
||||
toolbar_pos.x + recent_connection_button_width, toolbar_pos.y);
|
||||
|
||||
ImGui::SetCursorPos(trash_can_button_pos);
|
||||
|
||||
std::string trash_can = ICON_FA_TRASH_CAN;
|
||||
std::string recent_connection_delete_button_name =
|
||||
trash_can + "##RecentConnectionDelete" + it.first;
|
||||
|
||||
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
|
||||
ImVec2(recent_connection_button_width,
|
||||
recent_connection_button_height))) {
|
||||
show_confirm_delete_connection_ = true;
|
||||
delete_connection_name_ = it.first;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
SetDarkTextTooltip(
|
||||
localization::delete_connection[localization_language_index_]
|
||||
.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// connect button
|
||||
{
|
||||
ImVec2 connect_button_pos = ImVec2(
|
||||
toolbar_pos.x + 2 * recent_connection_button_width, toolbar_pos.y);
|
||||
|
||||
ImGui::SetCursorPos(connect_button_pos);
|
||||
|
||||
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
|
||||
std::string connect_to_this_connection_button_name =
|
||||
connect + "##ConnectionTo" + it.first;
|
||||
|
||||
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
|
||||
ImVec2(recent_connection_button_width,
|
||||
recent_connection_button_height))) {
|
||||
ConnectTo(it.second.remote_id, it.second.password.c_str(),
|
||||
it.second.remember_password);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
SetDarkTextTooltip(localization::connect_to_this_connection
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(3);
|
||||
}
|
||||
|
||||
if (count != recent_connections_count - 1) {
|
||||
ImVec2 line_start =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
|
||||
image_screen_pos.y);
|
||||
|
||||
ImVec2 line_end =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
|
||||
image_screen_pos.y + recent_connection_image_height +
|
||||
recent_connection_footer_height);
|
||||
|
||||
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
|
||||
IM_COL32(0, 0, 0, 122), 1.0f);
|
||||
}
|
||||
|
||||
if (delete_connection_ && delete_connection_name_ == it.first) {
|
||||
if (!thumbnail_->DeleteThumbnail(it.first)) {
|
||||
recent_connection_aliases_.erase(it.second.remote_id);
|
||||
SaveRecentConnectionAliases();
|
||||
reload_recent_connections_ = true;
|
||||
delete_connection_ = false;
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -243,7 +436,7 @@ int Render::ShowRecentConnections() {
|
||||
ImVec2 line_end =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
|
||||
image_screen_pos.y + recent_connection_image_height +
|
||||
recent_connection_button_height);
|
||||
recent_connection_footer_height);
|
||||
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
|
||||
IM_COL32(0, 0, 0, 122), 1.0f);
|
||||
}
|
||||
@@ -259,6 +452,9 @@ int Render::ShowRecentConnections() {
|
||||
if (show_confirm_delete_connection_) {
|
||||
ConfirmDeleteConnection();
|
||||
}
|
||||
if (show_edit_connection_alias_window_) {
|
||||
EditRecentConnectionAliasWindow();
|
||||
}
|
||||
if (show_offline_warning_window_) {
|
||||
OfflineWarningWindow();
|
||||
}
|
||||
@@ -320,6 +516,89 @@ int Render::ConfirmDeleteConnection() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::EditRecentConnectionAliasWindow() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
|
||||
ImGui::Begin("EditRecentConnectionAliasWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto window_width = ImGui::GetWindowSize().x;
|
||||
auto window_height = ImGui::GetWindowSize().y;
|
||||
std::string text =
|
||||
localization::input_connection_alias[localization_language_index_];
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
|
||||
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(window_height * 0.2f);
|
||||
ImGui::Text("%s", text.c_str());
|
||||
|
||||
ImGui::SetCursorPosX(window_width * 0.2f);
|
||||
ImGui::SetCursorPosY(window_height * 0.4f);
|
||||
ImGui::SetNextItemWidth(window_width * 0.6f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
|
||||
|
||||
if (focus_on_input_widget_) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
focus_on_input_widget_ = false;
|
||||
}
|
||||
|
||||
bool enter_pressed =
|
||||
ImGui::InputText("##recent_connection_alias", edit_connection_alias_,
|
||||
IM_ARRAYSIZE(edit_connection_alias_),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SetCursorPosX(window_width * 0.315f);
|
||||
ImGui::SetCursorPosY(window_height * 0.75f);
|
||||
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
|
||||
enter_pressed) {
|
||||
std::string alias = TrimConnectionAlias(edit_connection_alias_);
|
||||
if (alias.empty()) {
|
||||
recent_connection_aliases_.erase(edit_connection_alias_remote_id_);
|
||||
} else {
|
||||
recent_connection_aliases_[edit_connection_alias_remote_id_] = alias;
|
||||
}
|
||||
|
||||
SaveRecentConnectionAliases();
|
||||
show_edit_connection_alias_window_ = false;
|
||||
focus_on_input_widget_ = true;
|
||||
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
|
||||
edit_connection_alias_remote_id_.clear();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(
|
||||
localization::cancel[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_edit_connection_alias_window_ = false;
|
||||
focus_on_input_widget_ = true;
|
||||
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
|
||||
edit_connection_alias_remote_id_.clear();
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::OfflineWarningWindow() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGui::SetNextWindowPos(
|
||||
@@ -360,4 +639,4 @@ int Render::OfflineWarningWindow() {
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -211,7 +211,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->control_window_max_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_max_height_ = title_bar_height_ * 7.0f;
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
show_connection_status_window_ = true;
|
||||
|
||||
if (!props->peer_) {
|
||||
@@ -231,7 +231,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
|
||||
peer_to_init = props->peer_;
|
||||
local_id = props->local_id_;
|
||||
|
||||
+293
-35
@@ -13,6 +13,9 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -609,6 +612,11 @@ int Render::LoadSettingsFromCacheFile() {
|
||||
enable_autostart_ = config_center_->IsEnableAutostart();
|
||||
enable_daemon_ = config_center_->IsEnableDaemon();
|
||||
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
portable_service_prompt_suppressed_ =
|
||||
config_center_->IsPortableServicePromptSuppressed();
|
||||
portable_service_do_not_remind_ = portable_service_prompt_suppressed_;
|
||||
#endif
|
||||
|
||||
// File transfer save path
|
||||
{
|
||||
@@ -635,6 +643,113 @@ int Render::LoadSettingsFromCacheFile() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::LoadRecentConnectionAliases() {
|
||||
recent_connection_aliases_.clear();
|
||||
|
||||
std::ifstream alias_file(cache_path_ + "/recent_connection_aliases.json");
|
||||
if (!alias_file.good()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
nlohmann::json alias_json;
|
||||
alias_file >> alias_json;
|
||||
|
||||
const nlohmann::json* aliases = &alias_json;
|
||||
if (alias_json.contains("aliases") && alias_json["aliases"].is_object()) {
|
||||
aliases = &alias_json["aliases"];
|
||||
}
|
||||
|
||||
if (!aliases->is_object()) {
|
||||
LOG_WARN("Invalid recent connection alias file");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (auto it = aliases->begin(); it != aliases->end(); ++it) {
|
||||
if (!it.value().is_string()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string remote_id = it.key();
|
||||
std::string alias = it.value().get<std::string>();
|
||||
if (!remote_id.empty() && !alias.empty()) {
|
||||
recent_connection_aliases_[remote_id] = alias;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARN("Load recent connection aliases failed: {}", e.what());
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::SaveRecentConnectionAliases() const {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(cache_path_, ec);
|
||||
if (ec) {
|
||||
LOG_WARN("Create cache directory failed while saving aliases: {}",
|
||||
ec.message());
|
||||
return -1;
|
||||
}
|
||||
|
||||
nlohmann::json alias_json;
|
||||
alias_json["aliases"] = nlohmann::json::object();
|
||||
|
||||
for (const auto& [remote_id, alias] : recent_connection_aliases_) {
|
||||
if (!remote_id.empty() && !alias.empty()) {
|
||||
alias_json["aliases"][remote_id] = alias;
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream alias_file(cache_path_ + "/recent_connection_aliases.json",
|
||||
std::ios::trunc);
|
||||
if (!alias_file.good()) {
|
||||
LOG_WARN("Open recent connection alias file failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
alias_file << alias_json.dump(2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Render::GetRecentConnectionDisplayName(
|
||||
const Thumbnail::RecentConnection& connection) const {
|
||||
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
|
||||
if (alias_it != recent_connection_aliases_.end() &&
|
||||
!alias_it->second.empty()) {
|
||||
return alias_it->second;
|
||||
}
|
||||
|
||||
if (!connection.remote_host_name.empty() &&
|
||||
connection.remote_host_name != "unknown") {
|
||||
return connection.remote_host_name;
|
||||
}
|
||||
|
||||
return connection.remote_id;
|
||||
}
|
||||
|
||||
void Render::BeginEditRecentConnectionAlias(
|
||||
const Thumbnail::RecentConnection& connection) {
|
||||
edit_connection_alias_remote_id_ = connection.remote_id;
|
||||
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
|
||||
|
||||
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
|
||||
std::string alias =
|
||||
alias_it != recent_connection_aliases_.end()
|
||||
? alias_it->second
|
||||
: GetRecentConnectionDisplayName(connection);
|
||||
|
||||
if (!alias.empty()) {
|
||||
strncpy(edit_connection_alias_, alias.c_str(),
|
||||
sizeof(edit_connection_alias_) - 1);
|
||||
edit_connection_alias_[sizeof(edit_connection_alias_) - 1] = '\0';
|
||||
}
|
||||
|
||||
focus_on_input_widget_ = true;
|
||||
show_edit_connection_alias_window_ = true;
|
||||
}
|
||||
|
||||
int Render::ScreenCapturerInit() {
|
||||
#ifdef __APPLE__
|
||||
if (!EnsureMacScreenRecordingPermission()) {
|
||||
@@ -764,11 +879,16 @@ int Render::StartSpeakerCapturer() {
|
||||
}
|
||||
|
||||
if (speaker_capturer_) {
|
||||
speaker_capturer_->Start();
|
||||
const int ret = speaker_capturer_->Start();
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Start speaker capturer failed: {}", ret);
|
||||
return ret;
|
||||
}
|
||||
start_speaker_capturer_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int Render::StopSpeakerCapturer() {
|
||||
@@ -1149,8 +1269,9 @@ void Render::UpdateInteractions() {
|
||||
}
|
||||
|
||||
if (start_speaker_capturer_ && !speaker_capturer_is_started_) {
|
||||
StartSpeakerCapturer();
|
||||
speaker_capturer_is_started_ = true;
|
||||
if (0 == StartSpeakerCapturer()) {
|
||||
speaker_capturer_is_started_ = true;
|
||||
}
|
||||
} else if (!start_speaker_capturer_ && speaker_capturer_is_started_) {
|
||||
StopSpeakerCapturer();
|
||||
speaker_capturer_is_started_ = false;
|
||||
@@ -1181,10 +1302,16 @@ void Render::UpdateInteractions() {
|
||||
keyboard_capturer_is_started_ = true;
|
||||
}
|
||||
}
|
||||
if (keyboard_capturer_is_started_) {
|
||||
SendKeyboardHeartbeat(false);
|
||||
}
|
||||
} else if (keyboard_capturer_is_started_) {
|
||||
ForceReleasePressedKeys();
|
||||
StopKeyboardCapturer();
|
||||
keyboard_capturer_is_started_ = false;
|
||||
}
|
||||
|
||||
CheckRemoteKeyboardTimeouts();
|
||||
}
|
||||
|
||||
int Render::CreateMainWindow() {
|
||||
@@ -1247,6 +1374,13 @@ int Render::CreateMainWindow() {
|
||||
HICON tray_icon = LoadTrayIcon();
|
||||
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
|
||||
localization_language_index_);
|
||||
#elif defined(__APPLE__)
|
||||
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
|
||||
localization_language_index_);
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
tray_ = std::make_unique<LinuxTray>(main_window_, "CrossDesk",
|
||||
localization_language_index_,
|
||||
APP_EXIT_EVENT);
|
||||
#endif
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
|
||||
@@ -1546,20 +1680,47 @@ int Render::SetupFontAndStyle(ImFont** system_chinese_font_out) {
|
||||
}
|
||||
|
||||
int Render::DestroyMainWindowContext() {
|
||||
if (!main_ctx_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(main_ctx_);
|
||||
main_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::DestroyStreamWindowContext() {
|
||||
if (!stream_ctx_) {
|
||||
stream_window_inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
stream_window_inited_ = false;
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(stream_ctx_);
|
||||
stream_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::DestroyServerWindowContext() {
|
||||
if (!server_ctx_) {
|
||||
server_window_inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
server_window_inited_ = false;
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(server_ctx_);
|
||||
server_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1708,26 +1869,6 @@ int Render::DrawServerWindow() {
|
||||
}
|
||||
|
||||
int Render::Run() {
|
||||
latest_version_info_ = CheckUpdate();
|
||||
if (!latest_version_info_.empty() &&
|
||||
latest_version_info_.contains("version") &&
|
||||
latest_version_info_["version"].is_string()) {
|
||||
latest_version_ = 'v' + latest_version_info_["version"].get<std::string>();
|
||||
if (latest_version_info_.contains("releaseNotes") &&
|
||||
latest_version_info_["releaseNotes"].is_string()) {
|
||||
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
|
||||
} else {
|
||||
release_notes_ = "";
|
||||
}
|
||||
update_available_ = IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
||||
if (update_available_) {
|
||||
show_update_notification_window_ = true;
|
||||
}
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
update_available_ = false;
|
||||
}
|
||||
|
||||
path_manager_ = std::make_unique<PathManager>("CrossDesk");
|
||||
if (path_manager_) {
|
||||
exec_log_path_ = path_manager_->GetLogPath().string();
|
||||
@@ -1756,6 +1897,41 @@ int Render::Run() {
|
||||
InitializeLogger();
|
||||
LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION);
|
||||
|
||||
latest_version_info_ = CheckUpdate();
|
||||
if (!latest_version_info_.empty()) {
|
||||
std::string version;
|
||||
if (latest_version_info_.contains("latest_version") &&
|
||||
latest_version_info_["latest_version"].is_string()) {
|
||||
version = latest_version_info_["latest_version"].get<std::string>();
|
||||
} else if (latest_version_info_.contains("version") &&
|
||||
latest_version_info_["version"].is_string()) {
|
||||
version = latest_version_info_["version"].get<std::string>();
|
||||
}
|
||||
|
||||
if (!version.empty()) {
|
||||
latest_version_ = 'v' + version;
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
}
|
||||
if (latest_version_info_.contains("releaseNotes") &&
|
||||
latest_version_info_["releaseNotes"].is_string()) {
|
||||
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
|
||||
} else {
|
||||
release_notes_ = "";
|
||||
}
|
||||
update_available_ =
|
||||
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
||||
LOG_INFO("Update check: current={}, latest={}, available={}",
|
||||
CROSSDESK_VERSION, latest_version_, update_available_);
|
||||
if (update_available_) {
|
||||
show_update_notification_window_ = true;
|
||||
}
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
update_available_ = false;
|
||||
LOG_WARN("Update check skipped: version.json is empty or missing latest_version");
|
||||
}
|
||||
|
||||
InitializeSettings();
|
||||
InitializeSDL();
|
||||
InitializeModules();
|
||||
@@ -1779,6 +1955,7 @@ void Render::InitializeLogger() { InitLogger(exec_log_path_); }
|
||||
|
||||
void Render::InitializeSettings() {
|
||||
LoadSettingsFromCacheFile();
|
||||
LoadRecentConnectionAliases();
|
||||
|
||||
localization_language_index_ =
|
||||
localization::detail::ClampLanguageIndex(language_button_value_);
|
||||
@@ -1812,9 +1989,12 @@ void Render::InitializeSDL() {
|
||||
screen_height_ = dm->h;
|
||||
}
|
||||
|
||||
STREAM_REFRESH_EVENT = SDL_RegisterEvents(1);
|
||||
if (STREAM_REFRESH_EVENT == (uint32_t)-1) {
|
||||
LOG_ERROR("Failed to register custom SDL event");
|
||||
const uint32_t custom_event_base = SDL_RegisterEvents(2);
|
||||
if (custom_event_base == static_cast<uint32_t>(-1)) {
|
||||
LOG_ERROR("Failed to register custom SDL events");
|
||||
} else {
|
||||
STREAM_REFRESH_EVENT = custom_event_base;
|
||||
APP_EXIT_EVENT = custom_event_base + 1;
|
||||
}
|
||||
|
||||
LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_);
|
||||
@@ -1888,6 +2068,10 @@ void Render::MainLoop() {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
if (tray_) {
|
||||
tray_->ProcessEvents();
|
||||
}
|
||||
#endif
|
||||
|
||||
UpdateLabels();
|
||||
@@ -1898,7 +2082,11 @@ void Render::MainLoop() {
|
||||
HandleServerWindow();
|
||||
HandleWindowsServiceIntegration();
|
||||
|
||||
DrawMainWindow();
|
||||
const bool main_window_visible =
|
||||
main_window_ && !(SDL_GetWindowFlags(main_window_) & SDL_WINDOW_HIDDEN);
|
||||
if (main_window_visible) {
|
||||
DrawMainWindow();
|
||||
}
|
||||
if (stream_window_inited_) {
|
||||
DrawStreamWindow();
|
||||
}
|
||||
@@ -1911,6 +2099,29 @@ void Render::MainLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
bool Render::MinimizeMainWindowToTray() {
|
||||
if (!enable_minimize_to_tray_ || !main_window_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
if (!tray_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tray_->MinimizeToTray();
|
||||
return true;
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
if (!tray_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return tray_->MinimizeToTray();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Render::UpdateLabels() {
|
||||
if (!label_inited_ ||
|
||||
localization_language_index_last_ != localization_language_index_) {
|
||||
@@ -1967,11 +2178,13 @@ void Render::HandleWindowsServiceIntegration() {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool has_connected_remote =
|
||||
std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||
[](const auto& entry) {
|
||||
return entry.second == ConnectionStatus::Connected;
|
||||
});
|
||||
const bool has_connected_remote = [&] {
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
return std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||
[](const auto& entry) {
|
||||
return entry.second == ConnectionStatus::Connected;
|
||||
});
|
||||
}();
|
||||
if (!has_connected_remote) {
|
||||
ResetLocalWindowsServiceState(false);
|
||||
return;
|
||||
@@ -2247,6 +2460,7 @@ void Render::HandleServerWindow() {
|
||||
|
||||
if (need_to_destroy_server_window_) {
|
||||
DestroyServerWindow();
|
||||
DestroyServerWindowContext();
|
||||
need_to_destroy_server_window_ = false;
|
||||
}
|
||||
}
|
||||
@@ -2287,6 +2501,27 @@ void Render::Cleanup() {
|
||||
WaitForThumbnailSaveTasks();
|
||||
|
||||
AudioDeviceDestroy();
|
||||
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
||||
tray_.reset();
|
||||
#endif
|
||||
|
||||
if (stream_window_created_) {
|
||||
if (stream_window_) {
|
||||
SDL_SetWindowMouseGrab(stream_window_, false);
|
||||
}
|
||||
DestroyStreamWindow();
|
||||
}
|
||||
if (stream_ctx_) {
|
||||
DestroyStreamWindowContext();
|
||||
}
|
||||
|
||||
if (server_window_created_) {
|
||||
DestroyServerWindow();
|
||||
}
|
||||
if (server_ctx_) {
|
||||
DestroyServerWindowContext();
|
||||
}
|
||||
|
||||
DestroyMainWindowContext();
|
||||
DestroyMainWindow();
|
||||
SDL_Quit();
|
||||
@@ -2700,6 +2935,20 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
}
|
||||
}
|
||||
|
||||
if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) {
|
||||
LOG_INFO("Quit program from system tray");
|
||||
if (stream_window_) {
|
||||
SDL_SetWindowMouseGrab(stream_window_, false);
|
||||
}
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (tray_) {
|
||||
tray_->RemoveTrayIcon();
|
||||
}
|
||||
#endif
|
||||
exit_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_QUIT:
|
||||
if (stream_window_inited_) {
|
||||
@@ -2770,9 +3019,18 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
if (event.window.windowID != SDL_GetWindowID(stream_window_)) {
|
||||
exit_ = true;
|
||||
if (stream_window_ &&
|
||||
event.window.windowID == SDL_GetWindowID(stream_window_)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (main_window_ &&
|
||||
event.window.windowID == SDL_GetWindowID(main_window_) &&
|
||||
MinimizeMainWindowToTray()) {
|
||||
break;
|
||||
}
|
||||
|
||||
exit_ = true;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
|
||||
+69
-8
@@ -39,6 +39,10 @@
|
||||
|
||||
#if _WIN32
|
||||
#include "win_tray.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "mac_tray.h"
|
||||
#elif defined(__linux__)
|
||||
#include "linux_tray.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -180,7 +184,11 @@ class Render {
|
||||
SDL_Rect stream_render_rect_;
|
||||
SDL_Rect stream_render_rect_last_;
|
||||
ImVec2 control_window_pos_;
|
||||
ConnectionStatus connection_status_ = ConnectionStatus::Closed;
|
||||
// Written from the minirtc/libnice callback thread (OnConnectionStatusCb)
|
||||
// and the SDL main thread (remote_peer_panel connect button); read from
|
||||
// the SDL main render thread (stream/control windows) and the SDL audio
|
||||
// thread (SdlCaptureAudioIn). Atomic so those reads/writes are defined.
|
||||
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
|
||||
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
||||
int fps_ = 0;
|
||||
int frame_count_ = 0;
|
||||
@@ -278,6 +286,7 @@ class Render {
|
||||
int DrawStreamWindow();
|
||||
int DrawServerWindow();
|
||||
int ConfirmDeleteConnection();
|
||||
int EditRecentConnectionAliasWindow();
|
||||
int OfflineWarningWindow();
|
||||
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void DrawConnectionStatusText(
|
||||
@@ -343,11 +352,35 @@ class Render {
|
||||
static void FreeRemoteAction(RemoteAction& action);
|
||||
|
||||
private:
|
||||
struct PressedKeyboardKey {
|
||||
int key_code = 0;
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
};
|
||||
|
||||
struct RemoteKeyboardState {
|
||||
std::unordered_map<int, PressedKeyboardKey> pressed_keys;
|
||||
uint32_t last_seq = 0;
|
||||
uint32_t last_seen_tick = 0;
|
||||
bool keyboard_state_seen = false;
|
||||
};
|
||||
|
||||
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
static bool IsModifierVkKey(int key_code);
|
||||
void TrackPressedKeyState(int key_code, bool is_down);
|
||||
void TrackPressedKeyState(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
void ForceReleasePressedKeys();
|
||||
void SendKeyboardHeartbeat(bool force);
|
||||
void ApplyRemoteKeyboardEvent(const std::string& remote_id,
|
||||
const RemoteAction& remote_action);
|
||||
void ApplyRemoteKeyboardState(const std::string& remote_id,
|
||||
const RemoteAction& remote_action);
|
||||
bool InjectRemoteKeyboardKey(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
void ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
const char* reason);
|
||||
void CheckRemoteKeyboardTimeouts();
|
||||
int ProcessKeyboardEvent(const SDL_Event& event);
|
||||
int ProcessMouseEvent(const SDL_Event& event);
|
||||
|
||||
@@ -357,6 +390,12 @@ class Render {
|
||||
private:
|
||||
int SaveSettingsIntoCacheFile();
|
||||
int LoadSettingsFromCacheFile();
|
||||
int LoadRecentConnectionAliases();
|
||||
int SaveRecentConnectionAliases() const;
|
||||
std::string GetRecentConnectionDisplayName(
|
||||
const Thumbnail::RecentConnection& connection) const;
|
||||
void BeginEditRecentConnectionAlias(
|
||||
const Thumbnail::RecentConnection& connection);
|
||||
|
||||
int ScreenCapturerInit();
|
||||
int StartScreenCapturer();
|
||||
@@ -383,6 +422,7 @@ class Render {
|
||||
int AudioDeviceInit();
|
||||
int AudioDeviceDestroy();
|
||||
void HandleWindowsServiceIntegration();
|
||||
bool MinimizeMainWindowToTray();
|
||||
#if _WIN32
|
||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||
#if CROSSDESK_PORTABLE
|
||||
@@ -479,6 +519,10 @@ class Render {
|
||||
const int sdl_refresh_ms_ = 16; // ~60 FPS
|
||||
#if _WIN32
|
||||
std::unique_ptr<WinTray> tray_;
|
||||
#elif defined(__APPLE__)
|
||||
std::unique_ptr<MacTray> tray_;
|
||||
#elif defined(__linux__)
|
||||
std::unique_ptr<LinuxTray> tray_;
|
||||
#endif
|
||||
|
||||
// main window properties
|
||||
@@ -551,11 +595,16 @@ class Render {
|
||||
std::string controlled_remote_id_ = "";
|
||||
std::string focused_remote_id_ = "";
|
||||
std::string remote_client_id_ = "";
|
||||
std::unordered_set<int> pressed_keyboard_keys_;
|
||||
std::unordered_map<int, PressedKeyboardKey> pressed_keyboard_keys_;
|
||||
std::mutex pressed_keyboard_keys_mutex_;
|
||||
SDL_Event last_mouse_event;
|
||||
SDL_AudioStream* output_stream_;
|
||||
uint32_t keyboard_state_seq_ = 0;
|
||||
uint32_t last_keyboard_heartbeat_tick_ = 0;
|
||||
std::unordered_map<std::string, RemoteKeyboardState> remote_keyboard_states_;
|
||||
std::mutex remote_keyboard_states_mutex_;
|
||||
SDL_Event last_mouse_event{};
|
||||
SDL_AudioStream* output_stream_ = nullptr;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
uint32_t APP_EXIT_EVENT = 0;
|
||||
#if _WIN32
|
||||
std::atomic<bool> pending_windows_service_sas_{false};
|
||||
bool local_service_status_received_ = false;
|
||||
@@ -567,6 +616,9 @@ class Render {
|
||||
#if CROSSDESK_PORTABLE
|
||||
bool portable_service_prompt_checked_ = false;
|
||||
bool show_portable_service_install_window_ = false;
|
||||
bool show_portable_service_prompt_suppressed_window_ = false;
|
||||
bool portable_service_do_not_remind_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::atomic<PortableServiceInstallState> portable_service_install_state_{
|
||||
PortableServiceInstallState::idle};
|
||||
std::thread portable_service_install_thread_;
|
||||
@@ -652,10 +704,14 @@ class Render {
|
||||
bool is_server_mode_ = false;
|
||||
bool reload_recent_connections_ = true;
|
||||
bool show_confirm_delete_connection_ = false;
|
||||
bool show_edit_connection_alias_window_ = false;
|
||||
bool show_offline_warning_window_ = false;
|
||||
bool delete_connection_ = false;
|
||||
bool is_tab_bar_hovered_ = false;
|
||||
std::string delete_connection_name_ = "";
|
||||
std::unordered_map<std::string, std::string> recent_connection_aliases_;
|
||||
std::string edit_connection_alias_remote_id_ = "";
|
||||
char edit_connection_alias_[128] = "";
|
||||
std::string offline_warning_text_ = "";
|
||||
bool re_enter_remote_id_ = false;
|
||||
double copy_start_time_ = 0;
|
||||
@@ -684,8 +740,8 @@ class Render {
|
||||
// Map file_id to FileTransferState for global file transfer (props == null)
|
||||
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
|
||||
std::shared_mutex file_id_to_transfer_state_mutex_;
|
||||
SDL_AudioDeviceID input_dev_;
|
||||
SDL_AudioDeviceID output_dev_;
|
||||
SDL_AudioDeviceID input_dev_ = 0;
|
||||
SDL_AudioDeviceID output_dev_ = 0;
|
||||
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
|
||||
ScreenCapturer* screen_capturer_ = nullptr;
|
||||
SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr;
|
||||
@@ -694,7 +750,7 @@ class Render {
|
||||
MouseController* mouse_controller_ = nullptr;
|
||||
KeyboardCapturer* keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
uint64_t last_frame_time_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
@@ -764,6 +820,11 @@ class Render {
|
||||
void WaitForThumbnailSaveTasks();
|
||||
|
||||
/* ------ server mode ------ */
|
||||
// connection_status_ / connection_host_names_ are read on the main
|
||||
// render thread (DrawServerWindow, HandleWindowsServiceIntegration) and
|
||||
// written from minirtc/libnice callback threads (OnConnectionStatusCb,
|
||||
// OnReceiveDataBufferCb). Guard every access with this shared mutex.
|
||||
std::shared_mutex connection_status_mutex_;
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
std::unordered_map<std::string, std::string> connection_host_names_;
|
||||
std::string selected_server_remote_id_ = "";
|
||||
|
||||
+340
-84
@@ -6,6 +6,9 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -28,6 +31,8 @@
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kKeyboardHeartbeatIntervalMs = 500;
|
||||
constexpr uint32_t kRemoteKeyboardReleaseTimeoutMs = 2500;
|
||||
|
||||
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) {
|
||||
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
|
||||
@@ -415,34 +420,92 @@ bool Render::IsModifierVkKey(int key_code) {
|
||||
}
|
||||
}
|
||||
|
||||
void Render::TrackPressedKeyState(int key_code, bool is_down) {
|
||||
if (!IsWaylandSession() && !IsModifierVkKey(key_code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
void Render::TrackPressedKeyState(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
||||
if (is_down) {
|
||||
pressed_keyboard_keys_.insert(key_code);
|
||||
pressed_keyboard_keys_[key_code] =
|
||||
PressedKeyboardKey{key_code, scan_code, extended};
|
||||
} else {
|
||||
pressed_keyboard_keys_.erase(key_code);
|
||||
}
|
||||
}
|
||||
|
||||
void Render::ForceReleasePressedKeys() {
|
||||
std::vector<int> pressed_keys;
|
||||
std::vector<PressedKeyboardKey> pressed_keys;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
||||
if (pressed_keyboard_keys_.empty()) {
|
||||
return;
|
||||
pressed_keys.reserve(pressed_keyboard_keys_.size());
|
||||
for (const auto& [_, key] : pressed_keyboard_keys_) {
|
||||
pressed_keys.push_back(key);
|
||||
}
|
||||
pressed_keys.assign(pressed_keyboard_keys_.begin(),
|
||||
pressed_keyboard_keys_.end());
|
||||
pressed_keyboard_keys_.clear();
|
||||
}
|
||||
|
||||
for (int key_code : pressed_keys) {
|
||||
SendKeyCommand(key_code, false);
|
||||
for (const PressedKeyboardKey& key : pressed_keys) {
|
||||
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
SendKeyboardHeartbeat(true);
|
||||
}
|
||||
|
||||
void Render::SendKeyboardHeartbeat(bool force) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (!force && now - last_keyboard_heartbeat_tick_ <
|
||||
kKeyboardHeartbeatIntervalMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
RemoteAction remote_action{};
|
||||
remote_action.type = ControlType::keyboard_state;
|
||||
remote_action.ks.seq = ++keyboard_state_seq_;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
||||
size_t idx = 0;
|
||||
for (const auto& [_, key] : pressed_keyboard_keys_) {
|
||||
if (idx >= kMaxKeyboardStateKeys) {
|
||||
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
|
||||
pressed_keyboard_keys_.size());
|
||||
break;
|
||||
}
|
||||
remote_action.ks.pressed_keys[idx].key_value =
|
||||
static_cast<size_t>(key.key_code);
|
||||
remote_action.ks.pressed_keys[idx].scan_code = key.scan_code;
|
||||
remote_action.ks.pressed_keys[idx].extended = key.extended;
|
||||
++idx;
|
||||
}
|
||||
remote_action.ks.pressed_count = idx;
|
||||
}
|
||||
|
||||
const std::string target_id = controlled_remote_id_.empty()
|
||||
? focused_remote_id_
|
||||
: controlled_remote_id_;
|
||||
if (target_id.empty()) {
|
||||
last_keyboard_heartbeat_tick_ = now;
|
||||
return;
|
||||
}
|
||||
|
||||
auto props_it = client_properties_.find(target_id);
|
||||
if (props_it == client_properties_.end()) {
|
||||
last_keyboard_heartbeat_tick_ = now;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto props = props_it->second;
|
||||
if (props->connection_status_.load() != ConnectionStatus::Connected ||
|
||||
!props->peer_) {
|
||||
last_keyboard_heartbeat_tick_ = now;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string msg = remote_action.to_json();
|
||||
const int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->keyboard_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id,
|
||||
ret);
|
||||
}
|
||||
last_keyboard_heartbeat_tick_ = now;
|
||||
}
|
||||
|
||||
int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
@@ -471,7 +534,7 @@ int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
if (!target_id.empty()) {
|
||||
if (client_properties_.find(target_id) != client_properties_.end()) {
|
||||
auto props = client_properties_[target_id];
|
||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected &&
|
||||
props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
@@ -484,7 +547,7 @@ int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
}
|
||||
}
|
||||
|
||||
TrackPressedKeyState(key_code, is_down);
|
||||
TrackPressedKeyState(key_code, is_down, scan_code, extended);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -506,6 +569,181 @@ int Render::ProcessKeyboardEvent(const SDL_Event& event) {
|
||||
return SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
|
||||
}
|
||||
|
||||
bool Render::InjectRemoteKeyboardKey(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
#if _WIN32
|
||||
if (local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(local_interactive_stage_)) {
|
||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||
key_code, is_down, scan_code, extended, 1000);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::keyboard;
|
||||
action.k.key_value = static_cast<size_t>(key_code);
|
||||
action.k.scan_code = scan_code;
|
||||
action.k.extended = extended;
|
||||
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
|
||||
if (!json.is_discarded() &&
|
||||
IsTransientSecureDesktopInputFailure(json, action)) {
|
||||
LOG_INFO(
|
||||
"Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
LogSecureDesktopInputBlocked(&last_local_secure_input_block_log_tick_,
|
||||
"local",
|
||||
local_interactive_stage_.c_str());
|
||||
LOG_WARN(
|
||||
"Secure desktop keyboard injection failed, key_code={}, is_down={}, "
|
||||
"response={}",
|
||||
key_code, is_down, response);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!keyboard_capturer_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return keyboard_capturer_->SendKeyboardCommand(key_code, is_down, scan_code,
|
||||
extended) == 0;
|
||||
}
|
||||
|
||||
void Render::ApplyRemoteKeyboardEvent(const std::string& remote_id,
|
||||
const RemoteAction& remote_action) {
|
||||
const int key_code = static_cast<int>(remote_action.k.key_value);
|
||||
const bool is_down = remote_action.k.flag == KeyFlag::key_down;
|
||||
const uint32_t scan_code = remote_action.k.scan_code;
|
||||
const bool extended = remote_action.k.extended;
|
||||
const bool injected =
|
||||
InjectRemoteKeyboardKey(key_code, is_down, scan_code, extended);
|
||||
|
||||
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
|
||||
auto& state = remote_keyboard_states_[remote_id];
|
||||
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (is_down) {
|
||||
if (injected) {
|
||||
state.pressed_keys[key_code] =
|
||||
PressedKeyboardKey{key_code, scan_code, extended};
|
||||
}
|
||||
} else if (injected) {
|
||||
state.pressed_keys.erase(key_code);
|
||||
}
|
||||
}
|
||||
|
||||
void Render::ApplyRemoteKeyboardState(const std::string& remote_id,
|
||||
const RemoteAction& remote_action) {
|
||||
std::vector<PressedKeyboardKey> keys_to_release;
|
||||
std::vector<PressedKeyboardKey> keys_to_press;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
|
||||
auto& state = remote_keyboard_states_[remote_id];
|
||||
if (remote_action.ks.seq != 0 && state.last_seq != 0 &&
|
||||
static_cast<int32_t>(remote_action.ks.seq - state.last_seq) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.last_seq = remote_action.ks.seq;
|
||||
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
||||
state.keyboard_state_seen = true;
|
||||
|
||||
std::unordered_map<int, PressedKeyboardKey> desired_keys;
|
||||
const size_t pressed_count =
|
||||
remote_action.ks.pressed_count < kMaxKeyboardStateKeys
|
||||
? remote_action.ks.pressed_count
|
||||
: kMaxKeyboardStateKeys;
|
||||
for (size_t idx = 0; idx < pressed_count; ++idx) {
|
||||
const auto& key = remote_action.ks.pressed_keys[idx];
|
||||
const int key_code = static_cast<int>(key.key_value);
|
||||
desired_keys[key_code] =
|
||||
PressedKeyboardKey{key_code, key.scan_code, key.extended};
|
||||
}
|
||||
|
||||
for (const auto& [key_code, key] : state.pressed_keys) {
|
||||
if (desired_keys.find(key_code) == desired_keys.end()) {
|
||||
keys_to_release.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [key_code, key] : desired_keys) {
|
||||
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
|
||||
keys_to_press.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const PressedKeyboardKey& key : keys_to_release) {
|
||||
if (InjectRemoteKeyboardKey(key.key_code, false, key.scan_code,
|
||||
key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
|
||||
auto state_it = remote_keyboard_states_.find(remote_id);
|
||||
if (state_it != remote_keyboard_states_.end()) {
|
||||
state_it->second.pressed_keys.erase(key.key_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const PressedKeyboardKey& key : keys_to_press) {
|
||||
if (InjectRemoteKeyboardKey(key.key_code, true, key.scan_code,
|
||||
key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
|
||||
auto& state = remote_keyboard_states_[remote_id];
|
||||
state.pressed_keys[key.key_code] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Render::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
const char* reason) {
|
||||
std::vector<PressedKeyboardKey> keys_to_release;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
|
||||
auto state_it = remote_keyboard_states_.find(remote_id);
|
||||
if (state_it == remote_keyboard_states_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
keys_to_release.reserve(state_it->second.pressed_keys.size());
|
||||
for (const auto& [_, key] : state_it->second.pressed_keys) {
|
||||
keys_to_release.push_back(key);
|
||||
}
|
||||
remote_keyboard_states_.erase(state_it);
|
||||
}
|
||||
|
||||
if (!keys_to_release.empty()) {
|
||||
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
|
||||
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
||||
}
|
||||
for (const PressedKeyboardKey& key : keys_to_release) {
|
||||
InjectRemoteKeyboardKey(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
}
|
||||
|
||||
void Render::CheckRemoteKeyboardTimeouts() {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
std::vector<std::string> timed_out_remotes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
|
||||
for (const auto& [remote_id, state] : remote_keyboard_states_) {
|
||||
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
|
||||
state.last_seen_tick != 0 &&
|
||||
now - state.last_seen_tick > kRemoteKeyboardReleaseTimeoutMs) {
|
||||
timed_out_remotes.push_back(remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& remote_id : timed_out_remotes) {
|
||||
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
|
||||
}
|
||||
}
|
||||
|
||||
int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
controlled_remote_id_ = "";
|
||||
RemoteAction remote_action{};
|
||||
@@ -693,10 +931,10 @@ void Render::SdlCaptureAudioIn(void* userdata, Uint8* stream, int len) {
|
||||
}
|
||||
|
||||
if (1) {
|
||||
// std::shared_lock lock(render->client_properties_mutex_);
|
||||
std::shared_lock lock(render->client_properties_mutex_);
|
||||
for (const auto& it : render->client_properties_) {
|
||||
auto props = it.second;
|
||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||
if (props->peer_) {
|
||||
SendAudioFrame(props->peer_, (const char*)stream, len,
|
||||
render->audio_label_.c_str());
|
||||
@@ -1089,12 +1327,20 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
return;
|
||||
}
|
||||
|
||||
// std::shared_lock lock(render->client_properties_mutex_);
|
||||
if (remote_action.type == ControlType::host_infomation) {
|
||||
if (render->client_properties_.find(remote_id) !=
|
||||
render->client_properties_.end()) {
|
||||
bool is_client_mode = false;
|
||||
std::shared_ptr<SubStreamWindowProperties> props;
|
||||
{
|
||||
std::shared_lock lock(render->client_properties_mutex_);
|
||||
auto props_it = render->client_properties_.find(remote_id);
|
||||
if (props_it != render->client_properties_.end()) {
|
||||
is_client_mode = true;
|
||||
props = props_it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_client_mode) {
|
||||
// client mode
|
||||
auto props = render->client_properties_.find(remote_id)->second;
|
||||
if (props && props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
@@ -1110,17 +1356,22 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
FreeRemoteAction(remote_action);
|
||||
} else {
|
||||
// server mode
|
||||
render->connection_host_names_[remote_id] = std::string(
|
||||
remote_action.i.host_name, remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]",
|
||||
render->connection_host_names_[remote_id]);
|
||||
std::string host_name(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
{
|
||||
std::unique_lock lock(render->connection_status_mutex_);
|
||||
render->connection_host_names_[remote_id] = host_name;
|
||||
}
|
||||
LOG_INFO("Remote hostname: [{}]", host_name);
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
} else {
|
||||
// remote
|
||||
#if _WIN32
|
||||
if (render->local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(render->local_interactive_stage_)) {
|
||||
IsSecureDesktopInteractionRequired(render->local_interactive_stage_) &&
|
||||
remote_action.type != ControlType::keyboard &&
|
||||
remote_action.type != ControlType::keyboard_state) {
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
int absolute_x = 0;
|
||||
int absolute_y = 0;
|
||||
@@ -1151,33 +1402,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::keyboard) {
|
||||
const int key_code = static_cast<int>(remote_action.k.key_value);
|
||||
const bool is_down = remote_action.k.flag == KeyFlag::key_down;
|
||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||
key_code, is_down, remote_action.k.scan_code,
|
||||
remote_action.k.extended, 1000);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
if (!json.is_discarded() &&
|
||||
IsTransientSecureDesktopInputFailure(json, remote_action)) {
|
||||
LOG_INFO(
|
||||
"Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
return;
|
||||
}
|
||||
LogSecureDesktopInputBlocked(
|
||||
&render->last_local_secure_input_block_log_tick_, "local",
|
||||
render->local_interactive_stage_.c_str());
|
||||
LOG_WARN(
|
||||
"Secure desktop keyboard injection failed, key_code={}, "
|
||||
"is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
|
||||
@@ -1188,16 +1412,19 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
render->StartSpeakerCapturer();
|
||||
else if (!remote_action.a && render->start_speaker_capturer_)
|
||||
render->StopSpeakerCapturer();
|
||||
} else if (remote_action.type == ControlType::keyboard &&
|
||||
render->keyboard_capturer_) {
|
||||
render->keyboard_capturer_->SendKeyboardCommand(
|
||||
(int)remote_action.k.key_value,
|
||||
remote_action.k.flag == KeyFlag::key_down, remote_action.k.scan_code,
|
||||
remote_action.k.extended);
|
||||
} else if (remote_action.type == ControlType::keyboard) {
|
||||
render->ApplyRemoteKeyboardEvent(remote_id, remote_action);
|
||||
} else if (remote_action.type == ControlType::keyboard_state) {
|
||||
render->ApplyRemoteKeyboardState(remote_id, remote_action);
|
||||
} else if (remote_action.type == ControlType::display_id &&
|
||||
render->screen_capturer_) {
|
||||
render->selected_display_ = remote_action.d;
|
||||
render->screen_capturer_->SwitchTo(remote_action.d);
|
||||
const int ret = render->screen_capturer_->SwitchTo(remote_action.d);
|
||||
if (ret == 0) {
|
||||
render->selected_display_ = remote_action.d;
|
||||
} else {
|
||||
LOG_WARN("Display switch skipped, invalid display_id={}",
|
||||
remote_action.d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1226,6 +1453,8 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
render->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalServerClosed == status) {
|
||||
render->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalTlsCertError == status) {
|
||||
render->signal_connected_ = false;
|
||||
}
|
||||
} else {
|
||||
if (client_id.rfind("C-", 0) != 0) {
|
||||
@@ -1253,6 +1482,8 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalServerClosed == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalTlsCertError == status) {
|
||||
props->signal_connected_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1263,14 +1494,19 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
if (!render) return;
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(render->client_properties_mutex_);
|
||||
auto it = render->client_properties_.find(remote_id);
|
||||
auto props = (it != render->client_properties_.end()) ? it->second : nullptr;
|
||||
std::shared_ptr<SubStreamWindowProperties> props;
|
||||
{
|
||||
std::shared_lock lock(render->client_properties_mutex_);
|
||||
auto it = render->client_properties_.find(remote_id);
|
||||
if (it != render->client_properties_.end()) {
|
||||
props = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (props) {
|
||||
render->is_client_mode_ = true;
|
||||
render->show_connection_status_window_ = true;
|
||||
props->connection_status_ = status;
|
||||
props->connection_status_.store(status);
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
@@ -1336,6 +1572,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
case ConnectionStatus::Disconnected:
|
||||
case ConnectionStatus::Failed:
|
||||
case ConnectionStatus::Closed: {
|
||||
render->ReleaseRemotePressedKeys(remote_id, "connection_closed");
|
||||
props->connection_established_ = false;
|
||||
props->enable_mouse_control_ = false;
|
||||
render->ResetRemoteServiceStatus(*props);
|
||||
@@ -1385,7 +1622,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
} else {
|
||||
render->is_client_mode_ = false;
|
||||
render->show_connection_status_window_ = true;
|
||||
render->connection_status_[remote_id] = status;
|
||||
{
|
||||
std::unique_lock lock(render->connection_status_mutex_);
|
||||
render->connection_status_[remote_id] = status;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
@@ -1441,11 +1681,14 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
render->start_speaker_capturer_ = true;
|
||||
render->remote_client_id_ = remote_id;
|
||||
render->start_mouse_controller_ = true;
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
return kv.first.find("web") != std::string::npos;
|
||||
})) {
|
||||
render->show_cursor_ = true;
|
||||
{
|
||||
std::shared_lock lock(render->connection_status_mutex_);
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
return kv.first.find("web") != std::string::npos;
|
||||
})) {
|
||||
render->show_cursor_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -1453,12 +1696,19 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
case ConnectionStatus::Disconnected:
|
||||
case ConnectionStatus::Failed:
|
||||
case ConnectionStatus::Closed: {
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
return kv.second == ConnectionStatus::Closed ||
|
||||
kv.second == ConnectionStatus::Failed ||
|
||||
kv.second == ConnectionStatus::Disconnected;
|
||||
})) {
|
||||
render->ReleaseRemotePressedKeys(remote_id, "connection_closed");
|
||||
bool all_disconnected = false;
|
||||
{
|
||||
std::shared_lock lock(render->connection_status_mutex_);
|
||||
all_disconnected = std::all_of(
|
||||
render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
return kv.second == ConnectionStatus::Closed ||
|
||||
kv.second == ConnectionStatus::Failed ||
|
||||
kv.second == ConnectionStatus::Disconnected;
|
||||
});
|
||||
}
|
||||
if (all_disconnected) {
|
||||
render->need_to_destroy_server_window_ = true;
|
||||
render->is_server_mode_ = false;
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
@@ -1485,18 +1735,24 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
render->audio_capture_ = false;
|
||||
}
|
||||
|
||||
render->connection_status_.erase(remote_id);
|
||||
render->connection_host_names_.erase(remote_id);
|
||||
{
|
||||
std::unique_lock lock(render->connection_status_mutex_);
|
||||
render->connection_status_.erase(remote_id);
|
||||
render->connection_host_names_.erase(remote_id);
|
||||
}
|
||||
if (render->screen_capturer_) {
|
||||
render->screen_capturer_->ResetToInitialMonitor();
|
||||
}
|
||||
}
|
||||
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
return kv.first.find("web") == std::string::npos;
|
||||
})) {
|
||||
render->show_cursor_ = false;
|
||||
{
|
||||
std::shared_lock lock(render->connection_status_mutex_);
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
return kv.first.find("web") == std::string::npos;
|
||||
})) {
|
||||
render->show_cursor_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -191,7 +191,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
RemoteAction remote_action;
|
||||
remote_action.type = ControlType::display_id;
|
||||
remote_action.d = i;
|
||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->control_data_label_.c_str());
|
||||
@@ -215,7 +215,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
auto send_service_command = [&](ServiceCommandFlag flag,
|
||||
const char* log_action) {
|
||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected &&
|
||||
props->peer_) {
|
||||
RemoteAction remote_action;
|
||||
remote_action.type = ControlType::service_command;
|
||||
|
||||
@@ -26,20 +26,31 @@ int Render::StatusBar() {
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
|
||||
ImColor(1.0f, 1.0f, 1.0f), 100);
|
||||
bool tls_cert_error =
|
||||
signal_status_ == SignalStatus::SignalTlsCertError;
|
||||
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f,
|
||||
ImColor(signal_connected_ ? 0.0f : 1.0f,
|
||||
signal_connected_ ? 1.0f : 0.0f, 0.0f),
|
||||
tls_cert_error
|
||||
? ImColor(1.0f, 0.65f, 0.0f)
|
||||
: ImColor(signal_connected_ ? 0.0f : 1.0f,
|
||||
signal_connected_ ? 1.0f : 0.0f,
|
||||
0.0f),
|
||||
100);
|
||||
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
const char* signal_status_text =
|
||||
tls_cert_error
|
||||
? localization::signal_tls_cert_error[localization_language_index_]
|
||||
.c_str()
|
||||
: (signal_connected_
|
||||
? localization::signal_connected[localization_language_index_]
|
||||
.c_str()
|
||||
: localization::signal_disconnected
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
draw_list->AddText(
|
||||
ImVec2(status_bar_width * 0.045f,
|
||||
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)),
|
||||
ImColor(0.0f, 0.0f, 0.0f),
|
||||
signal_connected_
|
||||
? localization::signal_connected[localization_language_index_].c_str()
|
||||
: localization::signal_disconnected[localization_language_index_]
|
||||
.c_str());
|
||||
ImColor(0.0f, 0.0f, 0.0f), signal_status_text);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -300,17 +300,12 @@ int Render::TitleBar(bool main_window) {
|
||||
}
|
||||
|
||||
if (close_button_clicked) {
|
||||
#if _WIN32
|
||||
if (enable_minimize_to_tray_) {
|
||||
tray_->MinimizeToTray();
|
||||
} else {
|
||||
#endif
|
||||
const bool minimized_to_tray = main_window && MinimizeMainWindowToTray();
|
||||
if (!minimized_to_tray) {
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
#if _WIN32
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-06-23
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _LINUX_TRAY_H_
|
||||
#define _LINUX_TRAY_H_
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct LinuxTrayImpl;
|
||||
|
||||
class LinuxTray {
|
||||
public:
|
||||
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
int language_index, uint32_t exit_event_type);
|
||||
~LinuxTray();
|
||||
|
||||
bool MinimizeToTray();
|
||||
void RemoveTrayIcon();
|
||||
void ProcessEvents();
|
||||
|
||||
private:
|
||||
std::unique_ptr<LinuxTrayImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
#endif // _LINUX_TRAY_H_
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-06-23
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MAC_TRAY_H_
|
||||
#define _MAC_TRAY_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct MacTrayImpl;
|
||||
|
||||
class MacTray {
|
||||
public:
|
||||
MacTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
int language_index);
|
||||
~MacTray();
|
||||
|
||||
void MinimizeToTray();
|
||||
void RemoveTrayIcon();
|
||||
|
||||
private:
|
||||
std::unique_ptr<MacTrayImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _MAC_TRAY_H_
|
||||
@@ -0,0 +1,249 @@
|
||||
#include "mac_tray.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#include "localization.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
@interface CrossDeskMacTrayTarget : NSObject
|
||||
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
|
||||
- (void)statusItemClicked:(id)sender;
|
||||
- (void)exitApplication:(id)sender;
|
||||
@end
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct MacTrayImpl {
|
||||
explicit MacTrayImpl(::SDL_Window *window, std::string tray_tooltip,
|
||||
int language_index_value)
|
||||
: app_window(window),
|
||||
tooltip(std::move(tray_tooltip)),
|
||||
language_index(language_index_value),
|
||||
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
|
||||
|
||||
~MacTrayImpl() {
|
||||
RemoveTrayIcon();
|
||||
target = nil;
|
||||
}
|
||||
|
||||
void MinimizeToTray() {
|
||||
EnsureStatusItem();
|
||||
if (app_window) {
|
||||
SDL_HideWindow(app_window);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveTrayIcon() {
|
||||
if (!status_item) {
|
||||
return;
|
||||
}
|
||||
|
||||
[[NSStatusBar systemStatusBar] removeStatusItem:status_item];
|
||||
status_item = nil;
|
||||
}
|
||||
|
||||
void ShowWindow() {
|
||||
if (!app_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_ShowWindow(app_window);
|
||||
SDL_RaiseWindow(app_window);
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
|
||||
void ShowMenu() {
|
||||
EnsureStatusItem();
|
||||
if (!status_item) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
|
||||
NSString *exit_title =
|
||||
NSStringFromUtf8(localization::exit_program
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_index)]);
|
||||
NSMenuItem *exit_item = [[NSMenuItem alloc] initWithTitle:exit_title
|
||||
action:@selector(exitApplication:)
|
||||
keyEquivalent:@""];
|
||||
[exit_item setTarget:target];
|
||||
[menu addItem:exit_item];
|
||||
|
||||
NSStatusBarButton *button = [status_item button];
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const NSRect bounds = [button bounds];
|
||||
[menu popUpMenuPositioningItem:nil
|
||||
atLocation:NSMakePoint(NSMinX(bounds), NSMinY(bounds))
|
||||
inView:button];
|
||||
}
|
||||
|
||||
void RequestExit() {
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
private:
|
||||
void EnsureStatusItem() {
|
||||
if (status_item) {
|
||||
return;
|
||||
}
|
||||
|
||||
status_item = [[NSStatusBar systemStatusBar]
|
||||
statusItemWithLength:NSSquareStatusItemLength];
|
||||
NSStatusBarButton *button = [status_item button];
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
[button setToolTip:NSStringFromUtf8(tooltip)];
|
||||
|
||||
NSImage *crossdesk_icon = LoadCrossDeskIcon();
|
||||
if (crossdesk_icon) {
|
||||
NSImage *status_icon = [crossdesk_icon copy];
|
||||
[status_icon setSize:NSMakeSize(18.0, 18.0)];
|
||||
[status_icon setTemplate:NO];
|
||||
[button setImage:status_icon];
|
||||
[button setImagePosition:NSImageOnly];
|
||||
} else {
|
||||
[button setTitle:@"CD"];
|
||||
}
|
||||
|
||||
[button setTarget:target];
|
||||
[button setAction:@selector(statusItemClicked:)];
|
||||
[button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp];
|
||||
}
|
||||
|
||||
NSString *NSStringFromUtf8(const std::string &text) {
|
||||
return [NSString stringWithUTF8String:text.c_str()];
|
||||
}
|
||||
|
||||
NSImage *LoadCrossDeskIcon() {
|
||||
NSImage *icon = LoadIconFromBundleResource(@"crossdesk");
|
||||
if (!icon) {
|
||||
icon = LoadIconFromBundleResource(@"crossedesk");
|
||||
}
|
||||
if (!icon) {
|
||||
icon = LoadIconFromDevelopmentPath();
|
||||
}
|
||||
if (!icon) {
|
||||
icon = [NSApp applicationIconImage];
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
NSImage *LoadIconFromBundleResource(NSString *resource_name) {
|
||||
NSString *icon_path =
|
||||
[[NSBundle mainBundle] pathForResource:resource_name ofType:@"icns"];
|
||||
return LoadIconFromPath(icon_path);
|
||||
}
|
||||
|
||||
NSImage *LoadIconFromDevelopmentPath() {
|
||||
NSMutableArray<NSString *> *candidate_paths = [NSMutableArray array];
|
||||
|
||||
NSString *current_directory =
|
||||
[[NSFileManager defaultManager] currentDirectoryPath];
|
||||
[candidate_paths
|
||||
addObject:[current_directory
|
||||
stringByAppendingPathComponent:
|
||||
@"icons/macos/crossdesk.icns"]];
|
||||
|
||||
const char *base_path = SDL_GetBasePath();
|
||||
if (base_path && base_path[0] != '\0') {
|
||||
NSString *base_directory = NSStringFromUtf8(base_path);
|
||||
[candidate_paths
|
||||
addObject:[base_directory
|
||||
stringByAppendingPathComponent:
|
||||
@"icons/macos/crossdesk.icns"]];
|
||||
[candidate_paths
|
||||
addObject:[base_directory
|
||||
stringByAppendingPathComponent:
|
||||
@"../../../../icons/macos/crossdesk.icns"]];
|
||||
}
|
||||
|
||||
for (NSString *candidate_path in candidate_paths) {
|
||||
NSImage *icon = LoadIconFromPath(
|
||||
[candidate_path stringByStandardizingPath]);
|
||||
if (icon) {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSImage *LoadIconFromPath(NSString *icon_path) {
|
||||
if (![icon_path length]) {
|
||||
return nil;
|
||||
}
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:icon_path]) {
|
||||
return nil;
|
||||
}
|
||||
return [[NSImage alloc] initWithContentsOfFile:icon_path];
|
||||
}
|
||||
|
||||
::SDL_Window *app_window = nullptr;
|
||||
std::string tooltip;
|
||||
int language_index = 0;
|
||||
NSStatusItem *status_item = nil;
|
||||
CrossDeskMacTrayTarget *target = nil;
|
||||
};
|
||||
|
||||
MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
|
||||
int language_index)
|
||||
: impl_(
|
||||
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
|
||||
|
||||
MacTray::~MacTray() = default;
|
||||
|
||||
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
|
||||
|
||||
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@implementation CrossDeskMacTrayTarget {
|
||||
crossdesk::MacTrayImpl *owner_;
|
||||
}
|
||||
|
||||
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
owner_ = owner;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)statusItemClicked:(id)sender {
|
||||
(void)sender;
|
||||
if (!owner_) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSEvent *event = [NSApp currentEvent];
|
||||
if (event && [event type] == NSEventTypeRightMouseUp) {
|
||||
owner_->ShowMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
owner_->ShowWindow();
|
||||
}
|
||||
|
||||
- (void)exitApplication:(id)sender {
|
||||
(void)sender;
|
||||
if (owner_) {
|
||||
owner_->RequestExit();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -31,8 +31,9 @@ bool Render::ConnectionStatusWindow(
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
std::string text;
|
||||
const ConnectionStatus status = props->connection_status_.load();
|
||||
|
||||
if (ConnectionStatus::Connecting == props->connection_status_) {
|
||||
if (ConnectionStatus::Connecting == status) {
|
||||
text = localization::p2p_connecting[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -48,7 +49,23 @@ bool Render::ConnectionStatusWindow(
|
||||
}
|
||||
ret_flag = true;
|
||||
}
|
||||
} else if (ConnectionStatus::Connected == props->connection_status_) {
|
||||
} else if (ConnectionStatus::Gathering == status) {
|
||||
text = localization::p2p_gathering[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
// cancel
|
||||
if (ImGui::Button(
|
||||
localization::cancel[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
re_enter_remote_id_ = true;
|
||||
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
|
||||
if (props->peer_) {
|
||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||
}
|
||||
ret_flag = true;
|
||||
}
|
||||
} else if (ConnectionStatus::Connected == status) {
|
||||
text = localization::p2p_connected[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -58,7 +75,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
}
|
||||
} else if (ConnectionStatus::Disconnected == props->connection_status_) {
|
||||
} else if (ConnectionStatus::Disconnected == status) {
|
||||
text = localization::p2p_disconnected[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -68,7 +85,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
}
|
||||
} else if (ConnectionStatus::Failed == props->connection_status_) {
|
||||
} else if (ConnectionStatus::Failed == status) {
|
||||
text = localization::p2p_failed[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -78,7 +95,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
}
|
||||
} else if (ConnectionStatus::Closed == props->connection_status_) {
|
||||
} else if (ConnectionStatus::Closed == status) {
|
||||
text = localization::p2p_closed[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -88,7 +105,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
}
|
||||
} else if (ConnectionStatus::IncorrectPassword == props->connection_status_) {
|
||||
} else if (ConnectionStatus::IncorrectPassword == status) {
|
||||
if (!password_validating_) {
|
||||
if (password_validating_time_ == 1) {
|
||||
text = localization::input_password[localization_language_index_];
|
||||
@@ -151,8 +168,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
}
|
||||
} else if (ConnectionStatus::NoSuchTransmissionId ==
|
||||
props->connection_status_) {
|
||||
} else if (ConnectionStatus::NoSuchTransmissionId == status) {
|
||||
text = localization::no_such_id[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
#include "render.h"
|
||||
#include "tinyfiledialogs.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::SettingWindow() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
float portable_y_padding = 0.0f;
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
portable_y_padding = 0.05f;
|
||||
#endif
|
||||
|
||||
if (show_settings_window_) {
|
||||
if (settings_window_pos_reset_) {
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
@@ -18,12 +27,14 @@ int Render::SettingWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f));
|
||||
ImVec2(io.DisplaySize.x * 0.315f,
|
||||
io.DisplaySize.y * (0.9f + portable_y_padding)));
|
||||
#else
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f));
|
||||
ImVec2(io.DisplaySize.x * 0.315f,
|
||||
io.DisplaySize.y * (0.85f + portable_y_padding)));
|
||||
#endif
|
||||
} else {
|
||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
||||
@@ -32,12 +43,14 @@ int Render::SettingWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f));
|
||||
ImVec2(io.DisplaySize.x * 0.42f,
|
||||
io.DisplaySize.y * (0.9f + portable_y_padding)));
|
||||
#else
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f));
|
||||
ImVec2(io.DisplaySize.x * 0.42f,
|
||||
io.DisplaySize.y * (0.85f + portable_y_padding)));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -73,23 +86,21 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
if (ImGui::BeginCombo(
|
||||
"##language",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_button_value_)]
|
||||
.display_name
|
||||
.c_str())) {
|
||||
if (ImGui::BeginCombo("##language",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_button_value_)]
|
||||
.display_name.c_str())) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
for (int i = 0; i < static_cast<int>(supported_languages.size());
|
||||
++i) {
|
||||
bool selected = (i == language_button_value_);
|
||||
if (ImGui::Selectable(
|
||||
supported_languages[i].display_name.c_str(), selected))
|
||||
if (ImGui::Selectable(supported_languages[i].display_name.c_str(),
|
||||
selected))
|
||||
language_button_value_ = i;
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
@@ -125,7 +136,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -158,7 +169,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -194,7 +205,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -228,7 +239,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_hardware_video_codec",
|
||||
@@ -249,7 +260,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_turn", &enable_turn_);
|
||||
@@ -268,7 +279,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_srtp", &enable_srtp_);
|
||||
@@ -289,7 +300,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
|
||||
@@ -308,7 +319,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
|
||||
@@ -327,7 +338,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
|
||||
@@ -345,10 +356,6 @@ int Render::SettingWindow() {
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
#ifndef _WIN32
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
|
||||
#endif
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
@@ -359,15 +366,11 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_minimize_to_tray_",
|
||||
&enable_minimize_to_tray_);
|
||||
#ifndef _WIN32
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::EndDisabled();
|
||||
#endif
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
@@ -384,7 +387,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
}
|
||||
|
||||
std::string display_path =
|
||||
@@ -429,6 +432,80 @@ int Render::SettingWindow() {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s", localization::windows_service_settings_label
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.0f);
|
||||
} else if (ConfigCenter::LANGUAGE::ENGLISH == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.42f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
|
||||
}
|
||||
|
||||
const PortableServiceInstallState state =
|
||||
portable_service_install_state_.load(std::memory_order_acquire);
|
||||
const bool service_installed =
|
||||
IsCrossDeskServiceInstalled() ||
|
||||
state == PortableServiceInstallState::succeeded;
|
||||
if (service_installed) {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.9f);
|
||||
} else if (ConfigCenter::LANGUAGE::ENGLISH ==
|
||||
localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.32f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
|
||||
}
|
||||
ImGui::Text("%s", localization::windows_service_installed
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
} else {
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(localization::install_windows_service
|
||||
[localization_language_index_]
|
||||
.c_str())) {
|
||||
StartPortableWindowsServiceInstall();
|
||||
}
|
||||
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s", localization::installing_windows_service
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
} else if (state == PortableServiceInstallState::failed) {
|
||||
ImGui::SameLine();
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::failed[localization_language_index_].c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::PushTextWrapPos(title_bar_button_width_ * 10.0f);
|
||||
ImGui::TextWrapped("%s",
|
||||
localization::windows_service_install_failed
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f);
|
||||
} else {
|
||||
@@ -436,7 +513,7 @@ int Render::SettingWindow() {
|
||||
}
|
||||
|
||||
settings_items_offset +=
|
||||
settings_items_padding + title_bar_button_width_ * 0.3f;
|
||||
settings_items_padding + title_bar_button_width_ * 0.15f;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
@@ -463,9 +540,8 @@ int Render::SettingWindow() {
|
||||
LOG_INFO("Set localization language: {}",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
localization_language_index_)]
|
||||
.code
|
||||
.c_str());
|
||||
localization_language_index_)]
|
||||
.code.c_str());
|
||||
|
||||
// Video quality
|
||||
if (video_quality_button_value_ == 0) {
|
||||
@@ -542,14 +618,12 @@ int Render::SettingWindow() {
|
||||
}
|
||||
enable_daemon_last_ = enable_daemon_;
|
||||
|
||||
#if _WIN32
|
||||
if (enable_minimize_to_tray_) {
|
||||
config_center_->SetMinimizeToTray(true);
|
||||
} else {
|
||||
config_center_->SetMinimizeToTray(false);
|
||||
}
|
||||
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
|
||||
#endif
|
||||
|
||||
// File transfer save path
|
||||
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "localization.h"
|
||||
@@ -16,9 +17,8 @@ namespace {
|
||||
std::filesystem::path GetCurrentExecutablePath() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
DWORD length =
|
||||
GetModuleFileNameW(nullptr, buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
DWORD length = GetModuleFileNameW(nullptr, buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
@@ -46,7 +46,8 @@ bool InstallServiceWithElevation() {
|
||||
if (!std::filesystem::exists(service_path) ||
|
||||
!std::filesystem::exists(helper_path)) {
|
||||
LOG_ERROR(
|
||||
"Portable service install failed: service binaries missing, service={}, "
|
||||
"Portable service install failed: service binaries missing, "
|
||||
"service={}, "
|
||||
"helper={}",
|
||||
service_path.string(), helper_path.string());
|
||||
return false;
|
||||
@@ -106,12 +107,17 @@ void Render::CheckPortableWindowsService() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (portable_service_prompt_suppressed_) {
|
||||
return;
|
||||
}
|
||||
|
||||
portable_service_install_state_.store(PortableServiceInstallState::idle,
|
||||
std::memory_order_relaxed);
|
||||
show_portable_service_install_window_ = true;
|
||||
}
|
||||
|
||||
void Render::StartPortableWindowsServiceInstall() {
|
||||
portable_service_do_not_remind_ = false;
|
||||
PortableServiceInstallState expected = PortableServiceInstallState::idle;
|
||||
if (!portable_service_install_state_.compare_exchange_strong(
|
||||
expected, PortableServiceInstallState::installing,
|
||||
@@ -140,18 +146,82 @@ void Render::JoinPortableWindowsServiceInstallThread() {
|
||||
}
|
||||
|
||||
int Render::PortableServiceInstallWindow() {
|
||||
if (!show_portable_service_install_window_) {
|
||||
if (!show_portable_service_install_window_ &&
|
||||
!show_portable_service_prompt_suppressed_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
const float window_width = title_bar_button_width_ * 12.0f;
|
||||
const float window_height = title_bar_button_width_ * 4.0f;
|
||||
const float window_width =
|
||||
(std::min)(viewport->WorkSize.x * 0.6f, title_bar_button_width_ * 18.0f);
|
||||
const float window_height =
|
||||
(std::min)(viewport->WorkSize.y * 0.5f, title_bar_button_width_ * 8.0f);
|
||||
|
||||
if (show_portable_service_prompt_suppressed_window_) {
|
||||
const float notice_width = window_width;
|
||||
const float notice_height = (std::min)(viewport->WorkSize.y * 0.35f,
|
||||
title_bar_button_width_ * 4.6f);
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(
|
||||
viewport->WorkPos.x + (viewport->WorkSize.x - notice_width) / 2.0f,
|
||||
viewport->WorkPos.y +
|
||||
(viewport->WorkSize.y - notice_height) / 2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(notice_width, notice_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
|
||||
ImGui::Begin(
|
||||
localization::notification[localization_language_index_].c_str(),
|
||||
nullptr,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoTitleBar);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::SetWindowFontScale(0.55f);
|
||||
ImGui::SetCursorPosX(notice_width * 0.08f);
|
||||
ImGui::Text(
|
||||
"%s", localization::notification[localization_language_index_].c_str());
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::SetCursorPosX(notice_width * 0.06f);
|
||||
ImGui::SetCursorPosY(notice_height * 0.28f);
|
||||
ImGui::PushTextWrapPos(notice_width * 0.88f);
|
||||
ImGui::TextWrapped("%s",
|
||||
localization::windows_service_prompt_suppressed_message
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
const std::string ok_label = localization::ok[localization_language_index_];
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float ok_width =
|
||||
ImGui::CalcTextSize(ok_label.c_str()).x + style.FramePadding.x * 2.0f;
|
||||
ImGui::SetCursorPosX((notice_width - ok_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(notice_height * 0.75f);
|
||||
if (ImGui::Button(ok_label.c_str())) {
|
||||
show_portable_service_prompt_suppressed_window_ = false;
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if (!show_portable_service_install_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) /
|
||||
2.0f,
|
||||
(viewport->WorkSize.y - viewport->WorkPos.y - window_height) /
|
||||
2.0f),
|
||||
ImVec2(
|
||||
viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f,
|
||||
viewport->WorkPos.y + (viewport->WorkSize.y - window_height) / 2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||
ImGuiCond_Always);
|
||||
@@ -187,15 +257,13 @@ int Render::PortableServiceInstallWindow() {
|
||||
localization::installing_windows_service[localization_language_index_]
|
||||
.c_str();
|
||||
if (state == PortableServiceInstallState::succeeded) {
|
||||
status_text =
|
||||
localization::windows_service_install_success
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
status_text = localization::windows_service_install_success
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
} else if (state == PortableServiceInstallState::failed) {
|
||||
status_text =
|
||||
localization::windows_service_install_failed
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
status_text = localization::windows_service_install_failed
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +271,7 @@ int Render::PortableServiceInstallWindow() {
|
||||
ImGui::SetCursorPosX(window_width * 0.04f);
|
||||
ImGui::SetCursorPosY(window_height * 0.22f);
|
||||
ImGui::BeginChild("PortableServiceInstallContent",
|
||||
ImVec2(window_width * 0.92f, window_height * 0.5f),
|
||||
ImVec2(window_width * 0.92f, window_height * 0.45f),
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const float wrap_pos = ImGui::GetContentRegionAvail().x;
|
||||
@@ -220,7 +288,15 @@ int Render::PortableServiceInstallWindow() {
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const float button_y = window_height * 0.76f;
|
||||
ImGui::SetCursorPosX(window_width * 0.08f);
|
||||
ImGui::SetCursorPosY(window_height * 0.71f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
|
||||
ImGui::Checkbox(
|
||||
localization::do_not_remind_again[localization_language_index_].c_str(),
|
||||
&portable_service_do_not_remind_);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
const float button_y = window_height * 0.84f;
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const auto default_button_width = [&style](const std::string& label) {
|
||||
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
|
||||
@@ -259,6 +335,11 @@ int Render::PortableServiceInstallWindow() {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(cancel_label.c_str())) {
|
||||
if (portable_service_do_not_remind_) {
|
||||
portable_service_prompt_suppressed_ = true;
|
||||
config_center_->SetPortableServicePromptSuppressed(true);
|
||||
show_portable_service_prompt_suppressed_window_ = true;
|
||||
}
|
||||
show_portable_service_install_window_ = false;
|
||||
}
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -149,14 +151,17 @@ int Render::RemoteClientInfoWindow() {
|
||||
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> remote_entries;
|
||||
remote_entries.reserve(connection_status_.size());
|
||||
for (const auto& kv : connection_status_) {
|
||||
const auto host_it = connection_host_names_.find(kv.first);
|
||||
const std::string display_name =
|
||||
(host_it != connection_host_names_.end() && !host_it->second.empty())
|
||||
? host_it->second
|
||||
: kv.first;
|
||||
remote_entries.emplace_back(kv.first, display_name);
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
remote_entries.reserve(connection_status_.size());
|
||||
for (const auto& kv : connection_status_) {
|
||||
const auto host_it = connection_host_names_.find(kv.first);
|
||||
const std::string display_name =
|
||||
(host_it != connection_host_names_.end() && !host_it->second.empty())
|
||||
? host_it->second
|
||||
: kv.first;
|
||||
remote_entries.emplace_back(kv.first, display_name);
|
||||
}
|
||||
}
|
||||
|
||||
auto find_display_name_by_remote_id =
|
||||
@@ -220,10 +225,14 @@ int Render::RemoteClientInfoWindow() {
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
|
||||
if (!selected_server_remote_id_.empty()) {
|
||||
auto it = connection_status_.find(selected_server_remote_id_);
|
||||
const ConnectionStatus status = (it == connection_status_.end())
|
||||
? ConnectionStatus::Closed
|
||||
: it->second;
|
||||
ConnectionStatus status = ConnectionStatus::Closed;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
auto it = connection_status_.find(selected_server_remote_id_);
|
||||
if (it != connection_status_.end()) {
|
||||
status = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
@@ -376,4 +385,4 @@ int Render::RemoteClientInfoWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace crossdesk {
|
||||
void Render::DrawConnectionStatusText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
std::string text;
|
||||
switch (props->connection_status_) {
|
||||
switch (props->connection_status_.load()) {
|
||||
case ConnectionStatus::Disconnected:
|
||||
text = localization::p2p_disconnected[localization_language_index_];
|
||||
break;
|
||||
@@ -34,7 +34,7 @@ void Render::DrawConnectionStatusText(
|
||||
void Render::DrawReceivingScreenText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
if (!props->connection_established_ ||
|
||||
props->connection_status_ != ConnectionStatus::Connected) {
|
||||
props->connection_status_.load() != ConnectionStatus::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ int ScreenCapturerSck::Init(const int fps, cb_desktop_data cb) {
|
||||
}
|
||||
|
||||
screen_capturer_sck_impl_ = CreateScreenCapturerSck();
|
||||
screen_capturer_sck_impl_->Init(fps, on_data_);
|
||||
const int ret = screen_capturer_sck_impl_->Init(fps, on_data_);
|
||||
if (ret != 0) {
|
||||
screen_capturer_sck_impl_.reset();
|
||||
return ret;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -29,8 +33,11 @@ int ScreenCapturerSck::Destroy() {
|
||||
}
|
||||
|
||||
int ScreenCapturerSck::Start(bool show_cursor) {
|
||||
screen_capturer_sck_impl_->Start(show_cursor);
|
||||
return 0;
|
||||
if (!screen_capturer_sck_impl_) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return screen_capturer_sck_impl_->Start(show_cursor);
|
||||
}
|
||||
|
||||
int ScreenCapturerSck::Stop() {
|
||||
@@ -80,4 +87,4 @@ std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() {
|
||||
void ScreenCapturerSck::OnFrame() {}
|
||||
|
||||
void ScreenCapturerSck::CleanUp() {}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
#include <IOKit/graphics/IOGraphicsLib.h>
|
||||
#include <IOSurface/IOSurface.h>
|
||||
#include <ScreenCaptureKit/ScreenCaptureKit.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "display_info.h"
|
||||
@@ -28,6 +33,15 @@ class ScreenCapturerSckImpl;
|
||||
|
||||
static const int kFullDesktopScreenId = -1;
|
||||
|
||||
static std::string NSErrorToString(NSError *error) {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const char *description = [error.localizedDescription UTF8String];
|
||||
return description ? description : "";
|
||||
}
|
||||
|
||||
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
|
||||
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
|
||||
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
|
||||
@@ -78,6 +92,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
std::map<CGDirectDisplayID, int> display_id_map_reverse_;
|
||||
std::map<CGDirectDisplayID, std::string> display_id_name_map_;
|
||||
unsigned char *nv12_frame_ = nullptr;
|
||||
size_t nv12_frame_size_ = 0;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
int fps_ = 60;
|
||||
@@ -100,7 +115,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
// Helper object to receive Objective-C callbacks from ScreenCaptureKit and call into this C++
|
||||
// object. The helper may outlive this C++ instance, if a completion-handler is passed to
|
||||
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
|
||||
SckHelper *__strong helper_;
|
||||
SckHelper *__strong helper_ = nil;
|
||||
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's
|
||||
// thread.
|
||||
cb_desktop_data _on_data = nullptr;
|
||||
@@ -110,7 +125,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
// Guards some variables that may be accessed on different threads.
|
||||
std::mutex lock_;
|
||||
// Provides captured desktop frames.
|
||||
SCStream *__strong stream_;
|
||||
SCStream *__strong stream_ = nil;
|
||||
// Currently selected display, or 0 if the full desktop is selected. This capturer does not
|
||||
// support full-desktop capture, and will fall back to the first display.
|
||||
CGDirectDisplayID current_display_ = 0;
|
||||
@@ -182,6 +197,19 @@ ScreenCapturerSckImpl::ScreenCapturerSckImpl() {
|
||||
}
|
||||
|
||||
ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
||||
SckHelper *helper_to_release = nil;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (stream_) {
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
stream_ = nil;
|
||||
}
|
||||
_on_data = nullptr;
|
||||
helper_to_release = helper_;
|
||||
helper_ = nil;
|
||||
}
|
||||
[helper_to_release releaseCapturer];
|
||||
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
display_id_map_reverse_.clear();
|
||||
@@ -190,15 +218,22 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
||||
if (nv12_frame_) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = nullptr;
|
||||
nv12_frame_size_ = 0;
|
||||
}
|
||||
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
[helper_ releaseCapturer];
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
if (!cb) {
|
||||
LOG_ERROR("Screen capturer callback is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
_on_data = cb;
|
||||
fps_ = fps;
|
||||
fps_ = fps > 0 ? fps : 60;
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
display_id_map_reverse_.clear();
|
||||
display_id_name_map_.clear();
|
||||
|
||||
if (@available(macOS 10.15, *)) {
|
||||
bool has_permission = CGPreflightScreenCaptureAccess();
|
||||
@@ -216,8 +251,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) {
|
||||
if (error) {
|
||||
capture_error = error;
|
||||
LOG_ERROR("Failed to get shareable content: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
LOG_ERROR("Failed to get shareable content: {}", NSErrorToString(error));
|
||||
} else {
|
||||
content = result;
|
||||
}
|
||||
@@ -227,7 +261,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
|
||||
if (capture_error || !content || content.displays.count == 0) {
|
||||
LOG_ERROR("Failed to get display info, error: {}",
|
||||
std::string([capture_error.localizedDescription UTF8String]));
|
||||
NSErrorToString(capture_error));
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -284,51 +318,58 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
|
||||
if (stream_) {
|
||||
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
stream_ = nil;
|
||||
current_display_ = display_id_map_[monitor_index];
|
||||
StartOrReconfigureCapturer();
|
||||
}];
|
||||
} else {
|
||||
current_display_ = display_id_map_[monitor_index];
|
||||
StartOrReconfigureCapturer();
|
||||
auto display_it = display_id_map_.find(monitor_index);
|
||||
if (display_it == display_id_map_.end()) {
|
||||
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
|
||||
monitor_index, display_id_map_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
const CGDirectDisplayID target_display = display_it->second;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
current_display_ = target_display;
|
||||
}
|
||||
StartOrReconfigureCapturer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
||||
int target = initial_monitor_index_;
|
||||
if (display_info_list_.empty()) return -1;
|
||||
CGDirectDisplayID target_display = display_id_map_[target];
|
||||
if (current_display_ == target_display) return 0;
|
||||
if (stream_) {
|
||||
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
stream_ = nil;
|
||||
current_display_ = target_display;
|
||||
StartOrReconfigureCapturer();
|
||||
}];
|
||||
} else {
|
||||
current_display_ = target_display;
|
||||
StartOrReconfigureCapturer();
|
||||
auto display_it = display_id_map_.find(target);
|
||||
if (display_it == display_id_map_.end()) {
|
||||
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
|
||||
return -1;
|
||||
}
|
||||
|
||||
CGDirectDisplayID target_display = display_it->second;
|
||||
if (current_display_ == target_display) return 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
current_display_ = target_display;
|
||||
}
|
||||
StartOrReconfigureCapturer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Destroy() {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (stream_) {
|
||||
LOG_INFO("Destroying stream");
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
stream_ = nil;
|
||||
SckHelper *helper_to_release = nil;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (stream_) {
|
||||
LOG_INFO("Destroying stream");
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
stream_ = nil;
|
||||
}
|
||||
current_display_ = 0;
|
||||
permanent_error_ = false;
|
||||
_on_data = nullptr;
|
||||
helper_to_release = helper_;
|
||||
helper_ = nil;
|
||||
}
|
||||
current_display_ = 0;
|
||||
permanent_error_ = false;
|
||||
_on_data = nullptr;
|
||||
[helper_ releaseCapturer];
|
||||
helper_ = nil;
|
||||
|
||||
[helper_to_release releaseCapturer];
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -416,7 +457,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
|
||||
// TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for
|
||||
// best performance.
|
||||
NSError *add_stream_output_error;
|
||||
NSError *add_stream_output_error = nil;
|
||||
dispatch_queue_t queue = dispatch_queue_create("ScreenCaptureKit.Queue", DISPATCH_QUEUE_SERIAL);
|
||||
bool add_stream_output_result = [stream_ addStreamOutput:helper_
|
||||
type:SCStreamOutputTypeScreen
|
||||
@@ -425,7 +466,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
|
||||
if (!add_stream_output_result) {
|
||||
stream_ = nil;
|
||||
LOG_ERROR("addStreamOutput failed");
|
||||
LOG_ERROR("addStreamOutput failed: {}", NSErrorToString(add_stream_output_error));
|
||||
permanent_error_ = true;
|
||||
return;
|
||||
}
|
||||
@@ -436,7 +477,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
// calls stopCaptureWithCompletionHandler on the stream, which cancels
|
||||
// this handler.
|
||||
permanent_error_ = true;
|
||||
LOG_ERROR("startCaptureWithCompletionHandler failed");
|
||||
LOG_ERROR("startCaptureWithCompletionHandler failed: {}", NSErrorToString(error));
|
||||
} else {
|
||||
LOG_INFO("Capture started");
|
||||
}
|
||||
@@ -448,8 +489,18 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
|
||||
void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
CFDictionaryRef attachment) {
|
||||
(void)attachment;
|
||||
if (!pixelBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
||||
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
||||
if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height,
|
||||
CVPixelBufferGetPlaneCount(pixelBuffer));
|
||||
return;
|
||||
}
|
||||
|
||||
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
if (status != kCVReturnSuccess) {
|
||||
@@ -458,18 +509,37 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
}
|
||||
|
||||
size_t required_size = width * height * 3 / 2;
|
||||
if (!nv12_frame_ || (width_ * height_ * 3 / 2 < required_size)) {
|
||||
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
|
||||
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!_on_data) {
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nv12_frame_ || nv12_frame_size_ < required_size) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = new unsigned char[required_size];
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
nv12_frame_size_ = required_size;
|
||||
}
|
||||
width_ = static_cast<int>(width);
|
||||
height_ = static_cast<int>(height);
|
||||
|
||||
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
||||
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
||||
|
||||
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
||||
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
||||
if (!base_y || !base_uv || stride_y < width || stride_uv < width) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}",
|
||||
base_y != nullptr, base_uv != nullptr, stride_y, stride_uv);
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char *dst_y = nv12_frame_;
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
@@ -481,7 +551,8 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
|
||||
}
|
||||
|
||||
_on_data(nv12_frame_, width * height * 3 / 2, width, height,
|
||||
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
|
||||
static_cast<int>(height),
|
||||
display_id_name_map_[current_display_].c_str());
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
@@ -503,10 +574,14 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
}
|
||||
|
||||
SckHelper *local_helper = helper_;
|
||||
if (!local_helper) {
|
||||
LOG_ERROR("Cannot reconfigure capturer: helper is null");
|
||||
return;
|
||||
}
|
||||
|
||||
auto handler = ^(SCShareableContent *content, NSError *error) {
|
||||
if (error) {
|
||||
LOG_ERROR("getShareableContent failed: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
LOG_ERROR("getShareableContent failed: {}", NSErrorToString(error));
|
||||
[local_helper onShareableContentCreated:nil];
|
||||
return;
|
||||
}
|
||||
@@ -576,4 +651,4 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
|
||||
std::unique_ptr<ScreenCapturer> ScreenCapturerSck::CreateScreenCapturerSck() {
|
||||
return std::make_unique<ScreenCapturerSckImpl>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ int ScreenCapturerDxgi::Resume(int monitor_index) {
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
||||
std::lock_guard<std::mutex> lock(switch_mutex_);
|
||||
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
|
||||
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
|
||||
return -1;
|
||||
@@ -121,6 +122,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
||||
if (!CreateDuplicationForMonitor(monitor_index_)) {
|
||||
LOG_ERROR("DXGI: create duplication failed for monitor {}",
|
||||
monitor_index_.load());
|
||||
paused_ = false; // Reset paused_ on failure
|
||||
return -2;
|
||||
}
|
||||
paused_ = false;
|
||||
@@ -130,6 +132,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::ResetToInitialMonitor() {
|
||||
std::lock_guard<std::mutex> lock(switch_mutex_);
|
||||
if (display_info_list_.empty()) return -1;
|
||||
int target = initial_monitor_index_;
|
||||
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
|
||||
@@ -245,6 +248,7 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
||||
}
|
||||
|
||||
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
||||
std::lock_guard<std::mutex> lock(switch_mutex_);
|
||||
ReleaseDuplication();
|
||||
int current_monitor = monitor_index_.load();
|
||||
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||
@@ -374,8 +378,14 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
even_width, even_width, even_height);
|
||||
|
||||
if (callback_) {
|
||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
display_info_list_[monitor_index_].name.c_str());
|
||||
int idx = monitor_index_.load();
|
||||
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
|
||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
display_info_list_[idx].name.c_str());
|
||||
} else {
|
||||
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
|
||||
idx, display_info_list_.size());
|
||||
}
|
||||
}
|
||||
|
||||
d3d_context_->Unmap(staging_.Get(), 0);
|
||||
|
||||
@@ -72,6 +72,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
std::thread thread_;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_ = nullptr;
|
||||
std::mutex switch_mutex_;
|
||||
|
||||
unsigned char* nv12_frame_ = nullptr;
|
||||
int nv12_width_ = 0;
|
||||
|
||||
@@ -148,7 +148,14 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& di = display_info_list_[monitor_index_];
|
||||
int idx = monitor_index_.load();
|
||||
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_ERROR("GDI: CaptureLoop invalid monitor_index {} (list size {})",
|
||||
idx, display_info_list_.size());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
|
||||
continue;
|
||||
}
|
||||
const auto& di = display_info_list_[idx];
|
||||
int left = di.left;
|
||||
int top = di.top;
|
||||
int width = di.width & ~1;
|
||||
|
||||
@@ -306,7 +306,7 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (monitor_index >= display_info_list_.size()) {
|
||||
if (monitor_index < 0 || monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_ERROR("Invalid monitor index: {}", monitor_index);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,17 @@ namespace crossdesk {
|
||||
class SpeakerCapturerMacosx;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::string NSErrorToString(NSError* error) {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* description = [error.localizedDescription UTF8String];
|
||||
return description ? description : "";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
||||
@property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner;
|
||||
- (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner;
|
||||
@@ -28,15 +39,36 @@ class SpeakerCapturerMacosx;
|
||||
ofType:(SCStreamOutputType)type {
|
||||
if (type != SCStreamOutputTypeAudio) return;
|
||||
|
||||
crossdesk::SpeakerCapturerMacosx* owner = _owner;
|
||||
if (!owner || !owner->cb_) {
|
||||
return;
|
||||
}
|
||||
|
||||
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
|
||||
if (!blockBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t length = CMBlockBufferGetDataLength(blockBuffer);
|
||||
char* dataPtr = NULL;
|
||||
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
|
||||
OSStatus dataStatus =
|
||||
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
|
||||
if (dataStatus != noErr || dataPtr == nullptr || length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CMAudioFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
|
||||
if (!formatDesc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AudioStreamBasicDescription* asbd =
|
||||
CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc);
|
||||
if (!asbd || asbd->mChannelsPerFrame == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_owner->cb_ && dataPtr && length > 0 && asbd) {
|
||||
if (owner->cb_) {
|
||||
std::vector<short> out_pcm16;
|
||||
if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) {
|
||||
int channels = asbd->mChannelsPerFrame;
|
||||
@@ -86,7 +118,10 @@ class SpeakerCapturerMacosx;
|
||||
size_t total_bytes = out_pcm16.size() * sizeof(short);
|
||||
unsigned char* p = (unsigned char*)out_pcm16.data();
|
||||
for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) {
|
||||
_owner->cb_(p + offset, frame_bytes, "audio");
|
||||
if (!owner->cb_) {
|
||||
return;
|
||||
}
|
||||
owner->cb_(p + offset, frame_bytes, "audio");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +190,7 @@ int SpeakerCapturerMacosx::Init(speaker_data_cb cb) {
|
||||
|
||||
if (error || !impl_->content) {
|
||||
LOG_ERROR("Failed to get shareable content: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
NSErrorToString(error));
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -209,7 +244,7 @@ int SpeakerCapturerMacosx::Start() {
|
||||
error:&addOutputError];
|
||||
if (!ok || addOutputError) {
|
||||
LOG_ERROR("addStreamOutput error: {}",
|
||||
std::string([addOutputError.localizedDescription UTF8String]));
|
||||
NSErrorToString(addOutputError));
|
||||
impl_->stream = nil;
|
||||
impl_->delegate = nil;
|
||||
return -1;
|
||||
@@ -220,7 +255,7 @@ int SpeakerCapturerMacosx::Start() {
|
||||
[impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) {
|
||||
if (error) {
|
||||
LOG_ERROR("startCaptureWithCompletionHandler error: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
NSErrorToString(error));
|
||||
ret = -1;
|
||||
}
|
||||
dispatch_semaphore_signal(semaStart);
|
||||
@@ -238,13 +273,14 @@ int SpeakerCapturerMacosx::Stop() {
|
||||
[impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) {
|
||||
if (error) {
|
||||
LOG_ERROR("stopCaptureWithCompletionHandler error: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
NSErrorToString(error));
|
||||
}
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
||||
|
||||
impl_->stream = nil;
|
||||
impl_->delegate.owner = nullptr;
|
||||
impl_->delegate = nil;
|
||||
|
||||
return 0;
|
||||
@@ -269,4 +305,4 @@ int SpeakerCapturerMacosx::Destroy() {
|
||||
int SpeakerCapturerMacosx::Pause() { return 0; }
|
||||
|
||||
int SpeakerCapturerMacosx::Resume() { return Start(); }
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
+17
-10
@@ -225,9 +225,12 @@ int Thumbnail::LoadThumbnail(
|
||||
std::string remote_id;
|
||||
std::string cipher_password;
|
||||
std::string remote_host_name;
|
||||
std::string password;
|
||||
std::string original_image_name;
|
||||
bool remember_password = false;
|
||||
|
||||
if ('Y' == cipher_image_name[9] && cipher_image_name.size() >= 16) {
|
||||
if (cipher_image_name.size() > 9 && 'Y' == cipher_image_name[9] &&
|
||||
cipher_image_name.size() >= 16) {
|
||||
size_t pos_y = cipher_image_name.find('Y');
|
||||
size_t pos_at = cipher_image_name.find('@');
|
||||
|
||||
@@ -241,10 +244,11 @@ int Thumbnail::LoadThumbnail(
|
||||
remote_host_name =
|
||||
cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1);
|
||||
cipher_password = cipher_image_name.substr(pos_at + 1);
|
||||
password = AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
|
||||
remember_password = true;
|
||||
|
||||
original_image_name =
|
||||
remote_id + 'Y' + remote_host_name + "@" +
|
||||
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
|
||||
original_image_name = remote_id + 'Y' + remote_host_name + "@" +
|
||||
password;
|
||||
} else {
|
||||
size_t pos_n = cipher_image_name.find('N');
|
||||
// size_t pos_at = cipher_image_name.find('@');
|
||||
@@ -257,16 +261,19 @@ int Thumbnail::LoadThumbnail(
|
||||
remote_id = cipher_image_name.substr(0, pos_n);
|
||||
remote_host_name = cipher_image_name.substr(pos_n + 1);
|
||||
|
||||
original_image_name =
|
||||
remote_id + 'N' + remote_host_name + "@" +
|
||||
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
|
||||
original_image_name = remote_id + 'N' + remote_host_name;
|
||||
}
|
||||
|
||||
std::string image_path = save_path_ + cipher_image_name;
|
||||
Thumbnail::RecentConnection recent_connection;
|
||||
recent_connection.remote_id = remote_id;
|
||||
recent_connection.remote_host_name = remote_host_name;
|
||||
recent_connection.password = password;
|
||||
recent_connection.remember_password = remember_password;
|
||||
recent_connections.emplace_back(
|
||||
std::make_pair(original_image_name, Thumbnail::RecentConnection()));
|
||||
std::make_pair(original_image_name, recent_connection));
|
||||
LoadTextureFromFile(image_path.c_str(), renderer,
|
||||
&(recent_connections[i].second.texture), width,
|
||||
&(recent_connections.back().second.texture), width,
|
||||
height);
|
||||
}
|
||||
return 0;
|
||||
@@ -436,4 +443,4 @@ std::string Thumbnail::AES_decrypt(const std::string& ciphertext,
|
||||
|
||||
return std::string(reinterpret_cast<char*>(plaintext), plaintext_len);
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -8,7 +8,15 @@
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -16,12 +24,296 @@
|
||||
namespace crossdesk {
|
||||
|
||||
static std::string latest_release_date_ = "";
|
||||
static bool latest_patch_available_ = false;
|
||||
static int latest_patch_ = 0;
|
||||
|
||||
std::vector<int> SplitVersion(const std::string& ver);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxInlinePatchDigits = 4;
|
||||
|
||||
struct ParsedVersion {
|
||||
std::vector<int> numbers;
|
||||
std::string date;
|
||||
bool has_patch = false;
|
||||
int patch = 0;
|
||||
};
|
||||
|
||||
bool IsDigit(char c) {
|
||||
return std::isdigit(static_cast<unsigned char>(c)) != 0;
|
||||
}
|
||||
|
||||
bool IsAlphaNumeric(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0;
|
||||
}
|
||||
|
||||
bool IsAllDigits(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char c : value) {
|
||||
if (!IsDigit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseNonNegativeInt(const std::string& value, int* result) {
|
||||
if (!IsAllDigits(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const long long parsed = std::stoll(value);
|
||||
if (parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
*result = static_cast<int>(parsed);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryParseInlinePatch(const std::string& value, int* result) {
|
||||
if (value.size() > kMaxInlinePatchDigits) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseNonNegativeInt(value, result);
|
||||
}
|
||||
|
||||
size_t FindNumericStart(const std::string& version) {
|
||||
size_t start = 0;
|
||||
while (start < version.size() && !IsDigit(version[start])) {
|
||||
start++;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
size_t FindNumericEnd(const std::string& version, size_t start) {
|
||||
size_t end = start;
|
||||
while (end < version.size() &&
|
||||
(IsDigit(version[end]) || version[end] == '.')) {
|
||||
end++;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
bool HasDigitBoundary(const std::string& value, size_t pos, size_t len) {
|
||||
const bool before_ok = pos == 0 || !IsDigit(value[pos - 1]);
|
||||
const size_t end = pos + len;
|
||||
const bool after_ok = end >= value.size() || !IsDigit(value[end]);
|
||||
return before_ok && after_ok;
|
||||
}
|
||||
|
||||
bool IsCompactDateAt(const std::string& value, size_t pos) {
|
||||
if (pos + 8 > value.size() || !HasDigitBoundary(value, pos, 8)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 8; ++i) {
|
||||
if (!IsDigit(value[pos + i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CompactDateToIso(const std::string& compact_date) {
|
||||
return compact_date.substr(0, 4) + "-" + compact_date.substr(4, 2) + "-" +
|
||||
compact_date.substr(6, 2);
|
||||
}
|
||||
|
||||
bool ExtractDateFromText(const std::string& value,
|
||||
std::string* date,
|
||||
size_t* date_end) {
|
||||
for (size_t i = 0; i < value.size(); ++i) {
|
||||
if (IsCompactDateAt(value, i)) {
|
||||
*date = CompactDateToIso(value.substr(i, 8));
|
||||
*date_end = i + 8;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ParsedVersion ParseVersion(const std::string& version) {
|
||||
const size_t numeric_start = FindNumericStart(version);
|
||||
const size_t numeric_end = FindNumericEnd(version, numeric_start);
|
||||
|
||||
ParsedVersion parsed;
|
||||
parsed.numbers = SplitVersion(version.substr(numeric_start,
|
||||
numeric_end - numeric_start));
|
||||
|
||||
const std::string suffix = version.substr(numeric_end);
|
||||
size_t pos = 0;
|
||||
while (pos < suffix.size()) {
|
||||
while (pos < suffix.size() && !IsAlphaNumeric(suffix[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
const size_t token_start = pos;
|
||||
while (pos < suffix.size() && IsAlphaNumeric(suffix[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (token_start == pos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string token = suffix.substr(token_start, pos - token_start);
|
||||
if (parsed.date.empty() && IsCompactDateAt(token, 0)) {
|
||||
parsed.date = CompactDateToIso(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
int patch = 0;
|
||||
if (!parsed.has_patch && TryParseInlinePatch(token, &patch)) {
|
||||
parsed.has_patch = true;
|
||||
parsed.patch = patch;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
int CompareNumericVersion(const std::vector<int>& current,
|
||||
const std::vector<int>& latest) {
|
||||
std::vector<int> current_parts = current;
|
||||
std::vector<int> latest_parts = latest;
|
||||
const size_t len = std::max(current_parts.size(), latest_parts.size());
|
||||
current_parts.resize(len, 0);
|
||||
latest_parts.resize(len, 0);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (latest_parts[i] > current_parts[i]) {
|
||||
return 1;
|
||||
}
|
||||
if (latest_parts[i] < current_parts[i]) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ResetLatestMetadata() {
|
||||
latest_release_date_ = "";
|
||||
latest_patch_available_ = false;
|
||||
latest_patch_ = 0;
|
||||
}
|
||||
|
||||
bool ReadPatchField(const nlohmann::json& json, int* patch) {
|
||||
if (!json.contains("patch")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& patch_value = json["patch"];
|
||||
if (patch_value.is_number_integer()) {
|
||||
const long long parsed = patch_value.get<long long>();
|
||||
if (parsed < 0 || parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
*patch = static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (patch_value.is_string()) {
|
||||
return TryParseNonNegativeInt(patch_value.get<std::string>(), patch);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void LogHttpError(const httplib::Result& result) {
|
||||
LOG_WARN("Failed to fetch version.json: error={}, message={}",
|
||||
static_cast<int>(result.error()), httplib::to_string(result.error()));
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
LOG_WARN("version.json SSL error={}, OpenSSL error={}", result.ssl_error(),
|
||||
result.ssl_openssl_error());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
|
||||
bool PathExists(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
return std::filesystem::exists(path, ec);
|
||||
}
|
||||
|
||||
std::string GetEnvPathIfExists(const char* key) {
|
||||
const char* value = std::getenv(key);
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string path = value;
|
||||
return PathExists(path) ? path : "";
|
||||
}
|
||||
|
||||
std::string FindFirstExistingPath(
|
||||
const std::vector<std::string>& candidates) {
|
||||
for (const auto& candidate : candidates) {
|
||||
if (PathExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void ConfigureLinuxCaCerts(httplib::Client* cli) {
|
||||
const std::string ca_file = [&]() {
|
||||
const std::string env_path = GetEnvPathIfExists("SSL_CERT_FILE");
|
||||
if (!env_path.empty()) {
|
||||
return env_path;
|
||||
}
|
||||
|
||||
return FindFirstExistingPath({
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/ssl/cert.pem",
|
||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
||||
});
|
||||
}();
|
||||
|
||||
const std::string ca_dir = [&]() {
|
||||
const std::string env_path = GetEnvPathIfExists("SSL_CERT_DIR");
|
||||
if (!env_path.empty()) {
|
||||
return env_path;
|
||||
}
|
||||
|
||||
return FindFirstExistingPath({
|
||||
"/etc/ssl/certs",
|
||||
"/etc/pki/tls/certs",
|
||||
"/etc/openssl/certs",
|
||||
});
|
||||
}();
|
||||
|
||||
if (ca_file.empty() && ca_dir.empty()) {
|
||||
LOG_WARN("No Linux CA bundle found for version.json request; relying on OpenSSL defaults");
|
||||
return;
|
||||
}
|
||||
|
||||
cli->set_ca_cert_path(ca_file, ca_dir);
|
||||
LOG_INFO("Configured version.json TLS CA bundle: file={}, dir={}",
|
||||
ca_file.empty() ? "<none>" : ca_file,
|
||||
ca_dir.empty() ? "<none>" : ca_dir);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string ExtractNumericPart(const std::string& ver) {
|
||||
size_t start = 0;
|
||||
while (start < ver.size() && !std::isdigit(ver[start])) start++;
|
||||
size_t end = start;
|
||||
while (end < ver.size() && (std::isdigit(ver[end]) || ver[end] == '.')) end++;
|
||||
const size_t start = FindNumericStart(ver);
|
||||
const size_t end = FindNumericEnd(ver, start);
|
||||
return ver.substr(start, end - start);
|
||||
}
|
||||
|
||||
@@ -42,25 +334,13 @@ std::vector<int> SplitVersion(const std::string& ver) {
|
||||
// extract date from version string (format: v1.2.3-20251113-abc
|
||||
// or 1.2.3-20251113-abc)
|
||||
std::string ExtractDateFromVersion(const std::string& version) {
|
||||
size_t dash1 = version.find('-');
|
||||
if (dash1 != std::string::npos) {
|
||||
size_t dash2 = version.find('-', dash1 + 1);
|
||||
if (dash2 != std::string::npos) {
|
||||
std::string date_part = version.substr(dash1 + 1, dash2 - dash1 - 1);
|
||||
|
||||
bool is_date = true;
|
||||
for (char c : date_part) {
|
||||
if (!std::isdigit(c)) {
|
||||
is_date = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_date) {
|
||||
// convert YYYYMMDD to YYYY-MM-DD
|
||||
return date_part.substr(0, 4) + "-" + date_part.substr(4, 2) + "-" +
|
||||
date_part.substr(6, 2);
|
||||
}
|
||||
}
|
||||
const size_t numeric_start = FindNumericStart(version);
|
||||
const size_t numeric_end = FindNumericEnd(version, numeric_start);
|
||||
const std::string suffix = version.substr(numeric_end);
|
||||
std::string date;
|
||||
size_t date_end = 0;
|
||||
if (ExtractDateFromText(suffix, &date, &date_end)) {
|
||||
return date;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -73,55 +353,41 @@ bool IsNewerDate(const std::string& date1, const std::string& date2) {
|
||||
}
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest) {
|
||||
auto v1 = SplitVersion(ExtractNumericPart(current));
|
||||
auto v2 = SplitVersion(ExtractNumericPart(latest));
|
||||
|
||||
size_t len = std::max(v1.size(), v2.size());
|
||||
v1.resize(len, 0);
|
||||
v2.resize(len, 0);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (v2[i] > v1[i]) return true;
|
||||
if (v2[i] < v1[i]) return false;
|
||||
}
|
||||
|
||||
// if versions are equal, compare by release date
|
||||
if (!latest_release_date_.empty()) {
|
||||
// try to extract date from current version string
|
||||
std::string current_date = ExtractDateFromVersion(current);
|
||||
if (!current_date.empty()) {
|
||||
return IsNewerDate(current_date, latest_release_date_);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return IsNewerVersionWithMetadata(
|
||||
current, latest, latest_release_date_,
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
}
|
||||
|
||||
bool IsNewerVersionWithDate(const std::string& current_version,
|
||||
const std::string& current_date,
|
||||
const std::string& latest_version,
|
||||
const std::string& latest_date) {
|
||||
// compare versions
|
||||
auto v1 = SplitVersion(ExtractNumericPart(current_version));
|
||||
auto v2 = SplitVersion(ExtractNumericPart(latest_version));
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
const std::string& latest,
|
||||
const std::string& latest_date,
|
||||
int latest_patch) {
|
||||
(void)latest_date;
|
||||
|
||||
size_t len = std::max(v1.size(), v2.size());
|
||||
v1.resize(len, 0);
|
||||
v2.resize(len, 0);
|
||||
const ParsedVersion current_version = ParseVersion(current);
|
||||
const ParsedVersion latest_version = ParseVersion(latest);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (v2[i] > v1[i]) return true;
|
||||
if (v2[i] < v1[i]) return false;
|
||||
const int numeric_compare =
|
||||
CompareNumericVersion(current_version.numbers, latest_version.numbers);
|
||||
if (numeric_compare > 0) {
|
||||
return true;
|
||||
}
|
||||
if (numeric_compare < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if versions are equal, compare by release date
|
||||
if (!current_date.empty() && !latest_date.empty()) {
|
||||
return IsNewerDate(current_date, latest_date);
|
||||
const bool metadata_has_patch = latest_patch >= 0;
|
||||
const bool latest_has_patch = metadata_has_patch || latest_version.has_patch;
|
||||
if (latest_has_patch || current_version.has_patch) {
|
||||
const int resolved_latest_patch =
|
||||
metadata_has_patch ? latest_patch
|
||||
: (latest_version.has_patch ? latest_version.patch
|
||||
: 0);
|
||||
const int resolved_current_patch =
|
||||
current_version.has_patch ? current_version.patch : 0;
|
||||
return resolved_latest_patch > resolved_current_patch;
|
||||
}
|
||||
|
||||
// if dates are not available, versions are equal
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -130,8 +396,14 @@ nlohmann::json CheckUpdate() {
|
||||
|
||||
cli.set_connection_timeout(5);
|
||||
cli.set_read_timeout(5);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
if (auto res = cli.Get("/version.json")) {
|
||||
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
|
||||
ConfigureLinuxCaCerts(&cli);
|
||||
#endif
|
||||
|
||||
auto res = cli.Get("/version.json");
|
||||
if (res) {
|
||||
if (res->status == 200) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(res->body);
|
||||
@@ -140,19 +412,28 @@ nlohmann::json CheckUpdate() {
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
}
|
||||
latest_patch_ = 0;
|
||||
latest_patch_available_ = ReadPatchField(j, &latest_patch_);
|
||||
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
|
||||
j.value("latest_version", j.value("version", "")),
|
||||
j.value("releaseDate", ""),
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
return j;
|
||||
} catch (std::exception&) {
|
||||
latest_release_date_ = "";
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARN("Failed to parse version.json: {}", e.what());
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
LogHttpError(res);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -16,6 +16,12 @@ nlohmann::json CheckUpdate();
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest);
|
||||
|
||||
// Pass latest_patch < 0 when patch metadata is unavailable.
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
const std::string& latest,
|
||||
const std::string& latest_date,
|
||||
int latest_patch);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: bb0fae0617...0d9a9d6a32
@@ -0,0 +1,71 @@
|
||||
#include "device_controller.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
bool ExpectEqual(const char* name, size_t actual, size_t expected) {
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " mismatch\n"
|
||||
<< " expected: " << expected << "\n"
|
||||
<< " actual: " << actual << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectTrue(const char* name, bool value) {
|
||||
if (value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " expected true\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
bool ok = true;
|
||||
ok &= ExpectEqual("mouse type", crossdesk::ControlType::mouse, 0);
|
||||
ok &= ExpectEqual("keyboard type", crossdesk::ControlType::keyboard, 1);
|
||||
ok &= ExpectEqual("audio_capture type", crossdesk::ControlType::audio_capture,
|
||||
2);
|
||||
ok &= ExpectEqual("host_infomation type",
|
||||
crossdesk::ControlType::host_infomation, 3);
|
||||
ok &= ExpectEqual("display_id type", crossdesk::ControlType::display_id, 4);
|
||||
ok &= ExpectEqual("service_status type",
|
||||
crossdesk::ControlType::service_status, 5);
|
||||
ok &= ExpectEqual("service_command type",
|
||||
crossdesk::ControlType::service_command, 6);
|
||||
ok &= ExpectEqual("keyboard_state type",
|
||||
crossdesk::ControlType::keyboard_state, 7);
|
||||
|
||||
crossdesk::RemoteAction action{};
|
||||
action.type = crossdesk::ControlType::keyboard_state;
|
||||
action.ks.seq = 42;
|
||||
action.ks.pressed_count = 2;
|
||||
action.ks.pressed_keys[0] = {65, 30, false};
|
||||
action.ks.pressed_keys[1] = {0xA3, 29, true};
|
||||
|
||||
const std::string json = action.to_json();
|
||||
|
||||
crossdesk::RemoteAction parsed{};
|
||||
ok &= ExpectTrue("parse keyboard_state", parsed.from_json(json));
|
||||
ok &= ExpectEqual("parsed type", parsed.type,
|
||||
crossdesk::ControlType::keyboard_state);
|
||||
ok &= ExpectEqual("parsed seq", parsed.ks.seq, 42);
|
||||
ok &= ExpectEqual("parsed pressed_count", parsed.ks.pressed_count, 2);
|
||||
ok &= ExpectEqual("parsed key 0", parsed.ks.pressed_keys[0].key_value, 65);
|
||||
ok &= ExpectEqual("parsed scan 0", parsed.ks.pressed_keys[0].scan_code, 30);
|
||||
ok &= ExpectTrue("parsed extended 0",
|
||||
!parsed.ks.pressed_keys[0].extended);
|
||||
ok &= ExpectEqual("parsed key 1", parsed.ks.pressed_keys[1].key_value,
|
||||
0xA3);
|
||||
ok &= ExpectEqual("parsed scan 1", parsed.ks.pressed_keys[1].scan_code, 29);
|
||||
ok &= ExpectTrue("parsed extended 1", parsed.ks.pressed_keys[1].extended);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "version_checker.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
bool ExpectEqual(const std::string& name, bool actual, bool expected) {
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " mismatch\n"
|
||||
<< " expected: " << expected << "\n"
|
||||
<< " actual: " << actual << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
bool ok = true;
|
||||
|
||||
ok &= ExpectEqual("new patch-before-date is newer",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-20260529", "v1.3.5-1-20260529", "", -1),
|
||||
true);
|
||||
ok &= ExpectEqual("larger patch wins regardless of date",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-2-20260530", "v1.3.5-3-20260529", "", -1),
|
||||
true);
|
||||
ok &= ExpectEqual("smaller patch loses regardless of date",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-3-20260529", "v1.3.5-2-20260530", "", -1),
|
||||
false);
|
||||
ok &= ExpectEqual("old date-before-patch remains supported",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-20260529-1", "v1.3.5-20260529-2", "", -1),
|
||||
true);
|
||||
ok &= ExpectEqual("metadata patch overrides date",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-9-20260530", "v1.3.5", "2026-05-31", 10),
|
||||
true);
|
||||
ok &= ExpectEqual("date alone does not update same version",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-20260529", "v1.3.5-20260530", "", -1),
|
||||
false);
|
||||
ok &= ExpectEqual("numeric version still wins",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-9-20260529", "v1.3.6-1-20260529", "", -1),
|
||||
true);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -82,16 +82,27 @@ int main() {
|
||||
}
|
||||
|
||||
const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc");
|
||||
const std::string portable_rc =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_portable.rc");
|
||||
const std::string manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
const std::string debug_manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
const std::string portable_manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_portable.manifest");
|
||||
const std::string targets = ReadFile(repo_root / "xmake/targets.lua");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST");
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc,
|
||||
"crossdesk_portable.manifest");
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc, "RT_MANIFEST");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets,
|
||||
"scripts/windows/crossdesk_portable.rc");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets, "CROSSDESK_PORTABLE");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"level=\"requireAdministrator\"");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
@@ -108,10 +119,22 @@ int main() {
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"level=\"asInvoker\"");
|
||||
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"level=\"requireAdministrator\"");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2005/WindowsSettings");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
#ifdef _WIN32
|
||||
ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_portable.manifest");
|
||||
#endif
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
+10
-4
@@ -35,7 +35,12 @@ function setup_platform_settings()
|
||||
add_links("pulse-simple", "pulse")
|
||||
add_requires("libyuv")
|
||||
add_syslinks("pthread", "dl")
|
||||
add_links("SDL3", "asound", "X11", "Xtst", "Xrandr", "Xfixes")
|
||||
add_links("SDL3", "asound", "X11", "Xext", "Xrender", "Xft", "Xtst",
|
||||
"Xrandr", "Xfixes")
|
||||
add_existing_include_dirs({
|
||||
"/usr/include/freetype2",
|
||||
"/usr/local/include/freetype2"
|
||||
}, {system = true})
|
||||
|
||||
if is_config("USE_DRM", true) then
|
||||
add_links("drm")
|
||||
@@ -75,7 +80,8 @@ function setup_platform_settings()
|
||||
add_links("SDL3")
|
||||
add_ldflags("-Wl,-ld_classic")
|
||||
add_cxflags("-Wno-unused-variable")
|
||||
add_frameworks("OpenGL", "IOSurface", "ScreenCaptureKit", "AVFoundation",
|
||||
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox")
|
||||
add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
|
||||
"AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
|
||||
"AudioToolbox")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+36
-4
@@ -3,6 +3,11 @@ function setup_targets()
|
||||
|
||||
includes("submodules", "thirdparty")
|
||||
|
||||
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
|
||||
if is_config("CROSSDESK_PORTABLE", true) then
|
||||
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
|
||||
end
|
||||
|
||||
target("rd_log")
|
||||
set_kind("object")
|
||||
add_packages("spdlog")
|
||||
@@ -39,6 +44,12 @@ function setup_targets()
|
||||
add_includedirs("src/device_controller")
|
||||
add_files("tests/macos_keyboard_modifier_state_test.cpp")
|
||||
|
||||
target("keyboard_state_protocol_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_includedirs("src/device_controller", "src/common")
|
||||
add_files("tests/keyboard_state_protocol_test.cpp")
|
||||
|
||||
target("windows_manifest_resource_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
@@ -65,6 +76,19 @@ function setup_targets()
|
||||
set_default(false)
|
||||
add_files("tests/display_popup_hover_state_test.cpp")
|
||||
|
||||
target("version_checker_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_packages("cpp-httplib")
|
||||
add_deps("rd_log")
|
||||
add_includedirs("src/version_checker")
|
||||
add_files("tests/version_checker_test.cpp",
|
||||
"src/version_checker/version_checker.cpp")
|
||||
if is_os("macosx") then
|
||||
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
|
||||
add_frameworks("Security", "CoreFoundation")
|
||||
end
|
||||
|
||||
target("screen_capturer")
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "common")
|
||||
@@ -164,6 +188,10 @@ function setup_targets()
|
||||
add_deps("rd_log")
|
||||
add_files("src/version_checker/*.cpp")
|
||||
add_includedirs("src/version_checker", {public = true})
|
||||
if is_os("macosx") then
|
||||
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
|
||||
add_frameworks("Security", "CoreFoundation")
|
||||
end
|
||||
|
||||
target("tools")
|
||||
set_kind("object")
|
||||
@@ -186,11 +214,15 @@ function setup_targets()
|
||||
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
|
||||
"src/gui/windows", {public = true})
|
||||
if is_os("windows") then
|
||||
add_files("src/gui/tray/*.cpp")
|
||||
add_files("src/gui/tray/win_tray.cpp")
|
||||
add_includedirs("src/gui/tray", "src/service/windows",
|
||||
{public = true})
|
||||
elseif is_os("macosx") then
|
||||
add_files("src/gui/windows/*.mm")
|
||||
add_files("src/gui/windows/*.mm", "src/gui/tray/*.mm")
|
||||
add_includedirs("src/gui/tray", {public = true})
|
||||
elseif is_os("linux") then
|
||||
add_files("src/gui/tray/linux_tray.cpp")
|
||||
add_includedirs("src/gui/tray", {public = true})
|
||||
end
|
||||
|
||||
if is_os("windows") then
|
||||
@@ -223,7 +255,7 @@ function setup_targets()
|
||||
add_deps("rd_log", "path_manager")
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||
add_files("src/service/windows/session_helper_main.cpp")
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
add_files(crossdesk_windows_resource)
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
end
|
||||
|
||||
@@ -237,6 +269,6 @@ function setup_targets()
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
add_files(crossdesk_windows_resource)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user