Compare commits

...

28 Commits

Author SHA1 Message Date
dijunkun 0681f6540d [fix] fix persistent scrollbar in recent connections area when empty, refs #85 2026-06-26 17:29:04 +08:00
dijunkun d843901550 [fix] fix intermittent crash on incoming connection by guarding shared connection status state across threads 2026-06-26 17:21:09 +08:00
dijunkun b41630ebb4 [fix] fix blank connection status popup by handling Gathering state 2026-06-26 16:23:15 +08:00
dijunkun a815eecc73 [fix] fix close-issue workflow env indentation, pagination, and activity time base 2026-06-26 16:14:09 +08:00
dijunkun 7e0984fe9c [chore] refine online status indicator with gradient glow and remove status text 2026-06-26 16:09:21 +08:00
dijunkun 9c28cd2ab2 [fix] fix crash when switching monitors due to race condition and missing bounds checks in screen capturer 2026-06-26 14:44:30 +08:00
dijunkun 3c4000bdbb [fix] add missing Xft development dependency for Linux tray build 2026-06-25 00:35:25 +08:00
dijunkun d8e9fa5bba [feat] support minimizing to the system tray on Linux when closing 2026-06-23 17:11:42 +08:00
dijunkun e026491b9f [feat] support minimizing to the system tray on macOS when closing, refs #87 2026-06-23 17:11:42 +08:00
dijunkun 009699b375 [chore] optimize online status indicator icon 2026-06-22 16:52:51 +08:00
dijunkun 3677588a3d [fix] align recent connection name footer 2026-06-18 10:53:31 +08:00
dijunkun 3d280053a7 [feat] add custom names for recent connection devices and improve panel display 2026-06-17 17:48:17 +08:00
dijunkun fbde3f6a47 [fix] prevent remote keyboard keys from getting stuck 2026-06-15 17:28:34 +08:00
dijunkun 1c1a33fdce [fix] use evdev keycodes for Wayland keyboard input 2026-06-12 11:25:00 +08:00
kunkundi fea238722d [fix] fix macOS remote double-click and triple-click handling, refs #86 2026-06-05 00:25:08 +08:00
kunkundi 178d958c08 [fix] show self-hosted TLS certificate errors in status bar 2026-06-01 00:31:45 +08:00
kunkundi f9633f366b [fix] update MiniRTC: support loading macOS self-hosted TLS roots, refs #79 2026-06-01 00:31:27 +08:00
dijunkun 7a81f3e767 Merge branch 'file-transfer' of https://github.com/kunkundi/crossdesk into file-transfer 2026-05-31 23:21:42 +08:00
dijunkun bbbbbf7927 [fix] update MiniRTC: support loading Windows self-signed root certificates from intermediate stores, refs #79 2026-05-31 23:04:13 +08:00
dijunkun 5735f84008 [ci] include version alias in version.json for compatibility 2026-05-29 11:28:09 +08:00
kunkundi fe0cf42e5d [fix] use Linux system CA paths for update checks 2026-05-29 10:51:22 +08:00
kunkundi 04100584ce [fix] restore latest_version update metadata 2026-05-29 02:11:46 +08:00
kunkundi 9d3a422916 [fix] log update check failures and use macOS trust store 2026-05-29 02:00:13 +08:00
kunkundi 65d8284fb8 [ci] standardize hotfix package version ordering across platforms 2026-05-29 00:52:07 +08:00
kunkundi eea107db66 [chore] adjust settings window button vertical spacing 2026-05-28 23:36:25 +08:00
kunkundi 67812957db [ci] fix GitHub Actions Linux build version parsing 2026-05-28 23:30:42 +08:00
dijunkun 69d77e59cc [fix] make portable Windows Service setup optional, refs #84 2026-05-28 18:59:27 +08:00
dijunkun efcebfd82c [feat] add explicit hotfix patch version support 2026-05-28 15:22:35 +08:00
49 changed files with 4345 additions and 675 deletions
+146 -22
View File
@@ -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 }}",
+14 -25
View File
@@ -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 }}
+27 -7
View File
@@ -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 }}",
+19 -1
View File
@@ -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}"
+19 -1
View File
@@ -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}"
+19 -1
View File
@@ -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"
+19 -1
View File
@@ -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>
+8
View File
@@ -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"
+40
View File
@@ -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
View File
@@ -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
+21
View File
@@ -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",
+3
View File
@@ -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
+63 -6
View File
@@ -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;
+295
View File
@@ -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
@@ -1,11 +1,36 @@
#include "mouse_controller.h"
#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() {}
@@ -19,6 +44,36 @@ 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) {
if (remote_action.type != ControlType::mouse) {
@@ -41,58 +96,69 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
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;
int mouse_pos_x = normalized_x * display_info.width + display_info.left;
int mouse_pos_y = normalized_y * display_info.height + display_info.top;
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);
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);
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
2, 0, remote_action.m.s);
break;
default:
if (left_dragging_) {
@@ -106,8 +172,8 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
mouse_button = kCGMouseButtonLeft;
}
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
mouse_button);
mouse_event =
CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, mouse_button);
break;
}
@@ -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
+211 -170
View File
@@ -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[] = {
+389 -110
View File
@@ -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
+2 -2
View File
@@ -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_;
+283 -31
View File
@@ -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()) {
@@ -1187,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() {
@@ -1253,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_);
@@ -1552,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;
}
@@ -1714,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();
@@ -1762,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();
@@ -1785,6 +1955,7 @@ void Render::InitializeLogger() { InitLogger(exec_log_path_); }
void Render::InitializeSettings() {
LoadSettingsFromCacheFile();
LoadRecentConnectionAliases();
localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_);
@@ -1818,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_);
@@ -1894,6 +2068,10 @@ void Render::MainLoop() {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
#elif defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->ProcessEvents();
}
#endif
UpdateLabels();
@@ -1904,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();
}
@@ -1917,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_) {
@@ -1973,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;
@@ -2253,6 +2460,7 @@ void Render::HandleServerWindow() {
if (need_to_destroy_server_window_) {
DestroyServerWindow();
DestroyServerWindowContext();
need_to_destroy_server_window_ = false;
}
}
@@ -2293,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();
@@ -2706,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_) {
@@ -2776,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:
+64 -3
View File
@@ -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_;
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;
@@ -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_ = "";
+333 -82
View File
@@ -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,12 +1412,10 @@ 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_) {
const int ret = render->screen_capturer_->SwitchTo(remote_action.d);
@@ -1231,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) {
@@ -1258,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;
}
}
}
@@ -1268,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: {
@@ -1341,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);
@@ -1390,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: {
@@ -1446,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;
@@ -1458,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__)
@@ -1490,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;
+2 -2
View File
@@ -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;
+18 -7
View File
@@ -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();
+2 -7
View File
@@ -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
+40
View File
@@ -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_
+34
View File
@@ -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_
+249
View File
@@ -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__
+24 -8
View File
@@ -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);
+113 -39
View File
@@ -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) {
+22 -13
View File
@@ -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
+2 -2
View File
@@ -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;
}
@@ -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;
}
+17 -10
View File
@@ -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
+351 -70
View File
@@ -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
+7 -1
View File
@@ -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
+71
View File
@@ -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;
}
+54
View File
@@ -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;
}
+23
View File
@@ -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
View File
@@ -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
View File
@@ -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