Compare commits

..

81 Commits

Author SHA1 Message Date
dijunkun
f7f62c5fe0 [fix] update MiniRTC: refactor IceAgent to improve stability 2026-03-20 15:58:59 +08:00
dijunkun
2bbddbca6b [fix] fix Linux audio fallback when audio devices are unavaliable 2026-03-20 15:14:45 +08:00
dijunkun
f0f8f27f4c [fix] fix blocking join() in Linux clipboard monitor thread during shutdown 2026-03-20 15:02:34 +08:00
dijunkun
262af263f2 [fix] move keyboard capturer to a background thread and use poll-based X11 event handling to avoid main-thread blocking 2026-03-20 14:56:40 +08:00
dijunkun
38b7775b1b [fix] fix restart/shutdown races in process monitor 2026-03-20 14:50:42 +08:00
dijunkun
56c0bca62f [chore] adjust hyperlink spacing and alignment 2026-03-20 14:47:33 +08:00
dijunkun
4b1b09fd5b [fix] fix Linux fonts: use opentype instead of truetype 2026-03-20 13:01:13 +08:00
dijunkun
1d6425bbf4 [fix] update MiniRTC: fix compiler warnings by adding missing override specifiers 2026-03-20 04:36:58 +08:00
dijunkun
5ec6552d25 [fix] fix macOS intel CI build failure caused by python 3.13 PGO mismatch 2026-03-20 03:50:19 +08:00
dijunkun
79e4a0790a [fix] fix issue where wgc_plugin was not compiled 2026-03-20 02:59:31 +08:00
dijunkun
1d3cac54ab [feat] load wgc from wgc_plugin.dll at runtime and drop direct'windowsapp' linking, refs #74 2026-03-20 01:36:36 +08:00
dijunkun
2f26334775 [feat] unify UI font loading across platforms and prefer PingFang on macOS 2026-03-19 21:58:14 +08:00
dijunkun
9270d528e3 [feat] update miniRTC: fix compiler warnings caused by missing override specifiers 2026-03-19 21:57:52 +08:00
dijunkun
91db3a7e34 [feat] add Russian language support 2026-03-19 20:04:30 +08:00
dijunkun
d017561e54 [fix] fix typo ImGuiChildFlags_Border to ImGuiChildFlags_Borders 2026-03-19 16:16:51 +08:00
dijunkun
8e8a85bae3 [feat] upgrade actions/checkout and actions/cache to v5 for Node 24 compatibility 2026-03-19 15:03:58 +08:00
dijunkun
bea89e9111 [feat] crossdesk server image supports Linux ARM64, refs #72 2026-03-19 10:06:57 +08:00
dijunkun
499ce0190a [fix] process mouse events only from the stream window 2026-03-11 16:00:29 +08:00
dijunkun
91bde91238 [feat] probe presence before connect and show warning if offline 2026-03-10 17:46:44 +08:00
dijunkun
3e31ba102d [fix] prevent sending connection requests to offline devices 2026-03-10 10:53:58 +08:00
dijunkun
263c5eefd3 [fix] fix update button lag in release mode by using non-blocking URL opener. 2026-03-10 10:39:05 +08:00
dijunkun
b230b851e4 [fix] fix cannot close connection from Server Window when the peer is a web client 2026-03-10 00:39:00 +08:00
dijunkun
ff32477ffe [fix] update MiniRTC: fix crash on disconnect 2026-03-10 00:35:33 +08:00
dijunkun
c6c60decdb [fix] fix incorrect online status of recently connections 2026-03-09 22:52:05 +08:00
dijunkun
7505adeca8 [feat] update MiniRTC 2026-03-09 22:50:42 +08:00
dijunkun
754f1fba88 [feat] show 'Receiving screen' text before the remote frame arrives 2026-03-09 22:37:50 +08:00
dijunkun
8be46b870a [feat] add cancel button during connecting 2026-03-09 21:35:21 +08:00
dijunkun
81cb1d6c0b [fix] disable clipboard sharing when not in control mode 2026-03-05 17:46:27 +08:00
dijunkun
319416f1b7 [feat] update MiniRTC: optimize video quality and smoothness 2026-03-05 17:30:05 +08:00
dijunkun
d679c6251b [feat] update MiniRTC 2026-03-04 10:46:21 +08:00
dijunkun
a14baafda7 [fix] fix keyboard event loss due to start_keyboard_capturer_ flag improper setting, fixes #65 2026-03-04 10:36:39 +08:00
dijunkun
cfdc7d3106 [fix] update MiniRTC: fix bandwidth degradation caused by ALR-triggered resolution downgrade during static frames 2026-03-03 10:58:38 +08:00
dijunkun
33d51b8ce5 [fix] reset to initial monitor on connection close via ResetToInitialMonitor to fix black screen 2026-03-02 15:42:44 +08:00
dijunkun
b13dac2093 [feat] refine display of recent connections presence tooltip 2026-03-02 10:48:16 +08:00
dijunkun
a605c95e5a [fix] fix window rounding inconsistency under different DPI scales 2026-03-02 10:38:06 +08:00
dijunkun
9a5553a636 [chore] update fonts 2026-03-02 10:17:06 +08:00
dijunkun
ef02403da6 [fix] fix incorrect sizing of the online status indicator on high-DPI displays 2026-03-01 16:47:09 +08:00
dijunkun
adfab363c1 [feat] add online presence check before connecting and show offline warning dialog 2026-03-01 16:29:11 +08:00
dijunkun
123d4cf595 [fix] update MiniRTC: fix the macOS hardware decode fail when the server using openH264 encode 2026-03-01 15:40:50 +08:00
dijunkun
19feb8ff49 [feat] show device online/offline status in recent connection tooltip 2026-02-28 17:25:41 +08:00
dijunkun
9223bf9d2d [feat] add online status indicators for recent connections 2026-02-28 17:06:44 +08:00
dijunkun
11b5f87841 [feat] update MiniRTC: add signaling send/receive API support 2026-02-28 17:04:47 +08:00
dijunkun
cea59fb453 [feat] update MiniRTC 2026-02-27 17:53:51 +08:00
ZongYangBigPolo
3a179bf480 [feat] add macOS installer icon and optimize packaging script (#70) 2026-02-27 17:37:54 +08:00
dijunkun
b9c53024f1 [feat] set video quality to HIGH and enable hardware codec by default 2026-02-27 17:24:04 +08:00
dijunkun
62b37ad698 [fix] resolve failures in the WGC→DXGI→GDI fallback chain 2026-02-27 16:33:57 +08:00
dijunkun
de56cd5d3b [feat] update MiniRTC 2026-02-27 16:30:08 +08:00
dijunkun
8d9d78185a [fix] fix issue where client display list was incorrectly merged into the server display list 2026-02-27 16:27:37 +08:00
dijunkun
b10a6512fe [feat] add Windows DXGI/GDI screen capture with WGC→DXGI→GDI fallback support 2026-02-27 13:55:41 +08:00
dijunkun
a94da8802f [feat] make MainWindow and ServerWindow use rounded corners 2026-02-26 18:06:07 +08:00
dijunkun
4e6f82d00c [feat] restrict the dragging range of the ControlWindow 2026-02-26 15:55:04 +08:00
dijunkun
5e2ad99ec0 [feat] update MiniRTC to resolve color distortion in the OpenH264 decoder 2026-02-25 17:48:30 +08:00
dijunkun
8ab50ea362 [feat] add video resolution and conection mode in NetTrafficStats window 2026-02-25 15:33:17 +08:00
dijunkun
25e9958a69 Merge branch 'file-transfer' of https://github.com/kunkundi/crossdesk into file-transfer 2026-02-24 17:56:21 +08:00
dijunkun
410ea8b96b [feat] update MiniRTC 2026-02-24 17:56:02 +08:00
dijunkun
e656664cad [chore] adjust file transfer save path button pos 2026-02-24 14:36:03 +08:00
dijunkun
0e6cee0961 [fix] fix stream window rendering height 2026-02-24 14:31:34 +08:00
dijunkun
42506b8c1d [ci] combine Linux amd64 and arm64 builds into a single job using matrix 2026-02-13 02:31:58 +08:00
dijunkun
e35365d162 [feat] disable and style minimize_to_tray checkbox for non-Windows platforms 2026-02-13 02:29:52 +08:00
dijunkun
bf1c0f796d [fix] fix Linux system certificate loading failure 2026-02-13 01:56:49 +08:00
dijunkun
547532b28c [fix] fix server window scaling issues on high-DPI displays 2026-02-13 01:26:10 +08:00
dijunkun
a91e23abf6 [fix] fix raw pointer issues when closing connections 2026-02-13 01:12:21 +08:00
dijunkun
2b324f636b [fix] fix macOS system certificate loading failure 2026-02-12 22:49:54 +08:00
dijunkun
103b8372e4 [chore] rename packaged executable to CrossDesk.exe in NSIS and portable artifacts 2026-02-12 16:45:41 +08:00
dijunkun
f7f1724bf1 [feat] optimize hyperlink opening by replacing system start with CreateProcessW-based URL launch on Winodws 2026-02-12 16:22:57 +08:00
dijunkun
5d70e11f17 [feat] support Windows x64 portable build, refs #54 2026-02-12 16:03:06 +08:00
dijunkun
fb7ae90d46 [feat] add configurable file transfer save path in settings window, refs #63 2026-02-12 14:30:14 +08:00
dijunkun
957792a7a0 [feat] remove client certificate dependency 2026-02-11 16:23:43 +08:00
dijunkun
2e8ce6a2f0 [fix] reset default cert fingerprint if mismatch 2026-02-05 18:59:28 +08:00
dijunkun
9927a56b78 [feat] update MiniRTC 2026-02-05 18:05:35 +08:00
dijunkun
db3da52f83 [feat] clear cached fingerprint when verification fails 2026-02-05 17:15:59 +08:00
dijunkun
19a7c6978a [feat] update MiniRTC to resolve websocket reconnection and post task issues 2026-01-28 09:45:19 +08:00
dijunkun
b5e9ba03a1 [fix] double-buffer video frames and handle stream cleanup on main thread 2026-01-28 09:44:54 +08:00
dijunkun
cb5f8b91ad [feat] update update-notification icon 2026-01-27 21:11:26 +08:00
dijunkun
f627f60f1a [feat] use tooltips to display server-side file transfer status information 2026-01-27 17:50:21 +08:00
dijunkun
e9fce5b8b8 [feat] display remote controller hostname instead of remote id 2026-01-26 22:52:58 +08:00
dijunkun
a7820a79db [fix] fix incorrect peer_ usage in SendReliableDataFrame 2026-01-26 21:47:10 +08:00
dijunkun
b6a52dbcd4 [feat] add support for displaying multiple controller info and file transfer to controllers 2026-01-26 17:47:31 +08:00
dijunkun
7bbd10a50c [fix] fix rendering issues in stream and server windows when the main window is minimized 2026-01-22 17:56:00 +08:00
dijunkun
ee08b231db [fix] fix height when server window is restored from collapsed state 2026-01-20 23:58:43 +08:00
dijunkun
619e54dc0e [feat] add controller info and file transfer in server window 2026-01-20 21:22:20 +08:00
74 changed files with 6972 additions and 4177 deletions

View File

@@ -15,82 +15,28 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs: jobs:
# Linux amd64 build-linux:
build-linux-amd64: name: Build Linux (${{ matrix.arch }})
name: Build on Ubuntu 22.04 amd64 runs-on: ${{ matrix.runner }}
runs-on: ubuntu-22.04
container:
image: crossdesk/ubuntu20.04:latest
options: --user root
steps:
- name: Extract version number
id: version
run: |
VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
- name: Set legal Debian version
shell: bash
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Build CrossDesk
env:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
ls -la $XMAKE_GLOBALDIR
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package
run: |
chmod +x ./scripts/linux/pkg_amd64.sh
./scripts/linux/pkg_amd64.sh ${LEGAL_VERSION}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: crossdesk-linux-amd64-${{ env.LEGAL_VERSION }}
path: ${{ github.workspace }}/crossdesk-linux-amd64-${{ env.LEGAL_VERSION }}.deb
# Linux arm64
build-linux-arm64:
name: Build on Ubuntu 22.04 arm64
runs-on: ubuntu-22.04-arm
strategy: strategy:
matrix: matrix:
include: include:
- arch: amd64
runner: ubuntu-22.04
image: crossdesk/ubuntu20.04:latest
package_script: ./scripts/linux/pkg_amd64.sh
- arch: arm64 - arch: arm64
runner: ubuntu-22.04-arm
image: crossdesk/ubuntu20.04-arm64v8:latest image: crossdesk/ubuntu20.04-arm64v8:latest
package_script: ./scripts/linux/pkg_arm64.sh package_script: ./scripts/linux/pkg_arm64.sh
container: container:
image: ${{ matrix.image }} image: ${{ matrix.image }}
options: --user root options: --user root
steps: steps:
- name: Extract version number - name: Extract version number
id: version
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}" VERSION_NUM="${VERSION#v}"
@@ -98,20 +44,21 @@ jobs:
- name: Set legal Debian version - name: Set legal Debian version
shell: bash shell: bash
id: set_deb_version
run: | run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else else
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
fi fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
submodules: recursive submodules: recursive
@@ -123,19 +70,13 @@ jobs:
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk xmake b -vy --root crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package - name: Package
run: | run: |
chmod +x ${{ matrix.package_script }} chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${LEGAL_VERSION} ${{ matrix.package_script }} ${LEGAL_VERSION}
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }} name: crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}
path: ${{ github.workspace }}/crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}.deb path: ${{ github.workspace }}/crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}.deb
@@ -171,7 +112,7 @@ jobs:
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v4 uses: actions/cache@v5
with: with:
path: ~/.xmake/packages path: ~/.xmake/packages
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.sha }} key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.sha }}
@@ -182,7 +123,7 @@ jobs:
run: brew install xmake run: brew install xmake
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Initialize submodules - name: Initialize submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
@@ -192,19 +133,13 @@ jobs:
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk xmake b -vy crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package CrossDesk app - name: Package CrossDesk app
run: | run: |
chmod +x ${{ matrix.package_script }} chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${VERSION_NUM} ${{ matrix.package_script }} ${VERSION_NUM}
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }} name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
@@ -234,7 +169,7 @@ jobs:
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v4 uses: actions/cache@v5
with: with:
path: D:\xmake_global\.xmake\packages path: D:\xmake_global\.xmake\packages
key: ${{ runner.os }}-xmake-deps-intel-${{ github.sha }} key: ${{ runner.os }}-xmake-deps-intel-${{ github.sha }}
@@ -286,7 +221,7 @@ jobs:
Copy-Item $source $target -Force Copy-Item $source $target -Force
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Initialize submodules - name: Initialize submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
@@ -301,37 +236,45 @@ jobs:
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
xmake b -vy crossdesk xmake b -vy crossdesk
- name: Decode and save certificate
shell: powershell
run: |
New-Item -ItemType Directory -Force -Path certs
[System.IO.File]::WriteAllBytes('certs\crossdesk.cn_root.crt', [Convert]::FromBase64String('${{ secrets.CROSSDESK_CERT_BASE64 }}'))
- name: Package - name: Package
shell: pwsh shell: pwsh
run: | run: |
cd "${{ github.workspace }}\scripts\windows" cd "${{ github.workspace }}\scripts\windows"
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
- name: Package Portable
shell: pwsh
run: |
$portableDir = "${{ github.workspace }}\portable"
New-Item -ItemType Directory -Force -Path $portableDir
Copy-Item "${{ github.workspace }}\build\windows\x64\release\crossdesk.exe" "$portableDir\CrossDesk.exe"
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: crossdesk-win-x64-${{ env.VERSION_NUM }} name: crossdesk-win-x64-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
- name: Upload portable artifact
uses: actions/upload-artifact@v6
with:
name: crossdesk-win-x64-portable-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip
release: release:
name: Publish Release name: Publish Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: needs:
[build-linux-amd64, build-linux-arm64, build-macos, build-windows-x64] [build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
path: artifacts path: artifacts
@@ -359,6 +302,7 @@ jobs:
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
- name: List release files - name: List release files
run: ls -lh release/ run: ls -lh release/
@@ -416,6 +360,10 @@ jobs:
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe", "url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe" "filename": "crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe"
}, },
"windows-x64-portable": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip",
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip"
},
"macos-x64": { "macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg", "url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg" "filename": "crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg"

View File

@@ -11,7 +11,7 @@ jobs:
update-version-json: update-version-json:
name: Update version.json with release information name: Update version.json with release information
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -25,7 +25,7 @@ jobs:
VERSION_ONLY="${TAG_NAME#v}" VERSION_ONLY="${TAG_NAME#v}"
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
# Extract date from tag if available (format: v1.2.3-20251113-abc) # Extract date from tag if available (format: v1.2.3-20251113-abc)
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then
DATE_STR="${BASH_REMATCH[1]}" DATE_STR="${BASH_REMATCH[1]}"
@@ -45,7 +45,7 @@ jobs:
# Use jq to properly escape JSON # Use jq to properly escape JSON
RELEASE_BODY="${{ github.event.release.body }}" RELEASE_BODY="${{ github.event.release.body }}"
RELEASE_NAME="${{ github.event.release.name }}" RELEASE_NAME="${{ github.event.release.name }}"
# Handle empty values # Handle empty values
if [ -z "$RELEASE_BODY" ]; then if [ -z "$RELEASE_BODY" ]; then
RELEASE_BODY="" RELEASE_BODY=""
@@ -53,15 +53,15 @@ jobs:
if [ -z "$RELEASE_NAME" ]; then if [ -z "$RELEASE_NAME" ]; then
RELEASE_NAME="" RELEASE_NAME=""
fi fi
# Save to temporary files for proper handling # Save to temporary files for proper handling
echo -n "$RELEASE_BODY" > /tmp/release_body.txt echo -n "$RELEASE_BODY" > /tmp/release_body.txt
echo -n "$RELEASE_NAME" > /tmp/release_name.txt echo -n "$RELEASE_NAME" > /tmp/release_name.txt
# Use jq to escape JSON strings # Use jq to escape JSON strings
RELEASE_BODY_JSON=$(jq -Rs . < /tmp/release_body.txt) RELEASE_BODY_JSON=$(jq -Rs . < /tmp/release_body.txt)
RELEASE_NAME_JSON=$(jq -Rs . < /tmp/release_name.txt) RELEASE_NAME_JSON=$(jq -Rs . < /tmp/release_name.txt)
echo "RELEASE_BODY=${RELEASE_BODY_JSON}" >> $GITHUB_OUTPUT echo "RELEASE_BODY=${RELEASE_BODY_JSON}" >> $GITHUB_OUTPUT
echo "RELEASE_NAME=${RELEASE_NAME_JSON}" >> $GITHUB_OUTPUT echo "RELEASE_NAME=${RELEASE_NAME_JSON}" >> $GITHUB_OUTPUT
@@ -85,7 +85,7 @@ jobs:
else else
DOWNLOADS_JSON="" DOWNLOADS_JSON=""
fi fi
# If downloads is empty, use default structure # If downloads is empty, use default structure
if [ -z "$DOWNLOADS_JSON" ]; then if [ -z "$DOWNLOADS_JSON" ]; then
DOWNLOADS_JSON=$(cat << DOWNLOADS_EOF DOWNLOADS_JSON=$(cat << DOWNLOADS_EOF
@@ -94,6 +94,10 @@ jobs:
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe", "url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe" "filename": "crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe"
}, },
"windows-x64-portable": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip",
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip"
},
"macos-x64": { "macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg", "url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg" "filename": "crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg"
@@ -114,7 +118,7 @@ jobs:
DOWNLOADS_EOF DOWNLOADS_EOF
) )
fi fi
# Generate version.json using cat and heredoc # Generate version.json using cat and heredoc
cat > version.json << EOF cat > version.json << EOF
{ {
@@ -126,7 +130,7 @@ jobs:
"downloads": ${DOWNLOADS_JSON} "downloads": ${DOWNLOADS_JSON}
} }
EOF EOF
cat version.json cat version.json
- name: Upload version.json to server - name: Upload version.json to server
@@ -137,4 +141,4 @@ jobs:
remote_path: /var/www/html/version/ remote_path: /var/www/html/version/
remote_host: ${{ secrets.SERVER_HOST }} remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }} remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }} remote_key: ${{ secrets.SERVER_KEY }}

1
.gitignore vendored
View File

@@ -1,7 +1,6 @@
# Xmake cache # Xmake cache
.xmake/ .xmake/
build/ build/
certs/
# MacOS Cache # MacOS Cache
.DS_Store .DS_Store

View File

@@ -181,7 +181,7 @@ sudo docker run -d \
-e MAX_PORT=xxxxx \ -e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \ -v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \ -v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.3 crossdesk/crossdesk-server:v1.1.6
``` ```
上述命令中,用户需注意的参数如下: 上述命令中,用户需注意的参数如下:
@@ -208,13 +208,13 @@ sudo docker run -d \
-e MAX_PORT=60000 \ -e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \ -v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \ -v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.3 crossdesk/crossdesk-server:v1.1.6
``` ```
**注意** **注意**
- **服务器需开放端口COTURN_PORT/udpCOTURN_PORT/tcpMIN_PORT-MAX_PORT/udpCROSSDESK_SERVER_PORT/tcp。** - **服务器需开放端口COTURN_PORT/udpCOTURN_PORT/tcpMIN_PORT-MAX_PORT/udpCROSSDESK_SERVER_PORT/tcp。**
- 如果不挂载 volume容器删除后数据会丢失 - 如果不挂载 volume容器删除后数据会丢失
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下 - 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下 - 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下 - 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
@@ -232,16 +232,30 @@ sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
### 客户端 ### 客户端
1. 点击右上角设置进入设置页面。<br><br> 1. 点击右上角设置进入设置页面。<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br> <img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. 点击点击`自托管服务器配置`按钮。<br><br> 2. 点击`自托管服务器配置`按钮。<br><br>
<img width="600" height="140" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br> <img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**)。<br><br> 3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><br><br>
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
4. 后续如果自托管服务器被重置或因其他原因导致证书更换,可以点击`重置证书指纹`按钮重置客户端保存的证书指纹。<br><br> 5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><br><br>
Windows 平台使用**管理员权限**打开 PowerShell 执行
```
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
Linux
```
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
macOS
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### Web 客户端 ### Web 客户端
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。 详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。

View File

@@ -189,7 +189,7 @@ sudo docker run -d \
-e MAX_PORT=xxxxx \ -e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \ -v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \ -v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.3 crossdesk/crossdesk-server:v1.1.6
``` ```
The parameters you need to pay attention to are as follows: The parameters you need to pay attention to are as follows:
@@ -216,13 +216,13 @@ sudo docker run -d \
-e MAX_PORT=60000 \ -e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \ -v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \ -v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.3 crossdesk/crossdesk-server:v1.1.6
``` ```
**Notes** **Notes**
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORTMAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.** - **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORTMAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
- If you dont mount volumes, all data will be lost when the container is removed. - If you dont mount volumes, all data will be lost when the container is removed.
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`. - Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`.As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`. - The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
- Log files will be created and stored at `/var/log/crossdesk/`. - Log files will be created and stored at `/var/log/crossdesk/`.
@@ -243,16 +243,31 @@ Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/y
### Client Side ### Client Side
1. Click the settings icon in the top-right corner to enter the settings page.<br><br> 1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br> <img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. Click `Self-Hosted Server Configuration` button.<br><br> 2. Click `Self-Hosted Server Configuration` button.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br> <img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**).<br><br> 3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**) and click OK button.
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><br><br>
4. Check the `Self-hosted server configuration` option and click the OK button to save the settings. If the server is using a valid (official) certificate, the process ends here and the client will show that it is connected to the server.
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
On Windows, open PowerShell with **administrator privileges** and execute:
```
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
Linux
```
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
macOS
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
4. If the self-hosted server is later reset or the certificate is replaced for any reason, you can click the `Reset Certificate Fingerprint` button to clear the certificate fingerprint saved on the client.<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><br><br>
### Web Client ### Web Client
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。 See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。

View File

@@ -15,21 +15,18 @@ DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}" DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN" DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin" BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor" ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications" DESKTOP_DIR="$DEB_DIR/usr/share/applications"
rm -rf "$DEB_DIR" rm -rf "$DEB_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$CERT_SRC_DIR" "$DESKTOP_DIR" mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
cp build/linux/x86_64/release/crossdesk "$BIN_DIR/$PKG_NAME" cp build/linux/x86_64/release/crossdesk "$BIN_DIR/$PKG_NAME"
chmod +x "$BIN_DIR/$PKG_NAME" chmod +x "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME" ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
cp certs/crossdesk.cn_root.crt "$CERT_SRC_DIR/crossdesk.cn_root.crt"
for size in 16 24 32 48 64 96 128 256; do for size in 16 24 32 48 64 96 128 256; do
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps" mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
cp "icons/linux/crossdesk_${size}x${size}.png" \ cp "icons/linux/crossdesk_${size}x${size}.png" \
@@ -71,7 +68,6 @@ if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
rm -f /usr/bin/$PKG_NAME || true rm -f /usr/bin/$PKG_NAME || true
rm -f /usr/bin/$APP_NAME || true rm -f /usr/bin/$APP_NAME || true
rm -f /usr/share/applications/$PKG_NAME.desktop || true rm -f /usr/share/applications/$PKG_NAME.desktop || true
rm -rf /opt/$PKG_NAME/certs || true
for size in 16 24 32 48 64 96 128 256; do for size in 16 24 32 48 64 96 128 256; do
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done done
@@ -85,32 +81,9 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash #!/bin/bash
set -e set -e
CERT_SRC="/opt/crossdesk/certs"
CERT_FILE="crossdesk.cn_root.crt"
for user_home in /home/*; do
[ -d "$user_home" ] || continue
username=$(basename "$user_home")
config_dir="$user_home/.config/CrossDesk/certs"
target="$config_dir/$CERT_FILE"
if [ ! -f "$target" ]; then
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$target" || true
chown -R "$username:$username" "$user_home/.config/CrossDesk" || true
echo "✔ Installed cert for $username at $target"
fi
done
if [ -d "/root" ]; then
config_dir="/root/.config/CrossDesk/certs"
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$config_dir/$CERT_FILE" || true
chown -R root:root /root/.config/CrossDesk || true
fi
exit 0 exit 0
EOF EOF
chmod +x "$DEBIAN_DIR/postinst" chmod +x "$DEBIAN_DIR/postinst"
dpkg-deb --build "$DEB_DIR" dpkg-deb --build "$DEB_DIR"

View File

@@ -15,21 +15,18 @@ DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}" DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN" DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin" BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor" ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications" DESKTOP_DIR="$DEB_DIR/usr/share/applications"
rm -rf "$DEB_DIR" rm -rf "$DEB_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$CERT_SRC_DIR" "$DESKTOP_DIR" mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
cp build/linux/arm64/release/crossdesk "$BIN_DIR" cp build/linux/arm64/release/crossdesk "$BIN_DIR"
chmod +x "$BIN_DIR/$PKG_NAME" chmod +x "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME" ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
cp certs/crossdesk.cn_root.crt "$CERT_SRC_DIR/crossdesk.cn_root.crt"
for size in 16 24 32 48 64 96 128 256; do for size in 16 24 32 48 64 96 128 256; do
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps" mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
cp "icons/linux/crossdesk_${size}x${size}.png" \ cp "icons/linux/crossdesk_${size}x${size}.png" \
@@ -70,7 +67,6 @@ if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
rm -f /usr/bin/$PKG_NAME || true rm -f /usr/bin/$PKG_NAME || true
rm -f /usr/bin/$APP_NAME || true rm -f /usr/bin/$APP_NAME || true
rm -f /usr/share/applications/$PKG_NAME.desktop || true rm -f /usr/share/applications/$PKG_NAME.desktop || true
rm -rf /opt/$PKG_NAME/certs || true
for size in 16 24 32 48 64 96 128 256; do for size in 16 24 32 48 64 96 128 256; do
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done done
@@ -84,30 +80,6 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash #!/bin/bash
set -e set -e
CERT_SRC="/opt/crossdesk/certs"
CERT_FILE="crossdesk.cn_root.crt"
for user_home in /home/*; do
[ -d "$user_home" ] || continue
username=$(basename "$user_home")
config_dir="$user_home/.config/CrossDesk/certs"
target="$config_dir/$CERT_FILE"
if [ ! -f "$target" ]; then
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$target" || true
chown -R "$username:$username" "$user_home/.config/CrossDesk" || true
echo "✔ Installed cert for $username at $target"
fi
done
if [ -d "/root" ]; then
config_dir="/root/.config/CrossDesk/certs"
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$config_dir/$CERT_FILE" || true
chown -R root:root /root/.config/CrossDesk || true
fi
exit 0 exit 0
EOF EOF

View File

@@ -11,9 +11,6 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
APP_BUNDLE="${APP_NAME_UPPER}.app" APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents" CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -98,11 +95,6 @@ IDENTIFIER="cn.crossdesk.app"
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console ) USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' ) HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
# 复制证书文件
DEST="$HOME_DIR/Library/Application Support/CrossDesk/certs"
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
# 清除应用的权限授权,以便重新授权 # 清除应用的权限授权,以便重新授权
# 使用 tccutil 重置录屏权限和辅助功能权限 # 使用 tccutil 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then if command -v tccutil >/dev/null 2>&1; then
@@ -140,21 +132,45 @@ EOF
chmod +x build_pkg_scripts/postinstall chmod +x build_pkg_scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
productbuild \ productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \ --package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
"${PKG_NAME}" "${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}" echo "PKG package created: ${PKG_NAME}"
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE} rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
echo "PKG package created successfully." echo "PKG package created successfully."

View File

@@ -11,9 +11,6 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
APP_BUNDLE="${APP_NAME_UPPER}.app" APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents" CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -98,11 +95,6 @@ IDENTIFIER="cn.crossdesk.app"
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console ) USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' ) HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
# 复制证书文件
DEST="$HOME_DIR/Library/Application Support/CrossDesk/certs"
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
# 清除应用的权限授权,以便重新授权 # 清除应用的权限授权,以便重新授权
# 使用 tccutil 重置录屏权限和辅助功能权限 # 使用 tccutil 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then if command -v tccutil >/dev/null 2>&1; then
@@ -140,21 +132,45 @@ EOF
chmod +x build_pkg_scripts/postinstall chmod +x build_pkg_scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
productbuild \ productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \ --package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
"${PKG_NAME}" "${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}" echo "PKG package created: ${PKG_NAME}"
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE} rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
echo "PKG package created successfully." echo "PKG package created successfully."

View File

@@ -0,0 +1,2 @@
// Application icon (IDI_ICON1 = 1, which is the default app icon resource ID)
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"

View File

@@ -12,9 +12,6 @@
; Installer icon path ; Installer icon path
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico" !define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
; Certificate path
!define CERT_FILE "${__FILEDIR__}\..\..\certs\crossdesk.cn_root.crt"
; Compression settings ; Compression settings
SetCompressor /FINAL lzma SetCompressor /FINAL lzma
@@ -49,7 +46,7 @@ ShowInstDetails show
Section "MainSection" Section "MainSection"
; Check if CrossDesk is running ; Check if CrossDesk is running
StrCpy $1 "crossdesk.exe" StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1" nsProcess::_FindProcess "$1"
Pop $R0 Pop $R0
@@ -75,10 +72,7 @@ installApp:
SetOverwrite ifnewer SetOverwrite ifnewer
; Main application executable path ; Main application executable path
File /oname=crossdesk.exe "..\..\build\windows\x64\release\crossdesk.exe" File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
; Copy icon file to installation directory
File "${MUI_ICON}"
; Write uninstall information ; Write uninstall information
WriteUninstaller "$INSTDIR\uninstall.exe" WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -88,33 +82,23 @@ installApp:
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\crossdesk.ico" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\CrossDesk.exe"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoRepair" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoRepair" 1
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR" WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR"
SectionEnd SectionEnd
; After installation
Section -Post
ExecWait '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\mt.exe" -manifest "$INSTDIR\crossdesk.manifest" -outputresource:"$INSTDIR\crossdesk.exe";1'
SectionEnd
Section "Cert"
SetOutPath "$APPDATA\CrossDesk\certs"
File /r "${CERT_FILE}"
SectionEnd
Section -AdditionalIcons Section -AdditionalIcons
; Desktop shortcut ; Desktop shortcut
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico" CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
; Start menu shortcut ; Start menu shortcut
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico" CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
SectionEnd SectionEnd
Section "Uninstall" Section "Uninstall"
; Check if CrossDesk is running ; Check if CrossDesk is running
StrCpy $1 "crossdesk.exe" StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1" nsProcess::_FindProcess "$1"
Pop $R0 Pop $R0
@@ -137,7 +121,7 @@ cancelUninstall:
uninstallApp: uninstallApp:
; Delete main executable and uninstaller ; Delete main executable and uninstaller
Delete "$INSTDIR\crossdesk.exe" Delete "$INSTDIR\CrossDesk.exe"
Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\uninstall.exe"
; Recursively delete installation directory ; Recursively delete installation directory
@@ -160,5 +144,5 @@ SectionEnd
; ------ Functions ------ ; ------ Functions ------
Function LaunchApp Function LaunchApp
Exec "$INSTDIR\crossdesk.exe" Exec "$INSTDIR\CrossDesk.exe"
FunctionEnd FunctionEnd

View File

@@ -34,9 +34,16 @@
#endif #endif
#ifndef _WIN32 #ifndef _WIN32
Daemon* Daemon::instance_ = nullptr; volatile std::sig_atomic_t Daemon::stop_requested_ = 0;
#endif #endif
namespace {
constexpr int kRestartDelayMs = 1000;
#ifndef _WIN32
constexpr int kWaitPollIntervalMs = 200;
#endif
} // namespace
// get executable file path // get executable file path
static std::string GetExecutablePath() { static std::string GetExecutablePath() {
#ifdef _WIN32 #ifdef _WIN32
@@ -66,33 +73,35 @@ static std::string GetExecutablePath() {
return ""; return "";
} }
Daemon::Daemon(const std::string& name) Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
: name_(name)
#ifdef _WIN32 void Daemon::stop() {
, running_.store(false);
running_(false) #ifndef _WIN32
#else stop_requested_ = 1;
,
running_(true)
#endif #endif
{
} }
void Daemon::stop() { running_ = false; } bool Daemon::isRunning() const {
#ifndef _WIN32
bool Daemon::isRunning() const { return running_; } return running_.load() && (stop_requested_ == 0);
#else
return running_.load();
#endif
}
bool Daemon::start(MainLoopFunc loop) { bool Daemon::start(MainLoopFunc loop) {
#ifdef _WIN32 #ifdef _WIN32
running_ = true; running_.store(true);
return runWithRestart(loop); return runWithRestart(loop);
#elif __APPLE__ #elif __APPLE__
// macOS: Use child process monitoring (like Windows) to preserve GUI // macOS: Use child process monitoring (like Windows) to preserve GUI
running_ = true; stop_requested_ = 0;
running_.store(true);
return runWithRestart(loop); return runWithRestart(loop);
#else #else
// linux: Daemonize first, then run with restart monitoring // linux: Daemonize first, then run with restart monitoring
instance_ = this; stop_requested_ = 0;
// check if running from terminal before fork // check if running from terminal before fork
bool from_terminal = bool from_terminal =
@@ -134,29 +143,13 @@ bool Daemon::start(MainLoopFunc loop) {
} }
// set up signal handlers // set up signal handlers
signal(SIGTERM, [](int) { signal(SIGTERM, [](int) { stop_requested_ = 1; });
if (instance_) instance_->stop(); signal(SIGINT, [](int) { stop_requested_ = 1; });
});
signal(SIGINT, [](int) {
if (instance_) instance_->stop();
});
// ignore SIGPIPE // ignore SIGPIPE
signal(SIGPIPE, SIG_IGN); signal(SIGPIPE, SIG_IGN);
// set up SIGCHLD handler to reap zombie processes running_.store(true);
struct sigaction sa_chld;
sa_chld.sa_handler = [](int) {
// reap zombie processes
while (waitpid(-1, nullptr, WNOHANG) > 0) {
// continue until no more zombie children
}
};
sigemptyset(&sa_chld.sa_mask);
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa_chld, nullptr);
running_ = true;
return runWithRestart(loop); return runWithRestart(loop);
#endif #endif
} }
@@ -204,8 +197,7 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
restart_count++; restart_count++;
std::cerr << "Exception caught, restarting... (attempt " std::cerr << "Exception caught, restarting... (attempt "
<< restart_count << ")" << std::endl; << restart_count << ")" << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
} }
} }
return true; return true;
@@ -237,27 +229,41 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
if (!success) { if (!success) {
std::cerr << "Failed to create child process, error: " << GetLastError() std::cerr << "Failed to create child process, error: " << GetLastError()
<< std::endl; << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++; restart_count++;
continue; continue;
} }
while (isRunning()) {
DWORD wait_result = WaitForSingleObject(pi.hProcess, 200);
if (wait_result == WAIT_OBJECT_0) {
break;
}
if (wait_result == WAIT_FAILED) {
std::cerr << "Failed waiting child process, error: " << GetLastError()
<< std::endl;
break;
}
}
if (!isRunning()) {
TerminateProcess(pi.hProcess, 1);
WaitForSingleObject(pi.hProcess, 3000);
}
DWORD exit_code = 0; DWORD exit_code = 0;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code); GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess); CloseHandle(pi.hProcess);
CloseHandle(pi.hThread); CloseHandle(pi.hThread);
if (exit_code == 0) { if (!isRunning() || exit_code == 0) {
break; // normal exit break; // normal exit
} }
restart_count++; restart_count++;
std::cerr << "Child process exited with code " << exit_code std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")" << ", restarting... (attempt " << restart_count << ")"
<< std::endl; << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
#else #else
// linux: use fork + exec to create child process // linux: use fork + exec to create child process
pid_t pid = fork(); pid_t pid = fork();
@@ -266,21 +272,39 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
_exit(1); // exec failed _exit(1); // exec failed
} else if (pid > 0) { } else if (pid > 0) {
int status = 0; int status = 0;
pid_t waited_pid = waitpid(pid, &status, 0); pid_t waited_pid = -1;
while (isRunning()) {
waited_pid = waitpid(pid, &status, WNOHANG);
if (waited_pid == pid) {
break;
}
if (waited_pid < 0 && errno != EINTR) {
break;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(kWaitPollIntervalMs));
}
if (!isRunning() && waited_pid != pid) {
kill(pid, SIGTERM);
waited_pid = waitpid(pid, &status, 0);
}
if (waited_pid < 0) { if (waited_pid < 0) {
if (!isRunning()) {
break;
}
restart_count++; restart_count++;
std::cerr << "waitpid failed, errno: " << errno std::cerr << "waitpid failed, errno: " << errno
<< ", restarting... (attempt " << restart_count << ")" << ", restarting... (attempt " << restart_count << ")"
<< std::endl; << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
continue; continue;
} }
if (WIFEXITED(status)) { if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status); int exit_code = WEXITSTATUS(status);
if (exit_code == 0) { if (!isRunning() || exit_code == 0) {
break; // normal exit break; // normal exit
} }
restart_count++; restart_count++;
@@ -288,6 +312,9 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
<< ", restarting... (attempt " << restart_count << ")" << ", restarting... (attempt " << restart_count << ")"
<< std::endl; << std::endl;
} else if (WIFSIGNALED(status)) { } else if (WIFSIGNALED(status)) {
if (!isRunning()) {
break;
}
restart_count++; restart_count++;
std::cerr << "Child process crashed with signal " << WTERMSIG(status) std::cerr << "Child process crashed with signal " << WTERMSIG(status)
<< ", restarting... (attempt " << restart_count << ")" << ", restarting... (attempt " << restart_count << ")"
@@ -298,12 +325,10 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
"(attempt " "(attempt "
<< restart_count << ")" << std::endl; << restart_count << ")" << std::endl;
} }
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
} else { } else {
std::cerr << "Failed to fork child process" << std::endl; std::cerr << "Failed to fork child process" << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++; restart_count++;
} }
#endif #endif

View File

@@ -7,11 +7,11 @@
#ifndef _DAEMON_H_ #ifndef _DAEMON_H_
#define _DAEMON_H_ #define _DAEMON_H_
#include <atomic>
#include <csignal>
#include <functional> #include <functional>
#include <string> #include <string>
#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000
class Daemon { class Daemon {
public: public:
using MainLoopFunc = std::function<void()>; using MainLoopFunc = std::function<void()>;
@@ -28,12 +28,10 @@ class Daemon {
std::string name_; std::string name_;
bool runWithRestart(MainLoopFunc loop); bool runWithRestart(MainLoopFunc loop);
#ifdef _WIN32 #ifndef _WIN32
bool running_; static volatile std::sig_atomic_t stop_requested_;
#else
static Daemon* instance_;
volatile bool running_;
#endif #endif
std::atomic<bool> running_;
}; };
#endif #endif

View File

@@ -35,11 +35,8 @@ int main(int argc, char* argv[]) {
bool enable_daemon = false; bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk"); auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) { if (path_manager) {
std::string cert_path =
(path_manager->GetCertPath() / "crossdesk.cn_root.crt").string();
std::string cache_path = path_manager->GetCachePath().string(); std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini", crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
cert_path);
enable_daemon = config_center.IsEnableDaemon(); enable_daemon = config_center.IsEnableDaemon();
} }

View File

@@ -108,7 +108,7 @@ std::string GetHostName() {
#ifdef _WIN32 #ifdef _WIN32
WSADATA wsaData; WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed." << std::endl; LOG_ERROR("WSAStartup failed");
return ""; return "";
} }
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) { if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {

View File

@@ -0,0 +1,63 @@
#include "rounded_corner_button.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled,
ImU32 normal_col, ImU32 hover_col, ImU32 active_col,
ImU32 border_col) {
ImGuiWindow* current_window = ImGui::GetCurrentWindow();
if (current_window->SkipItems) return false;
const ImGuiStyle& style = ImGui::GetStyle();
ImGuiID button_id = current_window->GetID(label);
ImVec2 cursor_pos = current_window->DC.CursorPos;
ImVec2 button_size = ImGui::CalcItemSize(size, 0.0f, 0.0f);
ImRect button_rect(cursor_pos, ImVec2(cursor_pos.x + button_size.x,
cursor_pos.y + button_size.y));
ImGui::ItemSize(button_rect);
if (!ImGui::ItemAdd(button_rect, button_id)) return false;
bool is_hovered = false, is_held = false;
bool is_pressed = false;
if (enabled) {
is_pressed =
ImGui::ButtonBehavior(button_rect, button_id, &is_hovered, &is_held);
}
if (normal_col == 0) normal_col = ImGui::GetColorU32(ImGuiCol_Button);
if (hover_col == 0) hover_col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
if (active_col == 0) active_col = ImGui::GetColorU32(ImGuiCol_ButtonActive);
if (border_col == 0) border_col = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 fill_color = normal_col;
if (is_held && is_hovered)
fill_color = active_col;
else if (is_hovered)
fill_color = hover_col;
if (!enabled) fill_color = IM_COL32(120, 120, 120, 180);
ImDrawList* window_draw_list = ImGui::GetWindowDrawList();
window_draw_list->AddRectFilled(button_rect.Min, button_rect.Max, fill_color,
rounding, round_flags);
if (style.FrameBorderSize > 0.0f) {
window_draw_list->AddRect(button_rect.Min, button_rect.Max, border_col,
rounding, round_flags, style.FrameBorderSize);
}
ImU32 text_color =
ImGui::GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const char* label_end = ImGui::FindRenderedTextEnd(label);
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(text_color));
ImGui::RenderTextClipped(button_rect.Min, button_rect.Max, label, label_end,
nullptr, ImVec2(0.5f, 0.5f), &button_rect);
ImGui::PopStyleColor();
return is_pressed;
}
} // namespace crossdesk

View File

@@ -0,0 +1,20 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _ROUNDED_CORNER_BUTTON_H_
#define _ROUNDED_CORNER_BUTTON_H_
#include "imgui.h"
#include "imgui_internal.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled = true,
ImU32 normal_col = 0, ImU32 hover_col = 0,
ImU32 active_col = 0, ImU32 border_col = 0);
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,25 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-01-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOW_UTIL_MAC_H_
#define _WINDOW_UTIL_MAC_H_
struct SDL_Window;
namespace crossdesk {
// Best-effort: keep an SDL window above normal windows on macOS.
// No-op on non-macOS builds.
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
// Best-effort: exclude an SDL window from the Window menu and window cycling.
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
// affects the Window menu and Cmd-` window cycling.
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,64 @@
#include "window_util_mac.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
namespace crossdesk {
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
if (!window) {
return nil;
}
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
return nil;
#else
SDL_PropertiesID props = SDL_GetWindowProperties(window);
void* cocoa_window_ptr =
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
if (!cocoa_window_ptr) {
return nil;
}
return (__bridge NSWindow*)cocoa_window_ptr;
#endif
}
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
NSWindow* ns_window = GetNSWindowFromSDL(window);
if (!ns_window) {
(void)always_on_top;
return;
}
// Keep above normal windows.
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
[ns_window setLevel:level];
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
[ns_window setCollectionBehavior:behavior];
}
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
NSWindow* ns_window = GetNSWindowFromSDL(window);
if (!ns_window) {
(void)excluded;
return;
}
[ns_window setExcludedFromWindowsMenu:excluded];
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
behavior |= NSWindowCollectionBehaviorTransient;
[ns_window setCollectionBehavior:behavior];
}
} // namespace crossdesk
#endif // __APPLE__

View File

@@ -5,11 +5,8 @@
namespace crossdesk { namespace crossdesk {
ConfigCenter::ConfigCenter(const std::string& config_path, ConfigCenter::ConfigCenter(const std::string& config_path)
const std::string& cert_file_path) : config_path_(config_path) {
: config_path_(config_path),
cert_file_path_(cert_file_path),
cert_file_path_default_(cert_file_path) {
ini_.SetUnicode(true); ini_.SetUnicode(true);
Load(); Load();
} }
@@ -23,8 +20,14 @@ int ConfigCenter::Load() {
return -1; return -1;
} }
language_ = static_cast<LANGUAGE>( const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_))); ini_.GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
language_ = LANGUAGE::ENGLISH;
} else {
language_ = static_cast<LANGUAGE>(language_value);
}
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue( video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
section_, "video_quality", static_cast<long>(video_quality_))); section_, "video_quality", static_cast<long>(video_quality_)));
@@ -70,71 +73,6 @@ int ConfigCenter::Load() {
} else { } else {
coturn_server_port_ = 0; coturn_server_port_ = 0;
} }
const char* cert_file_path_value =
ini_.GetValue(section_, "cert_file_path", nullptr);
if (cert_file_path_value != nullptr && strlen(cert_file_path_value) > 0) {
cert_file_path_ = cert_file_path_value;
} else {
cert_file_path_ = "";
}
const char* cert_fingerprint_value =
ini_.GetValue(section_, "cert_fingerprint", nullptr);
if (cert_fingerprint_value != nullptr && strlen(cert_fingerprint_value) > 0) {
cert_fingerprint_ = cert_fingerprint_value;
} else {
cert_fingerprint_ = "";
}
const char* cert_fingerprint_server_host_value =
ini_.GetValue(section_, "cert_fingerprint_server_host", nullptr);
if (cert_fingerprint_server_host_value != nullptr &&
strlen(cert_fingerprint_server_host_value) > 0) {
cert_fingerprint_server_host_ = cert_fingerprint_server_host_value;
} else {
cert_fingerprint_server_host_ = "";
}
const char* default_cert_fingerprint_value =
ini_.GetValue(section_, "default_cert_fingerprint", nullptr);
if (default_cert_fingerprint_value != nullptr &&
strlen(default_cert_fingerprint_value) > 0) {
default_cert_fingerprint_ = default_cert_fingerprint_value;
} else {
default_cert_fingerprint_ = "";
}
const char* default_cert_fingerprint_server_host_value =
ini_.GetValue(section_, "default_cert_fingerprint_server_host", nullptr);
if (default_cert_fingerprint_server_host_value != nullptr &&
strlen(default_cert_fingerprint_server_host_value) > 0) {
default_cert_fingerprint_server_host_ =
default_cert_fingerprint_server_host_value;
} else {
default_cert_fingerprint_server_host_ = "";
}
if (enable_self_hosted_ && !cert_fingerprint_.empty() &&
!cert_fingerprint_server_host_.empty() &&
signal_server_host_ != cert_fingerprint_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
cert_fingerprint_server_host_, signal_server_host_);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
ini_.SaveFile(config_path_.c_str());
}
if (!enable_self_hosted_ && !default_cert_fingerprint_.empty() &&
!default_cert_fingerprint_server_host_.empty() &&
signal_server_host_default_ != default_cert_fingerprint_server_host_) {
LOG_INFO(
"Default server IP changed from {} to {}, clearing old fingerprint",
default_cert_fingerprint_server_host_, signal_server_host_default_);
default_cert_fingerprint_.clear();
default_cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "default_cert_fingerprint", false);
ini_.Delete(section_, "default_cert_fingerprint_server_host", false);
ini_.SaveFile(config_path_.c_str());
}
enable_autostart_ = enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_); ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
@@ -142,6 +80,15 @@ int ConfigCenter::Load() {
enable_minimize_to_tray_ = ini_.GetBoolValue( enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_); section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
if (file_transfer_save_path_value != nullptr &&
strlen(file_transfer_save_path_value) > 0) {
file_transfer_save_path_ = file_transfer_save_path_value;
} else {
file_transfer_save_path_ = "";
}
return 0; return 0;
} }
@@ -165,19 +112,6 @@ int ConfigCenter::Save() {
static_cast<long>(signal_server_port_)); static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port", ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_)); static_cast<long>(coturn_server_port_));
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
if (!cert_fingerprint_.empty()) {
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
}
}
if (!default_cert_fingerprint_.empty()) {
ini_.SetValue(section_, "default_cert_fingerprint",
default_cert_fingerprint_.c_str());
ini_.SetValue(section_, "default_cert_fingerprint_server_host",
default_cert_fingerprint_server_host_.c_str());
} }
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_); ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
@@ -185,6 +119,9 @@ int ConfigCenter::Save() {
ini_.SetBoolValue(section_, "enable_minimize_to_tray", ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_); enable_minimize_to_tray_);
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str()); SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) { if (rc < 0) {
return -1; return -1;
@@ -270,15 +207,6 @@ int ConfigCenter::SetSrtp(bool enable_srtp) {
} }
int ConfigCenter::SetServerHost(const std::string& signal_server_host) { int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
if (enable_self_hosted_ && !cert_fingerprint_.empty() &&
signal_server_host != signal_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
signal_server_host_, signal_server_host);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
}
signal_server_host_ = signal_server_host; signal_server_host_ = signal_server_host;
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str()); ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str()); SI_Error rc = ini_.SaveFile(config_path_.c_str());
@@ -310,67 +238,6 @@ int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
return 0; return 0;
} }
int ConfigCenter::SetCertFilePath(const std::string& cert_file_path) {
cert_file_path_ = cert_file_path;
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetCertFingerprint(const std::string& fingerprint) {
cert_fingerprint_ = fingerprint;
cert_fingerprint_server_host_ = signal_server_host_;
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetDefaultCertFingerprint(const std::string& fingerprint) {
default_cert_fingerprint_ = fingerprint;
default_cert_fingerprint_server_host_ = signal_server_host_default_;
ini_.SetValue(section_, "default_cert_fingerprint",
default_cert_fingerprint_.c_str());
ini_.SetValue(section_, "default_cert_fingerprint_server_host",
default_cert_fingerprint_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::ClearCertFingerprint() {
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::ClearDefaultCertFingerprint() {
default_cert_fingerprint_.clear();
default_cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "default_cert_fingerprint", false);
ini_.Delete(section_, "default_cert_fingerprint_server_host", false);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) { int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
enable_self_hosted_ = enable_self_hosted; enable_self_hosted_ = enable_self_hosted;
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_); ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
@@ -397,45 +264,12 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
coturn_server_port_ = static_cast<int>( coturn_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "coturn_server_port", 0)); ini_.GetLongValue(section_, "coturn_server_port", 0));
} }
const char* cert_file_path_value =
ini_.GetValue(section_, "cert_file_path", nullptr);
if (cert_file_path_value != nullptr && strlen(cert_file_path_value) > 0) {
cert_file_path_ = cert_file_path_value;
}
const char* cert_fingerprint_value =
ini_.GetValue(section_, "cert_fingerprint", nullptr);
if (cert_fingerprint_value != nullptr &&
strlen(cert_fingerprint_value) > 0) {
cert_fingerprint_ = cert_fingerprint_value;
}
const char* cert_fingerprint_server_host_value =
ini_.GetValue(section_, "cert_fingerprint_server_host", nullptr);
if (cert_fingerprint_server_host_value != nullptr &&
strlen(cert_fingerprint_server_host_value) > 0) {
cert_fingerprint_server_host_ = cert_fingerprint_server_host_value;
}
if (!cert_fingerprint_.empty() && !cert_fingerprint_server_host_.empty() &&
signal_server_host_ != cert_fingerprint_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
cert_fingerprint_server_host_, signal_server_host_);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
}
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str()); ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port", ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_)); static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port", ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_)); static_cast<long>(coturn_server_port_));
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
if (!cert_fingerprint_.empty()) {
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
}
} }
SI_Error rc = ini_.SaveFile(config_path_.c_str()); SI_Error rc = ini_.SaveFile(config_path_.c_str());
@@ -523,16 +357,6 @@ int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; } int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
std::string ConfigCenter::GetCertFilePath() const { return cert_file_path_; }
std::string ConfigCenter::GetCertFingerprint() const {
return cert_fingerprint_;
}
std::string ConfigCenter::GetDefaultCertFingerprint() const {
return default_cert_fingerprint_;
}
std::string ConfigCenter::GetDefaultServerHost() const { std::string ConfigCenter::GetDefaultServerHost() const {
return signal_server_host_default_; return signal_server_host_default_;
} }
@@ -545,10 +369,6 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
return coturn_server_port_default_; return coturn_server_port_default_;
} }
std::string ConfigCenter::GetDefaultCertFilePath() const {
return cert_file_path_default_;
}
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; } bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; } bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
@@ -556,4 +376,19 @@ bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; } bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; } bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
} // namespace crossdesk
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
std::string ConfigCenter::GetFileTransferSavePath() const {
return file_transfer_save_path_;
}
} // namespace crossdesk

View File

@@ -15,15 +15,13 @@ namespace crossdesk {
class ConfigCenter { class ConfigCenter {
public: public:
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1 }; enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 }; enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 }; enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 }; enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
public: public:
explicit ConfigCenter( explicit ConfigCenter(const std::string& config_path = "config.ini");
const std::string& config_path = "config.ini",
const std::string& cert_file_path = "crossdesk.cn_root.crt");
~ConfigCenter(); ~ConfigCenter();
// write config // write config
@@ -37,15 +35,11 @@ class ConfigCenter {
int SetServerHost(const std::string& signal_server_host); int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port); int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port); int SetCoturnServerPort(int coturn_server_port);
int SetCertFilePath(const std::string& cert_file_path);
int SetCertFingerprint(const std::string& fingerprint);
int SetDefaultCertFingerprint(const std::string& fingerprint);
int ClearCertFingerprint();
int ClearDefaultCertFingerprint();
int SetSelfHosted(bool enable_self_hosted); int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray); int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart); int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon); int SetDaemon(bool enable_daemon);
int SetFileTransferSavePath(const std::string& path);
// read config // read config
@@ -59,17 +53,14 @@ class ConfigCenter {
std::string GetSignalServerHost() const; std::string GetSignalServerHost() const;
int GetSignalServerPort() const; int GetSignalServerPort() const;
int GetCoturnServerPort() const; int GetCoturnServerPort() const;
std::string GetCertFilePath() const;
std::string GetCertFingerprint() const;
std::string GetDefaultCertFingerprint() const;
std::string GetDefaultServerHost() const; std::string GetDefaultServerHost() const;
int GetDefaultSignalServerPort() const; int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const; int GetDefaultCoturnServerPort() const;
std::string GetDefaultCertFilePath() const;
bool IsSelfHosted() const; bool IsSelfHosted() const;
bool IsMinimizeToTray() const; bool IsMinimizeToTray() const;
bool IsEnableAutostart() const; bool IsEnableAutostart() const;
bool IsEnableDaemon() const; bool IsEnableDaemon() const;
std::string GetFileTransferSavePath() const;
int Load(); int Load();
int Save(); int Save();
@@ -80,7 +71,7 @@ class ConfigCenter {
const char* section_ = "Settings"; const char* section_ = "Settings";
LANGUAGE language_ = LANGUAGE::CHINESE; LANGUAGE language_ = LANGUAGE::CHINESE;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::MEDIUM; VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60; VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264; VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false; bool hardware_video_codec_ = false;
@@ -92,16 +83,11 @@ class ConfigCenter {
int server_port_default_ = 9099; int server_port_default_ = 9099;
int coturn_server_port_ = 0; int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478; int coturn_server_port_default_ = 3478;
std::string cert_file_path_ = "";
std::string cert_file_path_default_ = "";
std::string cert_fingerprint_ = "";
std::string cert_fingerprint_server_host_ = "";
std::string default_cert_fingerprint_ = "";
std::string default_cert_fingerprint_server_host_ = "";
bool enable_self_hosted_ = false; bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false; bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false; bool enable_autostart_ = false;
bool enable_daemon_ = false; bool enable_daemon_ = false;
std::string file_transfer_save_path_ = "";
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif

View File

@@ -1,5 +1,8 @@
#include "keyboard_capturer.h" #include "keyboard_capturer.h"
#include <errno.h>
#include <poll.h>
#include "keyboard_converter.h" #include "keyboard_converter.h"
#include "rd_log.h" #include "rd_log.h"
@@ -10,7 +13,7 @@ static void* g_user_ptr = nullptr;
static int KeyboardEventHandler(Display* display, XEvent* event) { static int KeyboardEventHandler(Display* display, XEvent* event) {
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) { if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
KeySym keySym = XKeycodeToKeysym(display, event->xkey.keycode, 0); KeySym keySym = XLookupKeysym(&event->xkey, 0);
int key_code = XKeysymToKeycode(display, keySym); int key_code = XKeysymToKeycode(display, keySym);
bool is_key_down = (event->xkey.type == KeyPress); bool is_key_down = (event->xkey.type == KeyPress);
@@ -21,7 +24,9 @@ static int KeyboardEventHandler(Display* display, XEvent* event) {
return 0; return 0;
} }
KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) { KeyboardCapturer::KeyboardCapturer()
: display_(nullptr), root_(0), running_(false) {
XInitThreads();
display_ = XOpenDisplay(nullptr); display_ = XOpenDisplay(nullptr);
if (!display_) { if (!display_) {
LOG_ERROR("Failed to open X display."); LOG_ERROR("Failed to open X display.");
@@ -29,35 +34,87 @@ KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
} }
KeyboardCapturer::~KeyboardCapturer() { KeyboardCapturer::~KeyboardCapturer() {
Unhook();
if (display_) { if (display_) {
XCloseDisplay(display_); XCloseDisplay(display_);
display_ = nullptr;
} }
} }
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) { int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (!display_) {
LOG_ERROR("Display not initialized.");
return -1;
}
g_on_key_action = on_key_action; g_on_key_action = on_key_action;
g_user_ptr = user_ptr; g_user_ptr = user_ptr;
XSelectInput(display_, DefaultRootWindow(display_), if (running_) {
KeyPressMask | KeyReleaseMask); return 0;
while (running_) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
} }
root_ = DefaultRootWindow(display_);
XSelectInput(display_, root_, KeyPressMask | KeyReleaseMask);
XFlush(display_);
running_ = true;
const int x11_fd = ConnectionNumber(display_);
event_thread_ = std::thread([this, x11_fd]() {
while (running_) {
while (running_ && XPending(display_) > 0) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
}
if (!running_) {
break;
}
struct pollfd pfd = {x11_fd, POLLIN, 0};
int poll_ret = poll(&pfd, 1, 50);
if (poll_ret < 0) {
if (errno == EINTR) {
continue;
}
LOG_ERROR("poll for X11 events failed.");
running_ = false;
break;
}
if (poll_ret == 0) {
continue;
}
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
LOG_ERROR("poll got invalid X11 event fd state.");
running_ = false;
break;
}
if ((pfd.revents & POLLIN) == 0) {
continue;
}
}
});
return 0; return 0;
} }
int KeyboardCapturer::Unhook() { int KeyboardCapturer::Unhook() {
running_ = false;
if (event_thread_.joinable()) {
event_thread_.join();
}
g_on_key_action = nullptr; g_on_key_action = nullptr;
g_user_ptr = nullptr; g_user_ptr = nullptr;
running_ = false; if (display_ && root_ != 0) {
XSelectInput(display_, root_, 0);
if (display_) {
XSelectInput(display_, DefaultRootWindow(display_), 0);
XFlush(display_); XFlush(display_);
} }

View File

@@ -11,6 +11,9 @@
#include <X11/extensions/XTest.h> #include <X11/extensions/XTest.h>
#include <X11/keysym.h> #include <X11/keysym.h>
#include <atomic>
#include <thread>
#include "device_controller.h" #include "device_controller.h"
namespace crossdesk { namespace crossdesk {
@@ -28,7 +31,8 @@ class KeyboardCapturer : public DeviceController {
private: private:
Display* display_; Display* display_;
Window root_; Window root_;
bool running_; std::atomic<bool> running_;
std::thread event_thread_;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,222 +1,156 @@
/* /*
* @Author: DI JUNKUN * @Author: DI JUNKUN
* @Date: 2024-05-29 * @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved. * Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/ */
#ifndef _LOCALIZATION_H_ #ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_ #define _LOCALIZATION_H_
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
#include "localization_data.h"
#if _WIN32 #if _WIN32
#include <Windows.h> #include <Windows.h>
#endif #endif
namespace crossdesk {
namespace localization {
struct LanguageOption {
std::string code;
std::string display_name;
};
namespace crossdesk { class LocalizedString {
public:
constexpr explicit LocalizedString(const char* key) : key_(key) {}
const std::string& operator[](int language_index) const;
private:
const char* key_;
};
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
static const std::vector<LanguageOption> kSupportedLanguages = {
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
{"en-US", "English"},
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
return kSupportedLanguages;
}
namespace detail {
namespace localization { inline int ClampLanguageIndex(int language_index) {
if (language_index >= 0 &&
language_index < static_cast<int>(GetSupportedLanguages().size())) {
return language_index;
}
return 0;
}
static std::vector<std::string> local_desktop = { using TranslationTable =
reinterpret_cast<const char*>(u8"本桌面"), "Local Desktop"}; std::unordered_map<std::string,
static std::vector<std::string> local_id = { std::unordered_map<std::string, std::string>>;
reinterpret_cast<const char*>(u8"本机ID"), "Local ID"};
static std::vector<std::string> local_id_copied_to_clipboard = {
reinterpret_cast<const char*>(u8"已复制到剪贴板"), "Copied to clipboard"};
static std::vector<std::string> password = {
reinterpret_cast<const char*>(u8"密码"), "Password"};
static std::vector<std::string> max_password_len = {
reinterpret_cast<const char*>(u8"最大6个字符"), "Max 6 chars"};
static std::vector<std::string> remote_desktop = { inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
reinterpret_cast<const char*>(u8"控制远程桌面"), "Control Remote Desktop"}; const TranslationRow& row) {
static std::vector<std::string> remote_id = { return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
reinterpret_cast<const char*>(u8"对端ID"), "Remote ID"}; {"en-US", row.en},
static std::vector<std::string> connect = { {"ru-RU", reinterpret_cast<const char*>(row.ru)}};
reinterpret_cast<const char*>(u8"连接"), "Connect"}; }
static std::vector<std::string> recent_connections = {
reinterpret_cast<const char*>(u8"近期连接"), "Recent Connections"};
static std::vector<std::string> disconnect = {
reinterpret_cast<const char*>(u8"断开连接"), "Disconnect"};
static std::vector<std::string> fullscreen = {
reinterpret_cast<const char*>(u8"全屏"), " Fullscreen"};
static std::vector<std::string> show_net_traffic_stats = {
reinterpret_cast<const char*>(u8"显示流量统计"), "Show Net Traffic Stats"};
static std::vector<std::string> hide_net_traffic_stats = {
reinterpret_cast<const char*>(u8"隐藏流量统计"), "Hide Net Traffic Stats"};
static std::vector<std::string> video = {
reinterpret_cast<const char*>(u8"视频"), "Video"};
static std::vector<std::string> audio = {
reinterpret_cast<const char*>(u8"音频"), "Audio"};
static std::vector<std::string> data = {reinterpret_cast<const char*>(u8"数据"),
"Data"};
static std::vector<std::string> total = {
reinterpret_cast<const char*>(u8"总计"), "Total"};
static std::vector<std::string> in = {reinterpret_cast<const char*>(u8"输入"),
"In"};
static std::vector<std::string> out = {reinterpret_cast<const char*>(u8"输出"),
"Out"};
static std::vector<std::string> loss_rate = {
reinterpret_cast<const char*>(u8"丢包率"), "Loss Rate"};
static std::vector<std::string> exit_fullscreen = {
reinterpret_cast<const char*>(u8"退出全屏"), "Exit fullscreen"};
static std::vector<std::string> control_mouse = {
reinterpret_cast<const char*>(u8"控制"), "Control"};
static std::vector<std::string> release_mouse = {
reinterpret_cast<const char*>(u8"释放"), "Release"};
static std::vector<std::string> audio_capture = {
reinterpret_cast<const char*>(u8"声音"), "Audio"};
static std::vector<std::string> mute = {
reinterpret_cast<const char*>(u8" 静音"), " Mute"};
static std::vector<std::string> settings = {
reinterpret_cast<const char*>(u8"设置"), "Settings"};
static std::vector<std::string> language = {
reinterpret_cast<const char*>(u8"语言:"), "Language:"};
static std::vector<std::string> language_zh = {
reinterpret_cast<const char*>(u8"中文"), "Chinese"};
static std::vector<std::string> language_en = {
reinterpret_cast<const char*>(u8"英文"), "English"};
static std::vector<std::string> video_quality = {
reinterpret_cast<const char*>(u8"视频质量:"), "Video Quality:"};
static std::vector<std::string> video_frame_rate = {
reinterpret_cast<const char*>(u8"画面采集帧率:"),
"Video Capture Frame Rate:"};
static std::vector<std::string> video_quality_high = {
reinterpret_cast<const char*>(u8""), "High"};
static std::vector<std::string> video_quality_medium = {
reinterpret_cast<const char*>(u8""), "Medium"};
static std::vector<std::string> video_quality_low = {
reinterpret_cast<const char*>(u8""), "Low"};
static std::vector<std::string> video_encode_format = {
reinterpret_cast<const char*>(u8"视频编码格式:"), "Video Encode Format:"};
static std::vector<std::string> av1 = {reinterpret_cast<const char*>(u8"AV1"),
"AV1"};
static std::vector<std::string> h264 = {
reinterpret_cast<const char*>(u8"H.264"), "H.264"};
static std::vector<std::string> enable_hardware_video_codec = {
reinterpret_cast<const char*>(u8"启用硬件编解码器:"),
"Enable Hardware Video Codec:"};
static std::vector<std::string> enable_turn = {
reinterpret_cast<const char*>(u8"启用中继服务:"), "Enable TURN Service:"};
static std::vector<std::string> enable_srtp = {
reinterpret_cast<const char*>(u8"启用SRTP:"), "Enable SRTP:"};
static std::vector<std::string> self_hosted_server_config = {
reinterpret_cast<const char*>(u8"自托管服务器配置"),
"Self-Hosted Server Config"};
static std::vector<std::string> self_hosted_server_settings = {
reinterpret_cast<const char*>(u8"自托管服务器设置"),
"Self-Hosted Server Settings"};
static std::vector<std::string> self_hosted_server_address = {
reinterpret_cast<const char*>(u8"服务器地址:"), "Server Address:"};
static std::vector<std::string> self_hosted_server_port = {
reinterpret_cast<const char*>(u8"信令服务端口:"), "Signal Service Port:"};
static std::vector<std::string> self_hosted_server_coturn_server_port = {
reinterpret_cast<const char*>(u8"中继服务端口:"), "Relay Service Port:"};
static std::vector<std::string> self_hosted_server_certificate_path = {
reinterpret_cast<const char*>(u8"证书文件路径:"), "Certificate File Path:"};
static std::vector<std::string> select_a_file = {
reinterpret_cast<const char*>(u8"请选择文件"), "Please select a file"};
static std::vector<std::string> reset_cert_fingerprint = {
reinterpret_cast<const char*>(u8"重置证书指纹"),
"Reset Certificate Fingerprint"};
static std::vector<std::string> ok = {reinterpret_cast<const char*>(u8"确认"),
"OK"};
static std::vector<std::string> cancel = {
reinterpret_cast<const char*>(u8"取消"), "Cancel"};
static std::vector<std::string> new_password = { inline TranslationTable BuildTranslationTable() {
reinterpret_cast<const char*>(u8"请输入六位密码:"), TranslationTable table;
"Please input a six-char password:"}; for (const auto& row : kTranslationRows) {
table[row.key] = MakeLocalizedValues(row);
}
static std::vector<std::string> input_password = { return table;
reinterpret_cast<const char*>(u8"请输入密码:"), "Please input password:"}; }
static std::vector<std::string> validate_password = {
reinterpret_cast<const char*>(u8"验证密码中..."), "Validate password ..."}; inline const TranslationTable& GetTranslationTable() {
static std::vector<std::string> reinput_password = { static const TranslationTable table = BuildTranslationTable();
reinterpret_cast<const char*>(u8"请重新输入密码"), return table;
"Please input password again"}; }
static std::vector<std::string> remember_password = { inline const std::string& GetTranslatedText(const std::string& key,
reinterpret_cast<const char*>(u8"记住密码"), "Remember password"}; int language_index) {
static const std::string kEmptyText = "";
static std::vector<std::string> signal_connected = {
reinterpret_cast<const char*>(u8"已连接服务器"), "Connected"}; const auto& table = GetTranslationTable();
static std::vector<std::string> signal_disconnected = { const auto key_it = table.find(key);
reinterpret_cast<const char*>(u8"未连接服务器"), "Disconnected"}; if (key_it == table.end()) {
return kEmptyText;
static std::vector<std::string> p2p_connected = { }
reinterpret_cast<const char*>(u8"对等连接已建立"), "P2P Connected"};
static std::vector<std::string> p2p_disconnected = { const auto& localized_values = key_it->second;
reinterpret_cast<const char*>(u8"对等连接已断开"), "P2P Disconnected"}; const std::string& language_code =
static std::vector<std::string> p2p_connecting = { GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
reinterpret_cast<const char*>(u8"正在建立对等连接..."),
"P2P Connecting ..."}; const auto exact_it = localized_values.find(language_code);
static std::vector<std::string> p2p_failed = { if (exact_it != localized_values.end()) {
reinterpret_cast<const char*>(u8"对等连接失败"), "P2P Failed"}; return exact_it->second;
static std::vector<std::string> p2p_closed = { }
reinterpret_cast<const char*>(u8"对等连接已关闭"), "P2P closed"};
const auto english_it = localized_values.find("en-US");
static std::vector<std::string> no_such_id = { if (english_it != localized_values.end()) {
reinterpret_cast<const char*>(u8"无此ID"), "No such ID"}; return english_it->second;
}
static std::vector<std::string> about = {
reinterpret_cast<const char*>(u8"关于"), "About"}; const auto chinese_it = localized_values.find("zh-CN");
static std::vector<std::string> notification = { if (chinese_it != localized_values.end()) {
reinterpret_cast<const char*>(u8"通知"), "Notification"}; return chinese_it->second;
static std::vector<std::string> new_version_available = { }
reinterpret_cast<const char*>(u8"新版本可用"), "New Version Available"};
static std::vector<std::string> version = { return kEmptyText;
reinterpret_cast<const char*>(u8"版本"), "Version"}; }
static std::vector<std::string> release_date = {
reinterpret_cast<const char*>(u8"发布日期: "), "Release Date: "}; } // namespace detail
static std::vector<std::string> access_website = {
reinterpret_cast<const char*>(u8"访问官网: "), "Access Website: "}; inline const std::string& LocalizedString::operator[](
static std::vector<std::string> update = { int language_index) const {
reinterpret_cast<const char*>(u8"更新"), "Update"}; return detail::GetTranslatedText(key_, language_index);
}
static std::vector<std::string> confirm_delete_connection = {
reinterpret_cast<const char*>(u8"确认删除此连接"), #define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
"Confirm to delete this connection"}; inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
static std::vector<std::string> enable_autostart = { #undef CROSSDESK_DECLARE_LOCALIZED_STRING
reinterpret_cast<const char*>(u8"开机自启:"), "Auto Start:"};
static std::vector<std::string> enable_daemon = { #if _WIN32
reinterpret_cast<const char*>(u8"启用守护进程:"), "Enable Daemon:"}; inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::string> takes_effect_after_restart = { static std::vector<std::wstring> cache(GetSupportedLanguages().size());
reinterpret_cast<const char*>(u8"重启后生效"), const int normalized_index = detail::ClampLanguageIndex(language_index);
"Takes effect after restart"}; std::wstring& cached_text = cache[normalized_index];
static std::vector<std::string> select_file = { if (!cached_text.empty()) {
reinterpret_cast<const char*>(u8"选择文件"), "Select File"}; return cached_text.c_str();
static std::vector<std::string> file_transfer_progress = { }
reinterpret_cast<const char*>(u8"文件传输进度"), "File Transfer Progress"};
static std::vector<std::string> queued = { const std::string& utf8_text =
reinterpret_cast<const char*>(u8"队列中"), "Queued"}; detail::GetTranslatedText("exit_program", normalized_index);
static std::vector<std::string> sending = { if (utf8_text.empty()) {
reinterpret_cast<const char*>(u8"正在传输"), "Sending"}; cached_text = L"Exit";
static std::vector<std::string> completed = { return cached_text.c_str();
reinterpret_cast<const char*>(u8"已完成"), "Completed"}; }
static std::vector<std::string> failed = {
reinterpret_cast<const char*>(u8"失败"), "Failed"}; int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
#if _WIN32 if (wide_length <= 0) {
static std::vector<std::string> minimize_to_tray = { cached_text = L"Exit";
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"), return cached_text.c_str();
"Minimize to system tray when exit:"}; }
static std::vector<LPCWSTR> exit_program = {L"退出", L"Exit"};
#endif cached_text.resize(static_cast<size_t>(wide_length - 1));
#ifdef __APPLE__ MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
static std::vector<std::string> request_permissions = { wide_length);
reinterpret_cast<const char*>(u8"权限请求"), "Request Permissions"}; return cached_text.c_str();
static std::vector<std::string> screen_recording_permission = { }
reinterpret_cast<const char*>(u8"屏幕录制权限"), #endif
"Screen Recording Permission"};
static std::vector<std::string> accessibility_permission = { } // namespace localization
reinterpret_cast<const char*>(u8"辅助功能权限"), } // namespace crossdesk
"Accessibility Permission"};
static std::vector<std::string> permission_required_message = { #endif
reinterpret_cast<const char*>(u8"该应用需要授权以下权限:"),
"The application requires the following permissions:"};
#endif
} // namespace localization
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,166 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_DATA_H_
#define _LOCALIZATION_DATA_H_
namespace crossdesk {
namespace localization {
namespace detail {
struct TranslationRow {
const char* key;
const char* zh;
const char* en;
const char* ru;
};
// 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(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", u8"Управление") \
X(release_mouse, u8"释放", "Release", u8"Освободить") \
X(audio_capture, u8"声音", "Audio", u8"Звук") \
X(mute, u8" 静音", " Mute", 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", 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[] = {
#define CROSSDESK_DECLARE_TRANSLATION_ROW(name, zh, en, ru) {#name, zh, en, ru},
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_TRANSLATION_ROW)
#undef CROSSDESK_DECLARE_TRANSLATION_ROW
};
} // namespace detail
} // namespace localization
} // namespace crossdesk
#endif

36
src/gui/device_presence.h Normal file
View File

@@ -0,0 +1,36 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-28
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_PRESENCE_H_
#define _DEVICE_PRESENCE_H_
#include <mutex>
#include <string>
#include <unordered_map>
class DevicePresence {
public:
void SetOnline(const std::string& device_id, bool online) {
std::lock_guard<std::mutex> lock(mutex_);
cache_[device_id] = online;
}
bool IsOnline(const std::string& device_id) const {
std::lock_guard<std::mutex> lock(mutex_);
return cache_.count(device_id) > 0 && cache_.at(device_id);
}
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
cache_.clear();
}
private:
std::unordered_map<std::string, bool> cache_;
mutable std::mutex mutex_;
};
#endif

View File

@@ -17,7 +17,7 @@ int Render::LocalWindow() {
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * TITLE_BAR_HEIGHT), ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("LocalDesktopWindow", ImGui::BeginChild("LocalDesktopWindow",
@@ -42,11 +42,11 @@ int Render::LocalWindow() {
ImGuiCond_Always); ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255, ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255,
242.0f / 255, 1.0f)); 242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
ImGui::BeginChild( ImGui::BeginChild(
"LocalDesktopPanel", "LocalDesktopPanel",
ImVec2(local_window_width * 0.8f, local_window_height * 0.43f), ImVec2(local_window_width * 0.8f, local_window_height * 0.43f),
ImGuiChildFlags_Border, ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus); ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -101,7 +101,7 @@ int Render::LocalWindow() {
ImGuiCol_WindowBg, ImGuiCol_WindowBg,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f - (float)time_duration)); ImVec4(1.0f, 1.0f, 1.0f, 1.0f - (float)time_duration));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConnectionStatusWindow", nullptr, ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings); ImGuiWindowFlags_NoSavedSettings);
@@ -177,10 +177,11 @@ int Render::LocalWindow() {
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f)); ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding,
window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ResetPasswordWindow", nullptr, ImGui::Begin("ResetPasswordWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
@@ -299,4 +300,4 @@ int Render::LocalWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -9,7 +9,7 @@ int Render::RecentConnectionsWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
float recent_connection_window_width = io.DisplaySize.x; float recent_connection_window_width = io.DisplaySize.x;
float recent_connection_window_height = float recent_connection_window_height =
io.DisplaySize.y * (0.46f - STATUS_BAR_HEIGHT); io.DisplaySize.y * (0.455f - STATUS_BAR_HEIGHT);
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * 0.55f), ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * 0.55f),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
@@ -17,7 +17,7 @@ int Render::RecentConnectionsWindow() {
ImGui::BeginChild( ImGui::BeginChild(
"RecentConnectionsWindow", "RecentConnectionsWindow",
ImVec2(recent_connection_window_width, recent_connection_window_height), ImVec2(recent_connection_window_width, recent_connection_window_height),
ImGuiChildFlags_Border, ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus); ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -64,7 +64,7 @@ int Render::ShowRecentConnections() {
ImGui::BeginChild( ImGui::BeginChild(
"RecentConnectionsContainer", "RecentConnectionsContainer",
ImVec2(recent_connection_panel_width, recent_connection_panel_height), ImVec2(recent_connection_panel_width, recent_connection_panel_height),
ImGuiChildFlags_Border, ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar |
@@ -122,6 +122,8 @@ int Render::ShowRecentConnections() {
it.second.remote_host_name = "unknown"; it.second.remote_host_name = "unknown";
} }
bool online = device_presence_.IsOnline(it.second.remote_id);
ImVec2 image_screen_pos = ImVec2( ImVec2 image_screen_pos = ImVec2(
ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f, ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f,
ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f); ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f);
@@ -132,6 +134,29 @@ int Render::ShowRecentConnections() {
ImGui::Image( ImGui::Image(
(ImTextureID)(intptr_t)it.second.texture, (ImTextureID)(intptr_t)it.second.texture,
ImVec2(recent_connection_image_width, recent_connection_image_height)); ImVec2(recent_connection_image_width, recent_connection_image_height));
if (ImGui::IsItemHovered()) {
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::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 // remote id display button
{ {
@@ -155,14 +180,6 @@ int Render::ShowRecentConnections() {
ImGui::Text("%s", it.second.remote_id.c_str()); ImGui::Text("%s", it.second.remote_id.c_str());
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", it.second.remote_host_name.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
} }
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
@@ -242,6 +259,9 @@ int Render::ShowRecentConnections() {
if (show_confirm_delete_connection_) { if (show_confirm_delete_connection_) {
ConfirmDeleteConnection(); ConfirmDeleteConnection();
} }
if (show_offline_warning_window_) {
OfflineWarningWindow();
}
return 0; return 0;
} }
@@ -253,10 +273,10 @@ int Render::ConfirmDeleteConnection() {
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f)); ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConfirmDeleteConnectionWindow", nullptr, ImGui::Begin("ConfirmDeleteConnectionWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
@@ -299,4 +319,45 @@ int Render::ConfirmDeleteConnection() {
ImGui::PopStyleVar(); ImGui::PopStyleVar();
return 0; return 0;
} }
} // namespace crossdesk
int Render::OfflineWarningWindow() {
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("OfflineWarningWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
ImGui::SetCursorPosX(window_width * 0.43f);
ImGui::SetCursorPosY(window_height * 0.67f);
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_offline_warning_window_ = false;
}
auto text_width = ImGui::CalcTextSize(offline_warning_text_.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", offline_warning_text_.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk

View File

@@ -18,7 +18,7 @@ int Render::RemoteWindow() {
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * TITLE_BAR_HEIGHT), ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("RemoteDesktopWindow", ImGui::BeginChild("RemoteDesktopWindow",
@@ -48,7 +48,7 @@ int Render::RemoteWindow() {
ImGui::BeginChild( ImGui::BeginChild(
"RemoteDesktopWindow_1", "RemoteDesktopWindow_1",
ImVec2(remote_window_width * 0.8f, remote_window_height * 0.43f), ImVec2(remote_window_width * 0.8f, remote_window_height * 0.43f),
ImGuiChildFlags_Border, ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus); ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -165,7 +165,21 @@ static int InputTextCallback(ImGuiInputTextCallbackData* data) {
} }
int Render::ConnectTo(const std::string& remote_id, const char* password, int Render::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password) { bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_.IsOnline(remote_id)) {
int ret =
RequestSingleDevicePresence(remote_id, password, remember_password);
if (ret != 0) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
} else {
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
}
return -1;
}
LOG_INFO("Connect to [{}]", remote_id); LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id; focused_remote_id_ = remote_id;
@@ -195,7 +209,10 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
props->control_window_min_width_ = title_bar_height_ * 0.65f; props->control_window_min_width_ = title_bar_height_ * 0.65f;
props->control_window_min_height_ = title_bar_height_ * 1.3f; props->control_window_min_height_ = title_bar_height_ * 1.3f;
props->control_window_max_width_ = title_bar_height_ * 9.0f; props->control_window_max_width_ = title_bar_height_ * 9.0f;
props->control_window_max_height_ = title_bar_height_ * 6.0f; props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_ = ConnectionStatus::Connecting;
show_connection_status_window_ = true;
if (!props->peer_) { if (!props->peer_) {
LOG_INFO("Create peer [{}] instance failed", props->local_id_); LOG_INFO("Create peer [{}] instance failed", props->local_id_);
@@ -253,4 +270,4 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@
#include "IconsFontAwesome6.h" #include "IconsFontAwesome6.h"
#include "config_center.h" #include "config_center.h"
#include "device_controller_factory.h" #include "device_controller_factory.h"
#include "device_presence.h"
#include "imgui.h" #include "imgui.h"
#include "imgui_impl_sdl3.h" #include "imgui_impl_sdl3.h"
#include "imgui_impl_sdlrenderer3.h" #include "imgui_impl_sdlrenderer3.h"
@@ -42,6 +43,41 @@
namespace crossdesk { namespace crossdesk {
class Render { class Render {
public: public:
struct FileTransferState {
std::atomic<bool> file_sending_ = false;
std::atomic<uint64_t> file_sent_bytes_ = 0;
std::atomic<uint64_t> file_total_bytes_ = 0;
std::atomic<uint32_t> file_send_rate_bps_ = 0;
std::mutex file_transfer_mutex_;
std::chrono::steady_clock::time_point file_send_start_time_;
std::chrono::steady_clock::time_point file_send_last_update_time_;
uint64_t file_send_last_bytes_ = 0;
bool file_transfer_window_visible_ = false;
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
std::filesystem::path file_path;
std::string file_label;
std::string remote_id;
};
std::queue<QueuedFile> file_send_queue_;
std::mutex file_queue_mutex_;
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
struct FileTransferInfo {
std::string file_name;
std::filesystem::path file_path;
uint64_t file_size = 0;
FileTransferStatus status = FileTransferStatus::Queued;
uint64_t sent_bytes = 0;
uint32_t file_id = 0;
uint32_t rate_bps = 0;
};
std::vector<FileTransferInfo> file_transfer_list_;
std::mutex file_transfer_list_mutex_;
};
struct SubStreamWindowProperties { struct SubStreamWindowProperties {
Params params_; Params params_;
PeerPtr* peer_ = nullptr; PeerPtr* peer_ = nullptr;
@@ -59,7 +95,7 @@ class Render {
bool connection_established_ = false; bool connection_established_ = false;
bool rejoin_ = false; bool rejoin_ = false;
bool net_traffic_stats_button_pressed_ = false; bool net_traffic_stats_button_pressed_ = false;
bool mouse_control_button_pressed_ = true; bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false; bool mouse_controller_is_started_ = false;
bool audio_capture_button_pressed_ = true; bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true; bool control_mouse_ = true;
@@ -88,8 +124,13 @@ class Render {
float mouse_diff_control_bar_pos_y_ = 0; float mouse_diff_control_bar_pos_y_ = 0;
double control_bar_button_pressed_time_ = 0; double control_bar_button_pressed_time_ = 0;
double net_traffic_stats_button_pressed_time_ = 0; double net_traffic_stats_button_pressed_time_ = 0;
unsigned char* dst_buffer_ = nullptr; // Double-buffered NV12 frame storage. Written by decode callback thread,
size_t dst_buffer_capacity_ = 0; // consumed by SDL main thread.
std::mutex video_frame_mutex_;
std::shared_ptr<std::vector<unsigned char>> front_frame_;
std::shared_ptr<std::vector<unsigned char>> back_frame_;
bool render_rect_dirty_ = false;
bool stream_cleanup_pending_ = false;
float mouse_pos_x_ = 0; float mouse_pos_x_ = 0;
float mouse_pos_y_ = 0; float mouse_pos_y_ = 0;
float mouse_pos_x_last_ = 0; float mouse_pos_x_last_ = 0;
@@ -129,38 +170,10 @@ class Render {
std::chrono::steady_clock::time_point last_time_; std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_; XNetTrafficStats net_traffic_stats_;
// File transfer progress using QueuedFile = FileTransferState::QueuedFile;
std::atomic<bool> file_sending_ = false; using FileTransferStatus = FileTransferState::FileTransferStatus;
std::atomic<uint64_t> file_sent_bytes_ = 0; using FileTransferInfo = FileTransferState::FileTransferInfo;
std::atomic<uint64_t> file_total_bytes_ = 0; FileTransferState file_transfer_;
std::atomic<uint32_t> file_send_rate_bps_ = 0;
std::mutex file_transfer_mutex_;
std::chrono::steady_clock::time_point file_send_start_time_;
std::chrono::steady_clock::time_point file_send_last_update_time_;
uint64_t file_send_last_bytes_ = 0;
bool file_transfer_window_visible_ = false;
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
std::filesystem::path file_path;
std::string file_label;
};
std::queue<QueuedFile> file_send_queue_;
std::mutex file_queue_mutex_;
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
struct FileTransferInfo {
std::string file_name;
std::filesystem::path file_path;
uint64_t file_size = 0;
FileTransferStatus status = FileTransferStatus::Queued;
uint64_t sent_bytes = 0;
uint32_t file_id = 0;
uint32_t rate_bps = 0;
};
std::vector<FileTransferInfo> file_transfer_list_;
std::mutex file_transfer_list_mutex_;
}; };
public: public:
@@ -180,6 +193,8 @@ class Render {
void UpdateLabels(); void UpdateLabels();
void UpdateInteractions(); void UpdateInteractions();
void HandleRecentConnections(); void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandleStreamWindow(); void HandleStreamWindow();
void HandleServerWindow(); void HandleServerWindow();
void Cleanup(); void Cleanup();
@@ -193,9 +208,13 @@ class Render {
void ProcessFileDropEvent(const SDL_Event& event); void ProcessFileDropEvent(const SDL_Event& event);
void ProcessSelectedFile(const std::string& path, void ProcessSelectedFile(
std::shared_ptr<SubStreamWindowProperties>& props, const std::string& path,
const std::string& file_label); const std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label, const std::string& remote_id = "");
std::shared_ptr<SubStreamWindowProperties>
GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id);
private: private:
int CreateStreamRenderWindow(); int CreateStreamRenderWindow();
@@ -204,12 +223,12 @@ class Render {
int UpdateNotificationWindow(); int UpdateNotificationWindow();
int StreamWindow(); int StreamWindow();
int ServerWindow(); int ServerWindow();
int RemoteClientInfoWindow();
int LocalWindow(); int LocalWindow();
int RemoteWindow(); int RemoteWindow();
int RecentConnectionsWindow(); int RecentConnectionsWindow();
int SettingWindow(); int SettingWindow();
int SelfHostedServerWindow(); int SelfHostedServerWindow();
int ShowSimpleFileBrowser();
int ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props); int ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props);
int ControlBar(std::shared_ptr<SubStreamWindowProperties>& props); int ControlBar(std::shared_ptr<SubStreamWindowProperties>& props);
int AboutWindow(); int AboutWindow();
@@ -217,13 +236,17 @@ class Render {
bool ConnectionStatusWindow( bool ConnectionStatusWindow(
std::shared_ptr<SubStreamWindowProperties>& props); std::shared_ptr<SubStreamWindowProperties>& props);
int ShowRecentConnections(); int ShowRecentConnections();
bool OpenUrl(const std::string& url);
void Hyperlink(const std::string& label, const std::string& url, void Hyperlink(const std::string& label, const std::string& url,
const float window_width); const float window_width);
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props); int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
std::string OpenFileDialog(std::string title);
private: private:
int ConnectTo(const std::string& remote_id, const char* password, int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password); bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string& remote_id,
const char* password, bool remember_password);
int CreateMainWindow(); int CreateMainWindow();
int DestroyMainWindow(); int DestroyMainWindow();
int CreateStreamWindow(); int CreateStreamWindow();
@@ -238,9 +261,12 @@ class Render {
int DrawStreamWindow(); int DrawStreamWindow();
int DrawServerWindow(); int DrawServerWindow();
int ConfirmDeleteConnection(); int ConfirmDeleteConnection();
int OfflineWarningWindow();
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props); int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
void DrawConnectionStatusText( void DrawConnectionStatusText(
std::shared_ptr<SubStreamWindowProperties>& props); std::shared_ptr<SubStreamWindowProperties>& props);
void DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props);
#ifdef __APPLE__ #ifdef __APPLE__
int RequestPermissionWindow(); int RequestPermissionWindow();
bool CheckScreenRecordingPermission(); bool CheckScreenRecordingPermission();
@@ -269,14 +295,17 @@ class Render {
static void OnSignalStatusCb(SignalStatus status, const char* user_id, static void OnSignalStatusCb(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data); size_t user_id_size, void* user_data);
static void OnSignalMessageCb(const char* message, size_t size,
void* user_data);
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id, static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
size_t user_id_size, void* user_data); size_t user_id_size, void* user_data);
static void NetStatusReport(const char* client_id, size_t client_id_size, static void OnNetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode, TraversalMode mode,
const XNetTrafficStats* net_traffic_stats, const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size, const char* user_id, const size_t user_id_size,
void* user_data); void* user_data);
static SDL_HitTestResult HitTestCallback(SDL_Window* window, static SDL_HitTestResult HitTestCallback(SDL_Window* window,
const SDL_Point* area, void* data); const SDL_Point* area, void* data);
@@ -317,7 +346,8 @@ class Render {
// File transfer helper functions // File transfer helper functions
void StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props, void StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
const std::filesystem::path& file_path, const std::filesystem::path& file_path,
const std::string& file_label); const std::string& file_label,
const std::string& remote_id = "");
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props); void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
int AudioDeviceInit(); int AudioDeviceInit();
@@ -362,7 +392,6 @@ class Render {
ConfigCenter::LANGUAGE localization_language_ = ConfigCenter::LANGUAGE localization_language_ =
ConfigCenter::LANGUAGE::CHINESE; ConfigCenter::LANGUAGE::CHINESE;
std::unique_ptr<PathManager> path_manager_; std::unique_ptr<PathManager> path_manager_;
std::string cert_path_;
std::string exec_log_path_; std::string exec_log_path_;
std::string dll_log_path_; std::string dll_log_path_;
std::string cache_path_; std::string cache_path_;
@@ -385,9 +414,12 @@ class Render {
// recent connections // recent connections
std::vector<std::pair<std::string, Thumbnail::RecentConnection>> std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
recent_connections_; recent_connections_;
std::vector<std::string> recent_connection_ids_;
int recent_connection_image_width_ = 160; int recent_connection_image_width_ = 160;
int recent_connection_image_height_ = 90; int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0; uint32_t recent_connection_image_save_time_ = 0;
DevicePresence device_presence_;
bool need_to_send_recent_connections_ = true;
// main window render // main window render
SDL_Window* main_window_ = nullptr; SDL_Window* main_window_ = nullptr;
@@ -413,11 +445,14 @@ class Render {
bool screen_capturer_is_started_ = false; bool screen_capturer_is_started_ = false;
bool start_speaker_capturer_ = false; bool start_speaker_capturer_ = false;
bool speaker_capturer_is_started_ = false; bool speaker_capturer_is_started_ = false;
bool start_keyboard_capturer_ = true; bool start_keyboard_capturer_ = false;
bool show_cursor_ = false; bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false; bool keyboard_capturer_is_started_ = false;
bool foucs_on_main_window_ = false; bool foucs_on_main_window_ = false;
bool foucs_on_stream_window_ = false; bool focus_on_stream_window_ = false;
bool main_window_minimized_ = false;
uint32_t last_main_minimize_request_tick_ = 0;
uint32_t last_stream_minimize_request_tick_ = 0;
bool audio_capture_ = false; bool audio_capture_ = false;
int main_window_width_real_ = 720; int main_window_width_real_ = 720;
int main_window_height_real_ = 540; int main_window_height_real_ = 540;
@@ -467,7 +502,7 @@ class Render {
bool just_created_ = false; bool just_created_ = false;
std::string controlled_remote_id_ = ""; std::string controlled_remote_id_ = "";
std::string focused_remote_id_ = ""; std::string focused_remote_id_ = "";
bool need_to_send_host_info_ = false; std::string remote_client_id_ = "";
SDL_Event last_mouse_event; SDL_Event last_mouse_event;
SDL_AudioStream* output_stream_; SDL_AudioStream* output_stream_;
uint32_t STREAM_REFRESH_EVENT = 0; uint32_t STREAM_REFRESH_EVENT = 0;
@@ -504,16 +539,18 @@ class Render {
bool need_to_destroy_server_window_ = false; bool need_to_destroy_server_window_ = false;
bool server_window_created_ = false; bool server_window_created_ = false;
bool server_window_inited_ = false; bool server_window_inited_ = false;
int server_window_width_default_ = 300; int server_window_width_default_ = 250;
int server_window_height_default_ = 450; int server_window_height_default_ = 150;
float server_window_width_ = 300; float server_window_width_ = 250;
float server_window_height_ = 450; float server_window_height_ = 150;
float server_window_title_bar_height_ = 50.0f; float server_window_title_bar_height_ = 30.0f;
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12; SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
int server_window_normal_width_ = 300; int server_window_normal_width_ = 250;
int server_window_normal_height_ = 450; int server_window_normal_height_ = 150;
float server_window_dpi_scaling_w_ = 1.0f; float server_window_dpi_scaling_w_ = 1.0f;
float server_window_dpi_scaling_h_ = 1.0f; float server_window_dpi_scaling_h_ = 1.0f;
float window_rounding_ = 6.0f;
float window_rounding_default_ = 6.0f;
// server window collapsed mode // server window collapsed mode
bool server_window_collapsed_ = false; bool server_window_collapsed_ = false;
@@ -549,9 +586,11 @@ class Render {
bool is_server_mode_ = false; bool is_server_mode_ = false;
bool reload_recent_connections_ = true; bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false; bool show_confirm_delete_connection_ = false;
bool show_offline_warning_window_ = false;
bool delete_connection_ = false; bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false; bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_ = ""; std::string delete_connection_name_ = "";
std::string offline_warning_text_ = "";
bool re_enter_remote_id_ = false; bool re_enter_remote_id_ = false;
double copy_start_time_ = 0; double copy_start_time_ = 0;
SignalStatus signal_status_ = SignalStatus::SignalClosed; SignalStatus signal_status_ = SignalStatus::SignalClosed;
@@ -573,6 +612,10 @@ class Render {
std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>> std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
file_id_to_props_; file_id_to_props_;
std::shared_mutex file_id_to_props_mutex_; std::shared_mutex file_id_to_props_mutex_;
// Map file_id to FileTransferState for global file transfer (props == null)
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
std::shared_mutex file_id_to_transfer_state_mutex_;
SDL_AudioDeviceID input_dev_; SDL_AudioDeviceID input_dev_;
SDL_AudioDeviceID output_dev_; SDL_AudioDeviceID output_dev_;
ScreenCapturerFactory* screen_capturer_factory_ = nullptr; ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
@@ -586,8 +629,8 @@ class Render {
uint64_t last_frame_time_; uint64_t last_frame_time_;
bool show_new_version_icon_ = false; bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true; bool show_new_version_icon_in_menu_ = true;
uint64_t new_version_icon_last_trigger_time_ = 0; double new_version_icon_last_trigger_time_ = 0.0;
uint64_t new_version_icon_render_start_time_ = 0; double new_version_icon_render_start_time_ = 0.0;
#ifdef __APPLE__ #ifdef __APPLE__
bool show_request_permission_window_ = true; bool show_request_permission_window_ = true;
#endif #endif
@@ -598,16 +641,15 @@ class Render {
char self_hosted_id_[17] = ""; char self_hosted_id_[17] = "";
char self_hosted_user_id_[17] = ""; char self_hosted_user_id_[17] = "";
int language_button_value_ = 0; int language_button_value_ = 0;
int video_quality_button_value_ = 0; int video_quality_button_value_ = 2;
int video_frame_rate_button_value_ = 1; int video_frame_rate_button_value_ = 1;
int video_encode_format_button_value_ = 0; int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = false; bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true; bool enable_turn_ = true;
bool enable_srtp_ = false; bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn"; char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099"; char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478"; char coturn_server_port_[6] = "3478";
char cert_file_path_[256] = "";
bool enable_self_hosted_ = false; bool enable_self_hosted_ = false;
int language_button_value_last_ = 0; int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0; int video_quality_button_value_last_ = 0;
@@ -623,10 +665,11 @@ class Render {
bool enable_daemon_last_ = false; bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = false; bool enable_minimize_to_tray_ = false;
bool enable_minimize_to_tray_last_ = false; bool enable_minimize_to_tray_last_ = false;
char file_transfer_save_path_buf_[512] = "";
std::string file_transfer_save_path_last_ = "";
char signal_server_ip_self_[256] = ""; char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = ""; char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = ""; char coturn_server_port_self_[6] = "";
std::string tls_cert_path_self_ = "";
bool settings_window_pos_reset_ = true; bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true; bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_ = ""; std::string selected_current_file_path_ = "";
@@ -647,6 +690,17 @@ class Render {
/* ------ server mode ------ */ /* ------ server mode ------ */
std::unordered_map<std::string, ConnectionStatus> connection_status_; std::unordered_map<std::string, ConnectionStatus> connection_status_;
std::unordered_map<std::string, std::string> connection_host_names_;
std::string selected_server_remote_id_ = "";
std::string selected_server_remote_hostname_ = "";
std::mutex pending_presence_probe_mutex_;
bool pending_presence_probe_ = false;
bool pending_presence_result_ready_ = false;
bool pending_presence_online_ = false;
std::string pending_presence_remote_id_ = "";
std::string pending_presence_password_ = "";
bool pending_presence_remember_password_ = false;
FileTransferState file_transfer_;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif

View File

@@ -4,7 +4,9 @@
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <limits>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include "clipboard.h" #include "clipboard.h"
#include "device_controller.h" #include "device_controller.h"
@@ -19,6 +21,65 @@
namespace crossdesk { namespace crossdesk {
void Render::OnSignalMessageCb(const char* message, size_t size,
void* user_data) {
Render* render = (Render*)user_data;
if (!render || !message || size == 0) {
return;
}
std::string s(message, size);
auto j = nlohmann::json::parse(s, nullptr, false);
if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) {
return;
}
std::string type = j["type"].get<std::string>();
if (type == "presence") {
if (j.contains("devices") && j["devices"].is_array()) {
for (auto& dev : j["devices"]) {
if (!dev.is_object()) {
continue;
}
if (!dev.contains("id") || !dev["id"].is_string()) {
continue;
}
if (!dev.contains("online") || !dev["online"].is_boolean()) {
continue;
}
std::string id = dev["id"].get<std::string>();
bool online = dev["online"].get<bool>();
render->device_presence_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
render->pending_presence_probe_mutex_);
if (render->pending_presence_probe_ &&
render->pending_presence_remote_id_ == id) {
render->pending_presence_result_ready_ = true;
render->pending_presence_online_ = online;
}
}
}
}
} else if (type == "presence_update") {
if (j.contains("id") && j["id"].is_string() && j.contains("online") &&
j["online"].is_boolean()) {
std::string id = j["id"].get<std::string>();
bool online = j["online"].get<bool>();
if (!id.empty()) {
render->device_presence_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
render->pending_presence_probe_mutex_);
if (render->pending_presence_probe_ &&
render->pending_presence_remote_id_ == id) {
render->pending_presence_result_ready_ = true;
render->pending_presence_online_ = online;
}
}
}
}
}
}
int Render::SendKeyCommand(int key_code, bool is_down) { int Render::SendKeyCommand(int key_code, bool is_down) {
RemoteAction remote_action; RemoteAction remote_action;
remote_action.type = ControlType::keyboard; remote_action.type = ControlType::keyboard;
@@ -29,17 +90,16 @@ int Render::SendKeyCommand(int key_code, bool is_down) {
} }
remote_action.k.key_value = key_code; remote_action.k.key_value = key_code;
if (!controlled_remote_id_.empty()) { std::string target_id = controlled_remote_id_.empty() ? focused_remote_id_
// std::shared_lock lock(client_properties_mutex_); : controlled_remote_id_;
if (client_properties_.find(controlled_remote_id_) != if (!target_id.empty()) {
client_properties_.end()) { if (client_properties_.find(target_id) != client_properties_.end()) {
auto props = client_properties_[controlled_remote_id_]; auto props = client_properties_[target_id];
if (props->connection_status_ == ConnectionStatus::Connected) { if (props->connection_status_ == ConnectionStatus::Connected &&
props->peer_) {
std::string msg = remote_action.to_json(); std::string msg = remote_action.to_json();
if (props->peer_) { SendDataFrame(props->peer_, msg.c_str(), msg.size(),
SendDataFrame(props->peer_, msg.c_str(), msg.size(), props->data_label_.c_str());
props->data_label_.c_str());
}
} }
} }
} }
@@ -103,11 +163,10 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
} }
if (props->control_bar_hovered_ || props->display_selectable_hovered_) { if (props->control_bar_hovered_ || props->display_selectable_hovered_) {
remote_action.m.flag = MouseFlag::move; break;
} }
std::string msg = remote_action.to_json();
if (props->peer_) { if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(), SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str()); props->data_label_.c_str());
} }
@@ -153,8 +212,11 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
(float)(last_mouse_event.button.y - props->stream_render_rect_.y) / (float)(last_mouse_event.button.y - props->stream_render_rect_.y) /
render_height; render_height;
std::string msg = remote_action.to_json(); if (props->control_bar_hovered_) {
continue;
}
if (props->peer_) { if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(), SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str()); props->data_label_.c_str());
} }
@@ -237,31 +299,31 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
render->client_properties_.find(remote_id)->second.get(); render->client_properties_.find(remote_id)->second.get();
if (props->connection_established_) { if (props->connection_established_) {
if (!props->dst_buffer_) { {
props->dst_buffer_capacity_ = video_frame->size; std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->dst_buffer_ = new unsigned char[video_frame->size];
}
if (props->dst_buffer_capacity_ < video_frame->size) { if (!props->back_frame_) {
delete props->dst_buffer_; props->back_frame_ =
props->dst_buffer_capacity_ = video_frame->size; std::make_shared<std::vector<unsigned char>>(video_frame->size);
props->dst_buffer_ = new unsigned char[video_frame->size]; }
} if (props->back_frame_->size() != video_frame->size) {
props->back_frame_->resize(video_frame->size);
}
memcpy(props->dst_buffer_, video_frame->data, video_frame->size); std::memcpy(props->back_frame_->data(), video_frame->data,
bool need_to_update_render_rect = false; video_frame->size);
if (props->video_width_ != props->video_width_last_ ||
props->video_height_ != props->video_height_last_) {
need_to_update_render_rect = true;
props->video_width_last_ = props->video_width_;
props->video_height_last_ = props->video_height_;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
if (need_to_update_render_rect) { const bool size_changed = (props->video_width_ != video_frame->width) ||
render->UpdateRenderRect(); (props->video_height_ != video_frame->height);
if (size_changed) {
props->render_rect_dirty_ = true;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
props->front_frame_.swap(props->back_frame_);
} }
SDL_Event event; SDL_Event event;
@@ -320,7 +382,28 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
std::string remote_user_id = std::string(user_id, user_id_size); std::string remote_user_id = std::string(user_id, user_id_size);
static FileReceiver receiver; static FileReceiver receiver;
receiver.SetOnSendAck([render](const FileTransferAck& ack) -> int { // Update output directory from config
std::string configured_path =
render->config_center_->GetFileTransferSavePath();
if (!configured_path.empty()) {
receiver.SetOutputDir(std::filesystem::u8path(configured_path));
} else if (receiver.OutputDir().empty()) {
receiver = FileReceiver(); // re-init with default desktop path
}
receiver.SetOnSendAck([render,
remote_user_id](const FileTransferAck& ack) -> int {
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
if (is_server_sending) {
auto props =
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
if (props) {
PeerPtr* peer = props->peer_;
return SendReliableDataFrame(
peer, reinterpret_cast<const char*>(&ack),
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
}
}
return SendReliableDataFrame( return SendReliableDataFrame(
render->peer_, reinterpret_cast<const char*>(&ack), render->peer_, reinterpret_cast<const char*>(&ack),
sizeof(FileTransferAck), render->file_feedback_label_.c_str()); sizeof(FileTransferAck), render->file_feedback_label_.c_str());
@@ -330,6 +413,13 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
return; return;
} else if (source_id == render->clipboard_label_) { } else if (source_id == render->clipboard_label_) {
if (size > 0) { if (size > 0) {
std::string remote_user_id(user_id, user_id_size);
auto props =
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
if (props && !props->enable_mouse_control_) {
return;
}
std::string clipboard_text(data, size); std::string clipboard_text(data, size);
if (!Clipboard::SetText(clipboard_text)) { if (!Clipboard::SetText(clipboard_text)) {
LOG_ERROR("Failed to set clipboard content from remote"); LOG_ERROR("Failed to set clipboard content from remote");
@@ -361,42 +451,100 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
} }
} }
Render::FileTransferState* state = nullptr;
if (!props) { if (!props) {
LOG_WARN("FileTransferAck: no props found for file_id={}", ack.file_id); {
return; std::shared_lock lock(render->file_id_to_transfer_state_mutex_);
auto it = render->file_id_to_transfer_state_.find(ack.file_id);
if (it != render->file_id_to_transfer_state_.end()) {
state = it->second;
}
}
if (!state) {
LOG_WARN("FileTransferAck: no props/state found for file_id={}",
ack.file_id);
return;
}
} else {
state = &props->file_transfer_;
} }
// Update progress based on ACK // Update progress based on ACK
props->file_sent_bytes_ = ack.acked_offset; state->file_sent_bytes_ = ack.acked_offset;
props->file_total_bytes_ = ack.total_size; state->file_total_bytes_ = ack.total_size;
uint32_t rate_bps = 0; uint32_t rate_bps = 0;
{ {
uint32_t data_channel_bitrate = if (props) {
props->net_traffic_stats_.data_outbound_stats.bitrate; uint32_t data_channel_bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
if (data_channel_bitrate > 0 && props->file_sending_.load()) { if (data_channel_bitrate > 0 && state->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f); rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
uint32_t current_rate = props->file_send_rate_bps_.load(); uint32_t current_rate = state->file_send_rate_bps_.load();
if (current_rate > 0) { if (current_rate > 0) {
// 70% old + 30% new for smoother display // 70% old + 30% new for smoother display
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3); rate_bps =
static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
}
} else {
rate_bps = state->file_send_rate_bps_.load();
} }
} else { } else {
rate_bps = props->file_send_rate_bps_.load(); // Global transfer: no per-connection bitrate available.
// Estimate send rate from ACKed bytes delta over time.
const uint32_t current_rate = state->file_send_rate_bps_.load();
uint32_t estimated_rate_bps = 0;
const auto now = std::chrono::steady_clock::now();
uint64_t last_bytes = 0;
std::chrono::steady_clock::time_point last_time;
{
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
last_bytes = state->file_send_last_bytes_;
last_time = state->file_send_last_update_time_;
}
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
const uint64_t delta_bytes = ack.acked_offset - last_bytes;
const double delta_seconds =
std::chrono::duration<double>(now - last_time).count();
if (delta_seconds > 0.0 && delta_bytes > 0) {
const double bps =
(static_cast<double>(delta_bytes) * 8.0) / delta_seconds;
if (bps > 0.0) {
const double capped =
(std::min)(bps, static_cast<double>(
(std::numeric_limits<uint32_t>::max)()));
estimated_rate_bps = static_cast<uint32_t>(capped);
}
}
}
if (estimated_rate_bps > 0 && current_rate > 0) {
// 70% old + 30% new for smoother display
rate_bps = static_cast<uint32_t>(current_rate * 0.7 +
estimated_rate_bps * 0.3);
} else if (estimated_rate_bps > 0) {
rate_bps = estimated_rate_bps;
} else {
rate_bps = current_rate;
}
} }
props->file_send_rate_bps_ = rate_bps; state->file_send_rate_bps_ = rate_bps;
props->file_send_last_bytes_ = ack.acked_offset; state->file_send_last_bytes_ = ack.acked_offset;
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
props->file_send_last_update_time_ = now; state->file_send_last_update_time_ = now;
} }
// Update file transfer list: update progress and rate // Update file transfer list: update progress and rate
{ {
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_); std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto& info : props->file_transfer_list_) { for (auto& info : state->file_transfer_list_) {
if (info.file_id == ack.file_id) { if (info.file_id == ack.file_id) {
info.sent_bytes = ack.acked_offset; info.sent_bytes = ack.acked_offset;
info.file_size = ack.total_size; info.file_size = ack.total_size;
@@ -410,8 +558,8 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
if ((ack.flags & 0x01) != 0) { if ((ack.flags & 0x01) != 0) {
// Transfer completed - receiver has finished receiving the file // Transfer completed - receiver has finished receiving the file
// Reopen window if it was closed by user // Reopen window if it was closed by user
props->file_transfer_window_visible_ = true; state->file_transfer_window_visible_ = true;
props->file_sending_ = false; // Mark sending as finished state->file_sending_ = false; // Mark sending as finished
LOG_INFO( LOG_INFO(
"File transfer completed via ACK, file_id={}, total_size={}, " "File transfer completed via ACK, file_id={}, total_size={}, "
"acked_offset={}", "acked_offset={}",
@@ -419,11 +567,11 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
// Update file transfer list: mark as completed // Update file transfer list: mark as completed
{ {
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_); std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto& info : props->file_transfer_list_) { for (auto& info : state->file_transfer_list_) {
if (info.file_id == ack.file_id) { if (info.file_id == ack.file_id) {
info.status = info.status =
SubStreamWindowProperties::FileTransferStatus::Completed; Render::FileTransferState::FileTransferStatus::Completed;
info.sent_bytes = ack.total_size; info.sent_bytes = ack.total_size;
break; break;
} }
@@ -432,9 +580,15 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
// Unregister file_id mapping after completion // Unregister file_id mapping after completion
{ {
std::lock_guard<std::shared_mutex> lock( if (props) {
render->file_id_to_props_mutex_); std::lock_guard<std::shared_mutex> lock(
render->file_id_to_props_.erase(ack.file_id); render->file_id_to_props_mutex_);
render->file_id_to_props_.erase(ack.file_id);
} else {
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_transfer_state_mutex_);
render->file_id_to_transfer_state_.erase(ack.file_id);
}
} }
// Process next file in queue // Process next file in queue
@@ -456,24 +610,32 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
std::string remote_id(user_id, user_id_size); std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(render->client_properties_mutex_); // std::shared_lock lock(render->client_properties_mutex_);
if (render->client_properties_.find(remote_id) != if (remote_action.type == ControlType::host_infomation) {
render->client_properties_.end()) { if (render->client_properties_.find(remote_id) !=
// local render->client_properties_.end()) {
auto props = render->client_properties_.find(remote_id)->second; // client mode
if (remote_action.type == ControlType::host_infomation && auto props = render->client_properties_.find(remote_id)->second;
props->remote_host_name_.empty()) { if (props && props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name, props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size); remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_); LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
for (int i = 0; i < remote_action.i.display_num; i++) { for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back( props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i], DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i], remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i])); remote_action.i.right[i], remote_action.i.bottom[i]));
}
} }
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]);
FreeRemoteAction(remote_action);
} }
FreeRemoteAction(remote_action);
} else { } else {
// remote // remote
if (remote_action.type == ControlType::mouse && render->mouse_controller_) { if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
@@ -511,6 +673,7 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
render->signal_connected_ = false; render->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) { } else if (SignalStatus::SignalConnected == status) {
render->signal_connected_ = true; render->signal_connected_ = true;
render->need_to_send_recent_connections_ = true;
LOG_INFO("[{}] connected to signal server", client_id); LOG_INFO("[{}] connected to signal server", client_id);
} else if (SignalStatus::SignalFailed == status) { } else if (SignalStatus::SignalFailed == status) {
render->signal_connected_ = false; render->signal_connected_ = false;
@@ -568,7 +731,49 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
switch (status) { switch (status) {
case ConnectionStatus::Connected: { case ConnectionStatus::Connected: {
render->need_to_send_host_info_ = true; {
RemoteAction remote_action;
remote_action.i.display_num = render->display_info_list_.size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
render->display_info_list_[i].name);
remote_action.i.display_list[i] =
(char*)malloc(render->display_info_list_[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
render->display_info_list_[i].name.c_str(),
render->display_info_list_[i].name.length());
remote_action.i
.display_list[i][render->display_info_list_[i].name.length()] =
'\0';
remote_action.i.left[i] = render->display_info_list_[i].left;
remote_action.i.top[i] = render->display_info_list_[i].top;
remote_action.i.right[i] = render->display_info_list_[i].right;
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
render->control_data_label_.c_str());
FreeRemoteAction(remote_action);
}
if (!render->need_to_create_stream_window_ && if (!render->need_to_create_stream_window_ &&
!render->client_properties_.empty()) { !render->client_properties_.empty()) {
render->need_to_create_stream_window_ = true; render->need_to_create_stream_window_ = true;
@@ -578,19 +783,32 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
0, (int)render->title_bar_height_, 0, (int)render->title_bar_height_,
(int)render->stream_window_width_, (int)render->stream_window_width_,
(int)(render->stream_window_height_ - render->title_bar_height_)}; (int)(render->stream_window_height_ - render->title_bar_height_)};
render->start_keyboard_capturer_ = true;
break; break;
} }
case ConnectionStatus::Disconnected: case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed: case ConnectionStatus::Failed:
case ConnectionStatus::Closed: { case ConnectionStatus::Closed: {
props->connection_established_ = false; props->connection_established_ = false;
props->mouse_control_button_pressed_ = false; props->enable_mouse_control_ = false;
if (props->dst_buffer_ && props->stream_texture_) {
memset(props->dst_buffer_, 0, props->dst_buffer_capacity_); {
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_, std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->texture_width_); props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
} }
render->CleanSubStreamWindowProperties(props);
SDL_Event event;
event.type = render->STREAM_REFRESH_EVENT;
event.user.data1 = props.get();
SDL_PushEvent(&event);
render->focus_on_stream_window_ = false;
break; break;
} }
@@ -623,17 +841,55 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
switch (status) { switch (status) {
case ConnectionStatus::Connected: { case ConnectionStatus::Connected: {
{
RemoteAction remote_action;
remote_action.i.display_num = render->display_info_list_.size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
render->display_info_list_[i].name);
remote_action.i.display_list[i] =
(char*)malloc(render->display_info_list_[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
render->display_info_list_[i].name.c_str(),
render->display_info_list_[i].name.length());
remote_action.i
.display_list[i][render->display_info_list_[i].name.length()] =
'\0';
remote_action.i.left[i] = render->display_info_list_[i].left;
remote_action.i.top[i] = render->display_info_list_[i].top;
remote_action.i.right[i] = render->display_info_list_[i].right;
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(render->peer_, msg.data(), msg.size(),
render->control_data_label_.c_str());
FreeRemoteAction(remote_action);
}
render->need_to_create_server_window_ = true; render->need_to_create_server_window_ = true;
render->need_to_send_host_info_ = true;
render->is_server_mode_ = true; render->is_server_mode_ = true;
render->start_screen_capturer_ = true; render->start_screen_capturer_ = true;
render->start_speaker_capturer_ = true; render->start_speaker_capturer_ = true;
#ifdef CROSSDESK_DEBUG render->remote_client_id_ = remote_id;
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
#else
render->start_mouse_controller_ = true; render->start_mouse_controller_ = true;
#endif
if (std::all_of(render->connection_status_.begin(), if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) { render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") != std::string::npos; return kv.first.find("web") != std::string::npos;
@@ -643,6 +899,8 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
break; break;
} }
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: { case ConnectionStatus::Closed: {
if (std::all_of(render->connection_status_.begin(), if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) { render->connection_status_.end(), [](const auto& kv) {
@@ -656,7 +914,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
render->start_speaker_capturer_ = false; render->start_speaker_capturer_ = false;
render->start_mouse_controller_ = false; render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false; render->start_keyboard_capturer_ = false;
render->need_to_send_host_info_ = false; render->remote_client_id_ = "";
if (props) props->connection_established_ = false; if (props) props->connection_established_ = false;
if (render->audio_capture_) { if (render->audio_capture_) {
render->StopSpeakerCapturer(); render->StopSpeakerCapturer();
@@ -664,6 +922,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
} }
render->connection_status_.erase(remote_id); 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(), if (std::all_of(render->connection_status_.begin(),
@@ -681,11 +943,11 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
} }
} }
void Render::NetStatusReport(const char* client_id, size_t client_id_size, void Render::OnNetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode, TraversalMode mode,
const XNetTrafficStats* net_traffic_stats, const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size, const char* user_id, const size_t user_id_size,
void* user_data) { void* user_data) {
Render* render = (Render*)user_data; Render* render = (Render*)user_data;
if (!render) { if (!render) {
return; return;

View File

@@ -15,18 +15,6 @@
namespace crossdesk { namespace crossdesk {
std::string OpenFileDialog(std::string title) {
const char* path = tinyfd_openFileDialog(title.c_str(),
"", // default path
0, // number of filters
nullptr, // filters
nullptr, // filter description
0 // no multiple selection
);
return path ? path : "";
}
int CountDigits(int number) { int CountDigits(int number) {
if (number == 0) return 1; if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1; return (int)std::floor(std::log10(std::abs(number))) + 1;
@@ -53,13 +41,29 @@ int LossRateDisplay(float loss_rate) {
return 0; return 0;
} }
std::string Render::OpenFileDialog(std::string title) {
const char* path = tinyfd_openFileDialog(title.c_str(),
"", // default path
0, // number of filters
nullptr, // filters
nullptr, // filter description
0 // no multiple selection
);
return path ? path : "";
}
void Render::ProcessSelectedFile( void Render::ProcessSelectedFile(
const std::string& path, std::shared_ptr<SubStreamWindowProperties>& props, const std::string& path,
const std::string& file_label) { const std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label, const std::string& remote_id) {
if (path.empty()) { if (path.empty()) {
return; return;
} }
FileTransferState* file_transfer_state =
props ? &props->file_transfer_ : &file_transfer_;
LOG_INFO("Selected file: {}", path.c_str()); LOG_INFO("Selected file: {}", path.c_str());
std::filesystem::path file_path = std::filesystem::u8path(path); std::filesystem::path file_path = std::filesystem::u8path(path);
@@ -74,47 +78,51 @@ void Render::ProcessSelectedFile(
// Add file to transfer list // Add file to transfer list
{ {
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_); std::lock_guard<std::mutex> lock(
SubStreamWindowProperties::FileTransferInfo info; file_transfer_state->file_transfer_list_mutex_);
FileTransferState::FileTransferInfo info;
info.file_name = file_path.filename().u8string(); info.file_name = file_path.filename().u8string();
info.file_path = file_path; // Store full path for precise matching info.file_path = file_path; // Store full path for precise matching
info.file_size = file_size; info.file_size = file_size;
info.status = SubStreamWindowProperties::FileTransferStatus::Queued; info.status = FileTransferState::FileTransferStatus::Queued;
info.sent_bytes = 0; info.sent_bytes = 0;
info.file_id = 0; info.file_id = 0;
info.rate_bps = 0; info.rate_bps = 0;
props->file_transfer_list_.push_back(info); file_transfer_state->file_transfer_list_.push_back(info);
} }
props->file_transfer_window_visible_ = true; file_transfer_state->file_transfer_window_visible_ = true;
if (props->file_sending_.load()) { if (file_transfer_state->file_sending_.load()) {
// Add to queue // Add to queue
size_t queue_size = 0; size_t queue_size = 0;
{ {
std::lock_guard<std::mutex> lock(props->file_queue_mutex_); std::lock_guard<std::mutex> lock(file_transfer_state->file_queue_mutex_);
SubStreamWindowProperties::QueuedFile queued_file; FileTransferState::QueuedFile queued_file;
queued_file.file_path = file_path; queued_file.file_path = file_path;
queued_file.file_label = file_label; queued_file.file_label = file_label;
props->file_send_queue_.push(queued_file); queued_file.remote_id = remote_id;
queue_size = props->file_send_queue_.size(); file_transfer_state->file_send_queue_.push(queued_file);
queue_size = file_transfer_state->file_send_queue_.size();
} }
LOG_INFO("File added to queue: {} ({} files in queue)", LOG_INFO("File added to queue: {} ({} files in queue)",
file_path.filename().string().c_str(), queue_size); file_path.filename().string().c_str(), queue_size);
} else { } else {
StartFileTransfer(props, file_path, file_label); StartFileTransfer(props, file_path, file_label, remote_id);
if (props->file_sending_.load()) { if (file_transfer_state->file_sending_.load()) {
} else { } else {
// Failed to start (race condition: another file started between // Failed to start (race condition: another file started between
// check and call) Add to queue // check and call) Add to queue
size_t queue_size = 0; size_t queue_size = 0;
{ {
std::lock_guard<std::mutex> lock(props->file_queue_mutex_); std::lock_guard<std::mutex> lock(
SubStreamWindowProperties::QueuedFile queued_file; file_transfer_state->file_queue_mutex_);
FileTransferState::QueuedFile queued_file;
queued_file.file_path = file_path; queued_file.file_path = file_path;
queued_file.file_label = file_label; queued_file.file_label = file_label;
props->file_send_queue_.push(queued_file); queued_file.remote_id = remote_id;
queue_size = props->file_send_queue_.size(); file_transfer_state->file_send_queue_.push(queued_file);
queue_size = file_transfer_state->file_send_queue_.size();
} }
LOG_INFO( LOG_INFO(
"File added to queue after race condition: {} ({} files in " "File added to queue after race condition: {} ({} files in "
@@ -130,10 +138,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
float line_padding = title_bar_height_ * 0.12f; float line_padding = title_bar_height_ * 0.12f;
float line_thickness = title_bar_height_ * 0.07f; float line_thickness = title_bar_height_ * 0.07f;
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
if (props->control_bar_expand_) { if (props->control_bar_expand_) {
ImGui::SetCursorPosX(props->is_control_bar_in_left_ ImGui::SetCursorPosX(props->is_control_bar_in_left_
? props->control_window_width_ * 1.03f ? props->control_window_width_ * 0.03f
: props->control_window_width_ * 0.17f); : props->control_window_width_ * 0.17f);
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList* draw_list = ImGui::GetWindowDrawList();
@@ -190,24 +198,21 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
float mouse_y = ImGui::GetCursorScreenPos().y; float mouse_y = ImGui::GetCursorScreenPos().y;
float disable_mouse_x = mouse_x + line_padding; float disable_mouse_x = mouse_x + line_padding;
float disable_mouse_y = mouse_y + line_padding; float disable_mouse_y = mouse_y + line_padding;
std::string mouse = props->mouse_control_button_pressed_ std::string mouse = ICON_FA_COMPUTER_MOUSE;
? ICON_FA_COMPUTER_MOUSE
: ICON_FA_COMPUTER_MOUSE;
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(mouse.c_str(), ImVec2(button_width, button_height))) { if (ImGui::Button(mouse.c_str(), ImVec2(button_width, button_height))) {
if (props->connection_established_) { if (props->connection_established_) {
start_keyboard_capturer_ = !start_keyboard_capturer_; start_keyboard_capturer_ = !start_keyboard_capturer_;
props->control_mouse_ = !props->control_mouse_; props->control_mouse_ = !props->control_mouse_;
props->mouse_control_button_pressed_ = props->enable_mouse_control_ = !props->enable_mouse_control_;
!props->mouse_control_button_pressed_;
props->mouse_control_button_label_ = props->mouse_control_button_label_ =
props->mouse_control_button_pressed_ props->enable_mouse_control_
? localization::release_mouse[localization_language_index_] ? localization::release_mouse[localization_language_index_]
: localization::control_mouse[localization_language_index_]; : localization::control_mouse[localization_language_index_];
} }
} }
if (!props->mouse_control_button_pressed_) { if (!props->enable_mouse_control_) {
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y), draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
ImVec2(mouse_x + button_width - line_padding, ImVec2(mouse_x + button_width - line_padding,
mouse_y + button_height - line_padding), mouse_y + button_height - line_padding),
@@ -275,7 +280,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
std::string title = std::string title =
localization::select_file[localization_language_index_]; localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title); std::string path = OpenFileDialog(title);
this->ProcessSelectedFile(path, props, file_label_); ProcessSelectedFile(path, props, file_label_);
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -351,13 +356,11 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SameLine(); ImGui::SameLine();
} }
float expand_button_pos_x = float expand_button_pos_x = props->control_bar_expand_
props->control_bar_expand_ ? (props->is_control_bar_in_left_ ? (props->is_control_bar_in_left_
? props->control_window_width_ * 1.917f ? props->control_window_width_ * 0.917f
: props->control_window_width_ * 0.03f) : props->control_window_width_ * 0.03f)
: (props->is_control_bar_in_left_ : props->control_window_width_ * 0.11f;
? props->control_window_width_ * 1.02f
: props->control_window_width_ * 0.23f);
ImGui::SetCursorPosX(expand_button_pos_x); ImGui::SetCursorPosX(expand_button_pos_x);
@@ -389,9 +392,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
} }
int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) { int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SetCursorPos(ImVec2(props->is_control_bar_in_left_ ImGui::SetCursorPos(ImVec2(props->control_window_width_ * 0.048f,
? props->control_window_width_ * 1.02f
: props->control_window_width_ * 0.02f,
props->control_window_min_height_)); props->control_window_min_height_));
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
if (ImGui::BeginTable("NetTrafficStats", 4, ImGuiTableFlags_BordersH, if (ImGui::BeginTable("NetTrafficStats", 4, ImGuiTableFlags_BordersH,
@@ -452,12 +453,33 @@ int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
LossRateDisplay(props->net_traffic_stats_.total_inbound_stats.loss_rate); LossRateDisplay(props->net_traffic_stats_.total_inbound_stats.loss_rate);
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text("FPS"); ImGui::Text("FPS:");
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text("%d", props->fps_); ImGui::Text("%d", props->fps_);
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Text("%s:",
localization::resolution[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text("%dx%d", props->video_width_, props->video_height_);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Text(
"%s:",
localization::connection_mode[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text(
"%s",
props->traversal_mode_ == 0
? localization::connection_mode_direct[localization_language_index_]
.c_str()
: localization::connection_mode_relay[localization_language_index_]
.c_str());
ImGui::EndTable(); ImGui::EndTable();
} }
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);

View File

@@ -9,25 +9,27 @@ int Render::StatusBar() {
float status_bar_width = io.DisplaySize.x; float status_bar_width = io.DisplaySize.x;
float status_bar_height = io.DisplaySize.y * STATUS_BAR_HEIGHT; float status_bar_height = io.DisplaySize.y * STATUS_BAR_HEIGHT;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
static bool a, b, c, d, e; static bool a, b, c, d, e;
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT)), ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT)),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::BeginChild( ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
"StatusBar", ImVec2(status_bar_width, status_bar_height), ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGuiChildFlags_Border, ImGui::BeginChild("StatusBar", ImVec2(status_bar_width, status_bar_height),
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBringToFrontOnFocus); ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor(2);
ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f, ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f,
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f)); io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f));
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
ImColor(1.0f, 1.0f, 1.0f), 100);
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f, draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f,
ImColor(signal_connected_ ? 0.0f : 1.0f, ImColor(signal_connected_ ? 0.0f : 1.0f,
signal_connected_ ? 1.0f : 0.0f, 0.0f), signal_connected_ ? 1.0f : 0.0f, 0.0f),
100); 100);
draw_list->AddCircle(dot_pos, status_bar_height * 0.25f,
ImColor(1.0f, 1.0f, 1.0f), 100);
ImGui::SetWindowFontScale(0.6f); ImGui::SetWindowFontScale(0.6f);
draw_list->AddText( draw_list->AddText(
@@ -40,8 +42,7 @@ int Render::StatusBar() {
.c_str()); .c_str());
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor();
ImGui::EndChild(); ImGui::EndChild();
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -2,8 +2,10 @@
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h" #include "render.h"
#include "rounded_corner_button.h"
#define NEW_VERSION_ICON_RENDER_TIME_INTERVAL 2000 constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
namespace crossdesk { namespace crossdesk {
@@ -15,14 +17,28 @@ int Render::TitleBar(bool main_window) {
float title_bar_button_width = title_bar_button_width_; float title_bar_button_width = title_bar_button_width_;
float title_bar_button_height = title_bar_button_height_; float title_bar_button_height = title_bar_button_height_;
if (main_window) { if (main_window) {
title_bar_width = io.DisplaySize.x; // When the main window is minimized, Dear ImGui may report DisplaySize as
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT; // (0, 0). Do not overwrite shared title-bar metrics with zeros, otherwise
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f); // stream/server windows (which reuse these metrics) will lose their title
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH; // bars and appear collapsed.
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT; if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
title_bar_height_ = title_bar_height; title_bar_width = io.DisplaySize.x;
title_bar_button_width_ = title_bar_button_width; title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_button_height_ = title_bar_button_height; title_bar_height_padding = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
title_bar_height_ = title_bar_height;
title_bar_button_width_ = title_bar_button_width;
title_bar_button_height_ = title_bar_button_height;
} else {
// Keep using last known good values.
title_bar_width = title_bar_width_;
title_bar_height = title_bar_height_;
title_bar_height_padding = title_bar_height_;
title_bar_button_width = title_bar_button_width_;
title_bar_button_height = title_bar_button_height_;
}
} else { } else {
title_bar_width = io.DisplaySize.x; title_bar_width = io.DisplaySize.x;
title_bar_height = title_bar_button_height_; title_bar_height = title_bar_button_height_;
@@ -30,12 +46,12 @@ int Render::TitleBar(bool main_window) {
title_bar_button_height = title_bar_button_height_; title_bar_button_height = title_bar_button_height_;
} }
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(main_window ? "MainTitleBar" : "StreamTitleBar", ImGui::BeginChild(main_window ? "MainTitleBar" : "StreamTitleBar",
ImVec2(title_bar_width, title_bar_height_padding), ImVec2(title_bar_width, title_bar_height_padding),
ImGuiChildFlags_Border, ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus); ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -92,13 +108,11 @@ int Render::TitleBar(bool main_window) {
std::string about_str = localization::about[localization_language_index_]; std::string about_str = localization::about[localization_language_index_];
if (update_available_) { if (update_available_) {
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>( const double now_time = ImGui::GetTime();
std::chrono::steady_clock::now().time_since_epoch())
.count();
// every 2 seconds // every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >= if (now_time - new_version_icon_last_trigger_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) { kNewVersionIconBlinkIntervalSec) {
show_new_version_icon_ = true; show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time; new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time; new_version_icon_last_trigger_time_ = now_time;
@@ -106,9 +120,9 @@ int Render::TitleBar(bool main_window) {
// render for 1 second // render for 1 second
if (show_new_version_icon_) { if (show_new_version_icon_) {
about_str = about_str + " " + ICON_FA_TRIANGLE_EXCLAMATION; about_str = about_str + " " + ICON_FA_CIRCLE_ARROW_UP;
if (now_time - new_version_icon_render_start_time_ >= if (now_time - new_version_icon_render_start_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) { kNewVersionIconBlinkOnTimeSec) {
show_new_version_icon_ = false; show_new_version_icon_ = false;
} }
} else { } else {
@@ -137,13 +151,11 @@ int Render::TitleBar(bool main_window) {
} }
if (update_available_ && show_new_version_icon_in_menu_) { if (update_available_ && show_new_version_icon_in_menu_) {
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>( const double now_time = ImGui::GetTime();
std::chrono::steady_clock::now().time_since_epoch())
.count();
// every 2 seconds // every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >= if (now_time - new_version_icon_last_trigger_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) { kNewVersionIconBlinkIntervalSec) {
show_new_version_icon_ = true; show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time; new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time; new_version_icon_last_trigger_time_ = now_time;
@@ -152,14 +164,13 @@ int Render::TitleBar(bool main_window) {
// render for 1 second // render for 1 second
if (show_new_version_icon_) { if (show_new_version_icon_) {
ImGui::SetWindowFontScale(0.6f); ImGui::SetWindowFontScale(0.6f);
ImGui::SetCursorPos( ImGui::SetCursorPos(ImVec2(bar_pos_x + title_bar_button_width * 0.21f,
ImVec2(bar_pos_x + title_bar_button_width * 0.15f, bar_pos_y - title_bar_button_width * 0.24f));
bar_pos_y - title_bar_button_width * 0.325f)); ImGui::Text(ICON_FA_CIRCLE_ARROW_UP);
ImGui::Text(ICON_FA_TRIANGLE_EXCLAMATION);
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
if (now_time - new_version_icon_render_start_time_ >= if (now_time - new_version_icon_render_start_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) { kNewVersionIconBlinkOnTimeSec) {
show_new_version_icon_ = false; show_new_version_icon_ = false;
} }
} }
@@ -187,6 +198,11 @@ int Render::TitleBar(bool main_window) {
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS; std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
if (ImGui::Button(window_minimize_button.c_str(), if (ImGui::Button(window_minimize_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) { ImVec2(title_bar_button_width, title_bar_button_height))) {
if (main_window) {
last_main_minimize_request_tick_ = SDL_GetTicks();
} else {
last_stream_minimize_request_tick_ = SDL_GetTicks();
}
SDL_MinimizeWindow(main_window ? main_window_ : stream_window_); SDL_MinimizeWindow(main_window ? main_window_ : stream_window_);
} }
draw_list->AddLine( draw_list->AddLine(
@@ -270,8 +286,20 @@ int Render::TitleBar(bool main_window) {
float xmark_pos_y = title_bar_button_height * 0.5f; float xmark_pos_y = title_bar_button_height * 0.5f;
float xmark_size = title_bar_button_width * 0.33f; float xmark_size = title_bar_button_width * 0.33f;
std::string close_button = "##xmark"; // ICON_FA_XMARK; std::string close_button = "##xmark"; // ICON_FA_XMARK;
if (ImGui::Button(close_button.c_str(), bool close_button_clicked = false;
ImVec2(title_bar_button_width, title_bar_button_height))) { if (main_window) {
close_button_clicked = RoundedCornerButton(
close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height), 8.5f,
ImDrawFlags_RoundCornersTopRight, true, IM_COL32(0, 0, 0, 0),
IM_COL32(250, 0, 0, 255), IM_COL32(255, 0, 0, 128));
} else {
close_button_clicked =
ImGui::Button(close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height));
}
if (close_button_clicked) {
#if _WIN32 #if _WIN32
if (enable_minimize_to_tray_) { if (enable_minimize_to_tray_) {
tray_->MinimizeToTray(); tray_->MinimizeToTray();
@@ -284,6 +312,7 @@ int Render::TitleBar(bool main_window) {
} }
#endif #endif
} }
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f, draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
xmark_pos_y - xmark_size / 2 + 0.75f), xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f, ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f,
@@ -300,4 +329,4 @@ int Render::TitleBar(bool main_window) {
ImGui::EndChild(); ImGui::EndChild();
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -89,7 +89,7 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
GetCursorPos(&pt); GetCursorPos(&pt);
HMENU menu = CreatePopupMenu(); HMENU menu = CreatePopupMenu();
AppendMenuW(menu, MF_STRING, 1001, AppendMenuW(menu, MF_STRING, 1001,
localization::exit_program[language_index_]); localization::GetExitProgramLabel(language_index_));
SetForegroundWindow(hwnd_message_only_); SetForegroundWindow(hwnd_message_only_);
int cmd = int cmd =
@@ -112,4 +112,4 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
} }
return true; return true;
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -1,6 +1,10 @@
#include <cstdlib> #include <cstdlib>
#include <string> #include <string>
#if defined(_WIN32)
#include <windows.h>
#endif
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
@@ -8,11 +12,44 @@
namespace crossdesk { namespace crossdesk {
bool Render::OpenUrl(const std::string& url) {
#if defined(_WIN32)
int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
if (wide_len <= 0) {
return false;
}
std::wstring wide_url(static_cast<size_t>(wide_len), L'\0');
MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, &wide_url[0], wide_len);
if (!wide_url.empty() && wide_url.back() == L'\0') {
wide_url.pop_back();
}
std::wstring cmd = L"cmd.exe /c start \"\" \"" + wide_url + L"\"";
STARTUPINFOW startup_info = {sizeof(startup_info)};
PROCESS_INFORMATION process_info = {};
if (!CreateProcessW(nullptr, &cmd[0], nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &startup_info,
&process_info)) {
return false;
}
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
return true;
#elif defined(__APPLE__)
std::string cmd = "open " + url;
return system(cmd.c_str()) == 0;
#else
std::string cmd = "xdg-open " + url;
return system(cmd.c_str()) == 0;
#endif
}
void Render::Hyperlink(const std::string& label, const std::string& url, void Render::Hyperlink(const std::string& label, const std::string& url,
const float window_width) { const float window_width) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255)); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
ImGui::SetCursorPosX(window_width * 0.1f); ImGui::TextUnformatted(label.c_str());
ImGui::Text("%s", label.c_str());
ImGui::PopStyleColor(); ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
@@ -23,14 +60,7 @@ void Render::Hyperlink(const std::string& label, const std::string& url,
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip(); ImGui::EndTooltip();
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
#if defined(_WIN32) OpenUrl(url);
std::string cmd = "start " + url;
#elif defined(__APPLE__)
std::string cmd = "open " + url;
#else
std::string cmd = "xdg-open " + url;
#endif
system(cmd.c_str()); // open browser
} }
} }
} }
@@ -40,7 +70,7 @@ int Render::AboutWindow() {
float about_window_width = title_bar_button_width_ * 7.5f; float about_window_width = title_bar_button_width_ * 7.5f;
float about_window_height = latest_version_.empty() float about_window_height = latest_version_.empty()
? title_bar_button_width_ * 4.0f ? title_bar_button_width_ * 4.0f
: title_bar_button_width_ * 4.6f; : title_bar_button_width_ * 4.9f;
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2( ImGui::SetNextWindowPos(ImVec2(
@@ -52,8 +82,8 @@ int Render::AboutWindow() {
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
ImGui::Begin( ImGui::Begin(
localization::about[localization_language_index_].c_str(), nullptr, localization::about[localization_language_index_].c_str(), nullptr,
@@ -74,16 +104,23 @@ int Render::AboutWindow() {
ImGui::SetCursorPosX(about_window_width * 0.1f); ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", text.c_str()); ImGui::Text("%s", text.c_str());
if (update_available_) { if (0) {
std::string latest_version = std::string new_version_available =
localization::new_version_available[localization_language_index_] + localization::new_version_available[localization_language_index_] +
": " + latest_version_; ": ";
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", new_version_available.c_str());
std::string access_website = std::string access_website =
localization::access_website[localization_language_index_]; localization::access_website[localization_language_index_];
Hyperlink(latest_version, "https://crossdesk.cn", about_window_width); ImGui::SetCursorPosX((about_window_width -
} ImGui::CalcTextSize(latest_version_.c_str()).x) /
2.0f);
Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width);
ImGui::Text(""); ImGui::Spacing();
} else {
ImGui::Text("%s", "");
}
std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved."; std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved.";
std::string license_text = "Licensed under GNU LGPL v3."; std::string license_text = "Licensed under GNU LGPL v3.";
@@ -93,7 +130,7 @@ int Render::AboutWindow() {
ImGui::Text("%s", license_text.c_str()); ImGui::Text("%s", license_text.c_str());
ImGui::SetCursorPosX(about_window_width * 0.445f); ImGui::SetCursorPosX(about_window_width * 0.445f);
ImGui::SetCursorPosY(about_window_height * 0.75f); ImGui::SetCursorPosY(about_window_height * 0.8f);
// OK // OK
if (ImGui::Button(localization::ok[localization_language_index_].c_str())) { if (ImGui::Button(localization::ok[localization_language_index_].c_str())) {
show_about_window_ = false; show_about_window_ = false;

View File

@@ -15,10 +15,10 @@ bool Render::ConnectionStatusWindow(
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f)); ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConnectionStatusWindow", nullptr, ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
@@ -36,6 +36,18 @@ bool Render::ConnectionStatusWindow(
text = localization::p2p_connecting[localization_language_index_]; text = localization::p2p_connecting[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); 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 == props->connection_status_) { } else if (ConnectionStatus::Connected == props->connection_status_) {
text = localization::p2p_connected[localization_language_index_]; text = localization::p2p_connected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);

View File

@@ -42,50 +42,74 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
} }
} }
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1, 1, 1, 1)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 1.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 10.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowSize( float y_boundary = fullscreen_button_pressed_ ? 0.0f : title_bar_height_;
ImVec2(props->control_window_width_, props->control_window_height_), float container_x = 0.0f;
ImGuiCond_Always); float container_y = y_boundary;
float container_w = stream_window_width_;
float container_h = stream_window_height_ - y_boundary;
ImGui::SetNextWindowPos(ImVec2(0, title_bar_height_), ImGuiCond_Once); ImGui::SetNextWindowSize(ImVec2(container_w, container_h), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(container_x, container_y), ImGuiCond_Always);
float pos_x = 0; float pos_x = 0;
float pos_y = 0; float pos_y = 0;
float y_boundary = fullscreen_button_pressed_ ? 0 : title_bar_height_;
if (props->reset_control_bar_pos_) { std::string container_window_title =
float new_cursor_pos_x = 0; props->remote_id_ + "ControlContainerWindow";
float new_cursor_pos_y = 0; ImGui::Begin(container_window_title.c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoBackground);
ImGui::PopStyleVar();
ImVec2 container_pos = ImGui::GetWindowPos();
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
props->control_bar_hovered_) {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
ImVec2 delta = ImGui::GetIO().MouseDelta;
pos_x = current_x_rel + delta.x;
pos_y = current_y_rel + delta.y;
if (pos_x < 0.0f) pos_x = 0.0f;
if (pos_y < 0.0f) pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h)
pos_y = container_h - props->control_window_height_;
} else if (props->reset_control_bar_pos_) {
float new_cursor_pos_x = 0.0f;
float new_cursor_pos_y = 0.0f;
// set control window pos // set control window pos
if (props->control_window_pos_.y + props->control_window_height_ > float current_y_rel = props->control_window_pos_.y - container_pos.y;
stream_window_height_) { if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = stream_window_height_ - props->control_window_height_; pos_y = container_h - props->control_window_height_;
} else if (props->control_window_pos_.y < y_boundary) { } else if (current_y_rel < 0.0f) {
pos_y = y_boundary; pos_y = 0.0f;
} else { } else {
pos_y = props->control_window_pos_.y; pos_y = current_y_rel;
} }
if (props->is_control_bar_in_left_) { if (props->is_control_bar_in_left_) {
pos_x = 0; pos_x = 0.0f;
} else { } else {
pos_x = stream_window_width_ - props->control_window_width_; pos_x = container_w - props->control_window_width_;
} }
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
if (0 != props->mouse_diff_control_bar_pos_x_ && if (0 != props->mouse_diff_control_bar_pos_x_ &&
0 != props->mouse_diff_control_bar_pos_y_) { 0 != props->mouse_diff_control_bar_pos_y_) {
// set cursor pos // set cursor pos
new_cursor_pos_x = pos_x + props->mouse_diff_control_bar_pos_x_; new_cursor_pos_x =
new_cursor_pos_y = pos_y + props->mouse_diff_control_bar_pos_y_; container_pos.x + pos_x + props->mouse_diff_control_bar_pos_x_;
new_cursor_pos_y =
container_pos.y + pos_y + props->mouse_diff_control_bar_pos_y_;
SDL_WarpMouseInWindow(stream_window_, (int)new_cursor_pos_x, SDL_WarpMouseInWindow(stream_window_, (int)new_cursor_pos_x,
(int)new_cursor_pos_y); (int)new_cursor_pos_y);
@@ -94,12 +118,14 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
} else if (!props->reset_control_bar_pos_ && } else if (!props->reset_control_bar_pos_ &&
ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Left) ||
props->control_window_width_is_changing_) { props->control_window_width_is_changing_) {
if (props->control_window_pos_.x <= stream_window_width_ * 0.5f) { float current_x_rel = props->control_window_pos_.x - container_pos.x;
if (props->control_window_pos_.y + props->control_window_height_ > float current_y_rel = props->control_window_pos_.y - container_pos.y;
stream_window_height_) { if (current_x_rel <= container_w * 0.5f) {
pos_y = stream_window_height_ - props->control_window_height_; pos_x = 0.0f;
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else { } else {
pos_y = props->control_window_pos_.y; pos_y = current_y_rel;
} }
if (props->control_bar_expand_) { if (props->control_bar_expand_) {
@@ -118,47 +144,53 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
} }
} }
props->is_control_bar_in_left_ = true; props->is_control_bar_in_left_ = true;
} else if (props->control_window_pos_.x > stream_window_width_ * 0.5f) { } else if (current_x_rel > container_w * 0.5f) {
pos_x = 0; pos_x = container_w - props->control_window_width_;
pos_y = pos_y = (current_y_rel >= 0.0f &&
(props->control_window_pos_.y >= y_boundary && current_y_rel <= container_h - props->control_window_height_)
props->control_window_pos_.y <= ? current_y_rel
stream_window_height_ - props->control_window_height_) : (current_y_rel < 0.0f
? props->control_window_pos_.y ? 0.0f
: (props->control_window_pos_.y < : (container_h - props->control_window_height_));
(fullscreen_button_pressed_ ? 0 : title_bar_height_)
? (fullscreen_button_pressed_ ? 0 : title_bar_height_)
: (stream_window_height_ - props->control_window_height_));
if (props->control_bar_expand_) { if (props->control_bar_expand_) {
if (props->control_window_width_ >= props->control_window_max_width_) { if (props->control_window_width_ >= props->control_window_max_width_) {
props->control_window_width_ = props->control_window_max_width_; props->control_window_width_ = props->control_window_max_width_;
props->control_window_width_is_changing_ = false; props->control_window_width_is_changing_ = false;
pos_x = stream_window_width_ - props->control_window_max_width_; pos_x = container_w - props->control_window_max_width_;
} else { } else {
props->control_window_width_is_changing_ = true; props->control_window_width_is_changing_ = true;
pos_x = stream_window_width_ - props->control_window_width_; pos_x = container_w - props->control_window_width_;
} }
} else { } else {
if (props->control_window_width_ <= props->control_window_min_width_) { if (props->control_window_width_ <= props->control_window_min_width_) {
props->control_window_width_ = props->control_window_min_width_; props->control_window_width_ = props->control_window_min_width_;
props->control_window_width_is_changing_ = false; props->control_window_width_is_changing_ = false;
pos_x = stream_window_width_ - props->control_window_min_width_; pos_x = container_w - props->control_window_min_width_;
} else { } else {
props->control_window_width_is_changing_ = true; props->control_window_width_is_changing_ = true;
pos_x = stream_window_width_ - props->control_window_width_; pos_x = container_w - props->control_window_width_;
} }
} }
props->is_control_bar_in_left_ = false; props->is_control_bar_in_left_ = false;
} }
if (props->control_window_pos_.y + props->control_window_height_ > if (current_y_rel + props->control_window_height_ > container_h) {
stream_window_height_) { pos_y = container_h - props->control_window_height_;
pos_y = stream_window_height_ - props->control_window_height_; } else if (current_y_rel < 0.0f) {
} else if (props->control_window_pos_.y < y_boundary) { pos_y = 0.0f;
pos_y = y_boundary;
} }
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always); } else {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
pos_x = current_x_rel;
pos_y = current_y_rel;
if (pos_x < 0.0f) pos_x = 0.0f;
if (pos_y < 0.0f) pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h)
pos_y = container_h - props->control_window_height_;
} }
if (props->control_bar_expand_ && props->control_window_height_is_changing_) { if (props->control_bar_expand_ && props->control_window_height_is_changing_) {
@@ -180,10 +212,20 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
} }
std::string control_window_title = props->remote_id_ + "ControlWindow"; std::string control_window_title = props->remote_id_ + "ControlWindow";
ImGui::Begin(control_window_title.c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking); static bool a, b, c, d, e;
ImGui::PopStyleVar(); float child_cursor_x = pos_x;
float child_cursor_y = pos_y;
ImGui::SetCursorPos(ImVec2(child_cursor_x, child_cursor_y));
std::string control_child_window_title =
props->remote_id_ + "ControlChildWindow";
ImGui::BeginChild(
control_child_window_title.c_str(),
ImVec2(props->control_window_width_, props->control_window_height_),
ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration);
ImGui::PopStyleColor();
props->control_window_pos_ = ImGui::GetWindowPos(); props->control_window_pos_ = ImGui::GetWindowPos();
SDL_GetMouseState(&props->mouse_pos_x_, &props->mouse_pos_y_); SDL_GetMouseState(&props->mouse_pos_x_, &props->mouse_pos_y_);
@@ -192,31 +234,27 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
props->mouse_diff_control_bar_pos_y_ = props->mouse_diff_control_bar_pos_y_ =
props->mouse_pos_y_ - props->control_window_pos_.y; props->mouse_pos_y_ - props->control_window_pos_.y;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); if (props->control_window_pos_.y < container_pos.y ||
static bool a, b, c, d, e; props->control_window_pos_.y + props->control_window_height_ >
ImGui::SetNextWindowPos( (container_pos.y + container_h) ||
ImVec2(props->is_control_bar_in_left_ props->control_window_pos_.x < container_pos.x ||
? props->control_window_pos_.x - props->control_window_width_ props->control_window_pos_.x + props->control_window_width_ >
: props->control_window_pos_.x, (container_pos.x + container_w)) {
props->control_window_pos_.y), ImGui::ClearActiveID();
ImGuiCond_Always); props->reset_control_bar_pos_ = true;
props->mouse_diff_control_bar_pos_x_ = 0;
std::string control_child_window_title = props->mouse_diff_control_bar_pos_y_ = 0;
props->remote_id_ + "ControlChildWindow"; }
ImGui::BeginChild(control_child_window_title.c_str(),
ImVec2(props->control_window_width_ * 2.0f,
props->control_window_height_),
ImGuiChildFlags_Border, ImGuiWindowFlags_NoDecoration);
ImGui::PopStyleColor();
ControlBar(props); ControlBar(props);
props->control_bar_hovered_ = ImGui::IsWindowHovered(); props->control_bar_hovered_ = ImGui::IsWindowHovered();
ImGui::EndChild(); ImGui::EndChild();
ImGui::End(); ImGui::End();
ImGui::PopStyleVar(4); ImGui::PopStyleVar(3);
ImGui::PopStyleColor(); ImGui::PopStyleColor();
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -30,12 +30,14 @@ int BitrateDisplay(int bitrate) {
int Render::FileTransferWindow( int Render::FileTransferWindow(
std::shared_ptr<SubStreamWindowProperties>& props) { std::shared_ptr<SubStreamWindowProperties>& props) {
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
// Only show window if there are files in transfer list or currently // Only show window if there are files in transfer list or currently
// transferring // transferring
std::vector<SubStreamWindowProperties::FileTransferInfo> file_list; std::vector<SubStreamWindowProperties::FileTransferInfo> file_list;
{ {
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_); std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
file_list = props->file_transfer_list_; file_list = state->file_transfer_list_;
} }
// Sort file list: Sending first, then Completed, then Queued, then Failed // Sort file list: Sending first, then Completed, then Queued, then Failed
@@ -66,7 +68,7 @@ int Render::FileTransferWindow(
// It will be reopened automatically when: // It will be reopened automatically when:
// 1. A file transfer completes (in render_callback.cpp) // 1. A file transfer completes (in render_callback.cpp)
// 2. A new file starts sending from queue (in render.cpp) // 2. A new file starts sending from queue (in render.cpp)
if (!props->file_transfer_window_visible_) { if (!state->file_transfer_window_visible_) {
return 0; return 0;
} }
@@ -92,10 +94,10 @@ int Render::FileTransferWindow(
ImGui::PushFont(stream_windows_system_chinese_font_); ImGui::PushFont(stream_windows_system_chinese_font_);
} }
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 0.9f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f));
ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
@@ -115,7 +117,7 @@ int Render::FileTransferWindow(
// Close button handling // Close button handling
if (!window_opened) { if (!window_opened) {
props->file_transfer_window_visible_ = false; state->file_transfer_window_visible_ = false;
ImGui::End(); ImGui::End();
return 0; return 0;
} }
@@ -128,7 +130,7 @@ int Render::FileTransferWindow(
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
ImGui::BeginChild( ImGui::BeginChild(
"FileList", ImVec2(0, file_transfer_window_height * 0.75f), "FileList", ImVec2(0, file_transfer_window_height * 0.75f),
ImGuiChildFlags_Border, ImGuiWindowFlags_HorizontalScrollbar); ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
@@ -240,3 +242,4 @@ int Render::FileTransferWindow(
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -2,6 +2,7 @@
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h" #include "render.h"
#include "tinyfiledialogs.h"
namespace crossdesk { namespace crossdesk {
@@ -15,28 +16,28 @@ int Render::SettingWindow() {
!defined(__arm__) && USE_CUDA) || \ !defined(__arm__) && USE_CUDA) || \
defined(__APPLE__)) defined(__APPLE__))
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.07f)); ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f)); ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f));
#else #else
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.1f)); ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.8f)); ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f));
#endif #endif
} else { } else {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \ !defined(__arm__) && USE_CUDA) || \
defined(__APPLE__)) defined(__APPLE__))
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.07f)); ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f)); ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f));
#else #else
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.1f)); ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.8f)); ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f));
#endif #endif
} }
@@ -49,8 +50,8 @@ int Render::SettingWindow() {
int settings_items_offset = 0; int settings_items_offset = 0;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(localization::settings[localization_language_index_].c_str(), ImGui::Begin(localization::settings[localization_language_index_].c_str(),
nullptr, nullptr,
@@ -59,9 +60,9 @@ int Render::SettingWindow() {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
{ {
const char* language_items[] = { const auto& supported_languages = localization::GetSupportedLanguages();
localization::language_zh[localization_language_index_].c_str(), language_button_value_ =
localization::language_en[localization_language_index_].c_str()}; localization::detail::ClampLanguageIndex(language_button_value_);
settings_items_offset += settings_items_padding; settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset); ImGui::SetCursorPosY(settings_items_offset);
@@ -76,13 +77,23 @@ int Render::SettingWindow() {
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo("##language", if (ImGui::BeginCombo(
language_items[language_button_value_])) { "##language",
localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex(
language_button_value_)]
.display_name
.c_str())) {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < IM_ARRAYSIZE(language_items); i++) { for (int i = 0; i < static_cast<int>(supported_languages.size());
++i) {
bool selected = (i == language_button_value_); bool selected = (i == language_button_value_);
if (ImGui::Selectable(language_items[i], selected)) if (ImGui::Selectable(
supported_languages[i].display_name.c_str(), selected))
language_button_value_ = i; language_button_value_ = i;
if (selected) {
ImGui::SetItemDefaultFocus();
}
} }
ImGui::EndCombo(); ImGui::EndCombo();
@@ -330,10 +341,14 @@ int Render::SettingWindow() {
ImGui::EndTooltip(); ImGui::EndTooltip();
} }
} }
#if _WIN32
ImGui::Separator(); 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; settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset); ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
@@ -349,8 +364,67 @@ int Render::SettingWindow() {
ImGui::Checkbox("##enable_minimize_to_tray_", ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_); &enable_minimize_to_tray_);
} #ifndef _WIN32
ImGui::PopStyleColor();
ImGui::EndDisabled();
#endif #endif
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::file_transfer_save_path[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f);
}
std::string display_path =
strlen(file_transfer_save_path_buf_) > 0
? file_transfer_save_path_buf_
: localization::default_desktop[localization_language_index_];
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.95f, 0.95f, 0.95f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.9f, 0.9f, 0.9f, 1.0f));
ImGui::PushFont(main_windows_system_chinese_font_);
if (ImGui::Button(display_path.c_str(),
ImVec2(title_bar_button_width_ * 2.0f, 0))) {
const char* folder =
tinyfd_selectFolderDialog(localization::file_transfer_save_path
[localization_language_index_]
.c_str(),
strlen(file_transfer_save_path_buf_) > 0
? file_transfer_save_path_buf_
: nullptr);
if (folder) {
strncpy(file_transfer_save_path_buf_, folder,
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) -
1] = '\0';
}
}
if (ImGui::IsItemHovered() &&
strlen(file_transfer_save_path_buf_) > 0) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", file_transfer_save_path_buf_);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImGui::PopFont();
ImGui::PopStyleColor(3);
}
if (stream_window_inited_) { if (stream_window_inited_) {
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
@@ -374,16 +448,24 @@ int Render::SettingWindow() {
show_self_hosted_server_config_window_ = false; show_self_hosted_server_config_window_ = false;
// Language // Language
language_button_value_ =
localization::detail::ClampLanguageIndex(language_button_value_);
if (language_button_value_ == 0) { if (language_button_value_ == 0) {
config_center_->SetLanguage(ConfigCenter::LANGUAGE::CHINESE); localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
} else if (language_button_value_ == 1) {
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
} else { } else {
config_center_->SetLanguage(ConfigCenter::LANGUAGE::ENGLISH); localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
} }
config_center_->SetLanguage(localization_language_);
language_button_value_last_ = language_button_value_; language_button_value_last_ = language_button_value_;
localization_language_ = (ConfigCenter::LANGUAGE)language_button_value_;
localization_language_index_ = language_button_value_; localization_language_index_ = language_button_value_;
LOG_INFO("Set localization language: {}", LOG_INFO("Set localization language: {}",
localization_language_index_ == 0 ? "zh" : "en"); localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex(
localization_language_index_)]
.code
.c_str());
// Video quality // Video quality
if (video_quality_button_value_ == 0) { if (video_quality_button_value_ == 0) {
@@ -469,6 +551,10 @@ int Render::SettingWindow() {
enable_minimize_to_tray_last_ = enable_minimize_to_tray_; enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
#endif #endif
// File transfer save path
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
file_transfer_save_path_last_ = file_transfer_save_path_buf_;
settings_window_pos_reset_ = true; settings_window_pos_reset_ = true;
// Recreate peer instance // Recreate peer instance
@@ -516,6 +602,13 @@ int Render::SettingWindow() {
enable_turn_ = enable_turn_last_; enable_turn_ = enable_turn_last_;
} }
// Restore file transfer save path
strncpy(file_transfer_save_path_buf_,
file_transfer_save_path_last_.c_str(),
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) - 1] =
'\0';
settings_window_pos_reset_ = true; settings_window_pos_reset_ = true;
} }
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
@@ -527,4 +620,4 @@ int Render::SettingWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -14,15 +14,16 @@ int Render::MainWindow() {
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * (TITLE_BAR_HEIGHT)), ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * (TITLE_BAR_HEIGHT)),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::BeginChild( ImGui::BeginChild(
"DeskWindow", "DeskWindow",
ImVec2(local_remote_window_width, local_remote_window_height), ImVec2(local_remote_window_width, local_remote_window_height),
ImGuiChildFlags_Border, ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus); ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleColor(); ImGui::PopStyleColor(2);
LocalWindow(); LocalWindow();
@@ -56,4 +57,4 @@ int Render::MainWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -127,8 +127,8 @@ int Render::RequestPermissionWindow() {
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin( ImGui::Begin(

View File

@@ -28,98 +28,6 @@ std::vector<std::string> GetRootEntries() {
return roots; return roots;
} }
int Render::ShowSimpleFileBrowser() {
std::string display_text;
if (selected_current_file_path_.empty()) {
selected_current_file_path_ = std::filesystem::current_path().string();
}
if (!tls_cert_path_self_.empty()) {
display_text =
std::filesystem::path(tls_cert_path_self_).filename().string();
} else if (selected_current_file_path_ != "Root") {
display_text =
std::filesystem::path(selected_current_file_path_).filename().string();
if (display_text.empty()) {
display_text = selected_current_file_path_;
}
}
if (display_text.empty()) {
display_text =
localization::select_a_file[localization_language_index_].c_str();
}
if (show_file_browser_) {
ImGui::PushItemFlag(ImGuiItemFlags_AutoClosePopups, false);
float fixed_width = title_bar_button_width_ * 3.8f;
ImGui::SetNextItemWidth(fixed_width);
ImGui::SetNextWindowSizeConstraints(ImVec2(fixed_width, 0),
ImVec2(fixed_width, 100.0f));
if (ImGui::BeginCombo("##select_a_file", display_text.c_str(), 0)) {
ImGui::SetWindowFontScale(0.5f);
bool file_selected = false;
auto roots = GetRootEntries();
if (selected_current_file_path_ == "Root" ||
!std::filesystem::exists(selected_current_file_path_) ||
!std::filesystem::is_directory(selected_current_file_path_)) {
for (const auto& root : roots) {
if (ImGui::Selectable(root.c_str())) {
selected_current_file_path_ = root;
tls_cert_path_self_.clear();
}
}
} else {
std::filesystem::path p(selected_current_file_path_);
if (ImGui::Selectable("..")) {
if (std::find(roots.begin(), roots.end(),
selected_current_file_path_) != roots.end()) {
selected_current_file_path_ = "Root";
} else if (p.has_parent_path()) {
selected_current_file_path_ = p.parent_path().string();
} else {
selected_current_file_path_ = "Root";
}
tls_cert_path_self_.clear();
}
try {
for (const auto& entry : std::filesystem::directory_iterator(
selected_current_file_path_)) {
std::string name = entry.path().filename().string();
if (entry.is_directory()) {
if (ImGui::Selectable(name.c_str())) {
selected_current_file_path_ = entry.path().string();
tls_cert_path_self_.clear();
}
} else {
if (ImGui::Selectable(name.c_str())) {
tls_cert_path_self_ = entry.path().string();
file_selected = true;
show_file_browser_ = false;
}
}
}
} catch (const std::exception& e) {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error: %s", e.what());
}
}
ImGui::EndCombo();
}
ImGui::PopItemFlag();
} else {
show_file_browser_ = true;
}
return 0;
}
int Render::SelfHostedServerWindow() { int Render::SelfHostedServerWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
if (show_self_hosted_server_config_window_) { if (show_self_hosted_server_config_window_) {
@@ -128,12 +36,12 @@ int Render::SelfHostedServerWindow() {
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.298f, io.DisplaySize.y * 0.25f)); ImVec2(io.DisplaySize.x * 0.298f, io.DisplaySize.y * 0.25f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.41f)); ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.35f));
} else { } else {
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.27f, io.DisplaySize.y * 0.3f)); ImVec2(io.DisplaySize.x * 0.27f, io.DisplaySize.y * 0.3f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.41f)); ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.35f));
} }
self_hosted_server_config_window_pos_reset_ = false; self_hosted_server_config_window_pos_reset_ = false;
@@ -143,8 +51,8 @@ int Render::SelfHostedServerWindow() {
{ {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(localization::self_hosted_server_settings ImGui::Begin(localization::self_hosted_server_settings
[localization_language_index_] [localization_language_index_]
@@ -212,35 +120,6 @@ int Render::SelfHostedServerWindow() {
IM_ARRAYSIZE(coturn_server_port_self_)); IM_ARRAYSIZE(coturn_server_port_self_));
} }
ImGui::Separator();
// {
// ImGui::AlignTextToFramePadding();
// ImGui::Text(
// "%s",
// localization::reset_cert_fingerprint[localization_language_index_]
// .c_str());
// ImGui::SameLine();
// if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
// ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
// } else {
// ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
// }
// ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
// ShowSimpleFileBrowser();
// }
{
ImGui::AlignTextToFramePadding();
if (ImGui::Button(localization::reset_cert_fingerprint
[localization_language_index_]
.c_str())) {
config_center_->ClearCertFingerprint();
LOG_INFO("Certificate fingerprint cleared by user");
}
}
if (stream_window_inited_) { if (stream_window_inited_) {
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
@@ -263,7 +142,6 @@ int Render::SelfHostedServerWindow() {
config_center_->SetServerHost(signal_server_ip_self_); config_center_->SetServerHost(signal_server_ip_self_);
config_center_->SetServerPort(atoi(signal_server_port_self_)); config_center_->SetServerPort(atoi(signal_server_port_self_));
config_center_->SetCoturnServerPort(atoi(coturn_server_port_self_)); config_center_->SetCoturnServerPort(atoi(coturn_server_port_self_));
config_center_->SetCertFilePath(tls_cert_path_self_);
strncpy(signal_server_ip_, signal_server_ip_self_, strncpy(signal_server_ip_, signal_server_ip_self_,
sizeof(signal_server_ip_) - 1); sizeof(signal_server_ip_) - 1);
signal_server_ip_[sizeof(signal_server_ip_) - 1] = '\0'; signal_server_ip_[sizeof(signal_server_ip_) - 1] = '\0';
@@ -273,9 +151,6 @@ int Render::SelfHostedServerWindow() {
strncpy(coturn_server_port_, coturn_server_port_self_, strncpy(coturn_server_port_, coturn_server_port_self_,
sizeof(coturn_server_port_) - 1); sizeof(coturn_server_port_) - 1);
coturn_server_port_[sizeof(coturn_server_port_) - 1] = '\0'; coturn_server_port_[sizeof(coturn_server_port_) - 1] = '\0';
strncpy(cert_file_path_, tls_cert_path_self_.c_str(),
sizeof(cert_file_path_) - 1);
cert_file_path_[sizeof(cert_file_path_) - 1] = '\0';
self_hosted_server_config_window_pos_reset_ = true; self_hosted_server_config_window_pos_reset_ = true;
} }
@@ -306,7 +181,6 @@ int Render::SelfHostedServerWindow() {
} else { } else {
coturn_server_port_self_[0] = '\0'; coturn_server_port_self_[0] = '\0';
} }
tls_cert_path_self_ = config_center_->GetCertFilePath();
} }
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);

View File

@@ -1,48 +1,47 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h" #include "render.h"
#include "rounded_corner_button.h"
namespace crossdesk { namespace crossdesk {
namespace { namespace {
constexpr float kDragThresholdPx = 3.0f; int CountDigits(int number) {
if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
}
// Handles dragging for the *last submitted ImGui item*. void BitrateDisplay(uint32_t bitrate) {
// `reset_on_deactivate` should be false when the caller needs to know whether a const int num_of_digits = CountDigits(static_cast<int>(bitrate));
// deactivation was a click (no drag) vs a drag-release (dragging==true). if (num_of_digits <= 3) {
inline void HandleWindowDragForLastItem(SDL_Window* window, bool& dragging, ImGui::Text("%u bps", bitrate);
float& start_mouse_x, } else if (num_of_digits > 3 && num_of_digits <= 6) {
float& start_mouse_y, int& start_win_x, ImGui::Text("%u kbps", bitrate / 1000);
int& start_win_y, } else {
bool reset_on_deactivate = true) { ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
if (!window) {
return;
} }
}
if (ImGui::IsItemActivated()) { std::string FormatBytes(uint64_t bytes) {
SDL_GetGlobalMouseState(&start_mouse_x, &start_mouse_y); char buf[64];
SDL_GetWindowPosition(window, &start_win_x, &start_win_y); if (bytes < 1024ULL) {
dragging = false; std::snprintf(buf, sizeof(buf), "%llu B", (unsigned long long)bytes);
} } else if (bytes < 1024ULL * 1024ULL) {
std::snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
if (ImGui::IsItemActive()) { } else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
if (!dragging && std::snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
ImGui::IsMouseDragging(ImGuiMouseButton_Left, kDragThresholdPx)) { } else {
dragging = true; std::snprintf(buf, sizeof(buf), "%.2f GB",
} bytes / (1024.0 * 1024.0 * 1024.0));
if (dragging) {
float mouse_x = 0.0f;
float mouse_y = 0.0f;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
const int dx = (int)(mouse_x - start_mouse_x);
const int dy = (int)(mouse_y - start_mouse_y);
SDL_SetWindowPosition(window, start_win_x + dx, start_win_y + dy);
}
}
if (reset_on_deactivate && ImGui::IsItemDeactivated()) {
dragging = false;
} }
return std::string(buf);
} }
} // namespace } // namespace
@@ -50,152 +49,331 @@ int Render::ServerWindow() {
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_), ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("##server_window", nullptr, ImGui::Begin("##server_window", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse); ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
// Collapsed mode: no buttons; drag to move; click to restore. server_window_title_bar_height_ = title_bar_height_;
if (server_window_collapsed_) {
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
ImGui::InvisibleButton("##server_collapsed_area",
ImVec2(server_window_width_, server_window_height_));
HandleWindowDragForLastItem(server_window_, ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
server_window_collapsed_dragging_,
server_window_collapsed_drag_start_mouse_x_,
server_window_collapsed_drag_start_mouse_y_,
server_window_collapsed_drag_start_win_x_,
server_window_collapsed_drag_start_win_y_,
/*reset_on_deactivate=*/false);
const bool request_restore =
ImGui::IsItemDeactivated() && !server_window_collapsed_dragging_;
if (ImGui::IsItemDeactivated()) {
server_window_collapsed_dragging_ = false;
}
if (request_restore && server_window_) {
int w = 0;
int x = 0;
int y = 0;
int h = 0;
SDL_GetWindowSize(server_window_, &w, &h);
SDL_GetWindowPosition(server_window_, &x, &y);
const int normal_h = server_window_normal_height_;
SDL_SetWindowSize(server_window_, w, normal_h);
SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = false;
}
ImGui::End();
return 0;
}
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild( ImGui::BeginChild(
"ServerTitleBar", "ServerTitleBar",
ImVec2(server_window_width_, server_window_title_bar_height_), ImVec2(server_window_width_, server_window_title_bar_height_),
ImGuiChildFlags_Border, ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus); ImGuiWindowFlags_NoBringToFrontOnFocus);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
float server_title_bar_button_width = server_window_title_bar_height_; float server_title_bar_button_width = server_window_title_bar_height_;
float server_title_bar_button_height = server_window_title_bar_height_; float server_title_bar_button_height = server_window_title_bar_height_;
// Drag area: the title bar excluding the right-side buttons. // Collapse/expand toggle button (FontAwesome icon).
{ {
const float drag_w =
server_window_width_ - server_title_bar_button_width * 2;
const float drag_h = server_title_bar_button_height;
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f)); ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
ImGui::InvisibleButton("##server_title_drag_area", ImVec2(drag_w, drag_h));
HandleWindowDragForLastItem( ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
server_window_, server_window_dragging_, ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
server_window_drag_start_mouse_x_, server_window_drag_start_mouse_y_, ImGui::PushStyleColor(ImGuiCol_ButtonActive,
server_window_drag_start_win_x_, server_window_drag_start_win_y_); ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
}
float minimize_button_pos_x = ImGui::SetWindowFontScale(0.5f);
server_window_width_ - server_title_bar_button_width * 2; const char* icon =
ImGui::SetCursorPos(ImVec2(minimize_button_pos_x, 0.0f)); server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); std::string toggle_label = std::string(icon) + "##server_toggle";
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
float minimize_pos_x = bool toggle_clicked = RoundedCornerButton(
minimize_button_pos_x + server_title_bar_button_width * 0.33f; toggle_label.c_str(),
float minimize_pos_y = server_title_bar_button_height * 0.5f; ImVec2(server_title_bar_button_width, server_title_bar_button_height),
std::string server_minimize_button = "##minimize"; // ICON_FA_MINUS; 8.5f, ImDrawFlags_RoundCornersTopLeft, true, IM_COL32(0, 0, 0, 0),
if (ImGui::Button(server_minimize_button.c_str(), IM_COL32(0, 0, 0, 25), IM_COL32(255, 255, 255, 255));
ImVec2(server_title_bar_button_width, if (toggle_clicked) {
server_title_bar_button_height))) { if (server_window_) {
if (server_window_) { int w = 0;
int w = 0; int h = 0;
int h = 0; int x = 0;
int x = 0; int y = 0;
int y = 0; SDL_GetWindowSize(server_window_, &w, &h);
SDL_GetWindowSize(server_window_, &w, &h); SDL_GetWindowPosition(server_window_, &x, &y);
SDL_GetWindowPosition(server_window_, &x, &y);
const int collapsed_h = (int)server_window_title_bar_height_; if (server_window_collapsed_) {
// Collapse upward: keep top edge stable. const int normal_h = server_window_normal_height_;
SDL_SetWindowSize(server_window_, w, collapsed_h); SDL_SetWindowSize(server_window_, w, normal_h);
SDL_SetWindowPosition(server_window_, x, y); SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = true; server_window_collapsed_ = false;
} else {
const int collapsed_h = (int)server_window_title_bar_height_;
// Collapse upward: keep top edge stable.
SDL_SetWindowSize(server_window_, w, collapsed_h);
SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = true;
}
}
} }
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
} }
draw_list->AddLine(
ImVec2(minimize_pos_x, minimize_pos_y),
ImVec2(minimize_pos_x + server_title_bar_button_width * 0.33f,
minimize_pos_y),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(3);
float xmark_button_pos_x =
server_window_width_ - server_title_bar_button_width;
ImGui::SetCursorPos(ImVec2(xmark_button_pos_x, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0, 0, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0, 0, 0.5f));
float xmark_pos_x = xmark_button_pos_x + server_title_bar_button_width * 0.5f;
float xmark_pos_y = server_title_bar_button_height * 0.5f;
float xmark_size = server_title_bar_button_width * 0.33f;
std::string server_close_button = "##xmark"; // ICON_FA_XMARK;
if (ImGui::Button(server_close_button.c_str(),
ImVec2(server_title_bar_button_width,
server_title_bar_button_height))) {
LOG_ERROR("Close button clicked");
LeaveConnection(peer_, self_hosted_id_);
}
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f,
xmark_pos_y + xmark_size / 2 - 0.5f),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(
ImVec2(xmark_pos_x + xmark_size / 2 - 1.75f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x - xmark_size / 2, xmark_pos_y + xmark_size / 2 - 1.0f),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(3);
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopStyleVar(); ImGui::PopStyleVar(2);
ImGui::PopStyleColor(); ImGui::PopStyleColor();
RemoteClientInfoWindow();
ImGui::End(); ImGui::End();
return 0; return 0;
} }
} // namespace crossdesk
int Render::RemoteClientInfoWindow() {
float remote_client_info_window_width = server_window_width_ * 0.8f;
float remote_client_info_window_height =
(server_window_height_ - server_window_title_bar_height_) * 0.9f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
ImGui::BeginChild(
"RemoteClientInfoWindow",
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
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);
}
auto find_display_name_by_remote_id =
[&remote_entries](const std::string& remote_id) -> std::string {
for (const auto& entry : remote_entries) {
if (entry.first == remote_id) {
return entry.second;
}
}
return {};
};
if (!selected_server_remote_id_.empty() &&
find_display_name_by_remote_id(selected_server_remote_id_).empty()) {
selected_server_remote_id_.clear();
selected_server_remote_hostname_.clear();
}
if (selected_server_remote_id_.empty() && !remote_entries.empty()) {
selected_server_remote_id_ = remote_entries.front().first;
}
if (!selected_server_remote_id_.empty()) {
selected_server_remote_hostname_ =
find_display_name_by_remote_id(selected_server_remote_id_);
}
ImGui::SetWindowFontScale(font_scale);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::controller[localization_language_index_].c_str());
ImGui::SameLine();
const char* selected_preview = "-";
if (!selected_server_remote_hostname_.empty()) {
selected_preview = selected_server_remote_hostname_.c_str();
} else if (!remote_client_id_.empty()) {
selected_preview = remote_client_id_.c_str();
}
ImGui::SetNextItemWidth(remote_client_info_window_width *
(localization_language_index_ == 0 ? 0.68f : 0.62f));
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
ImGui::AlignTextToFramePadding();
if (ImGui::BeginCombo("##server_remote_id", selected_preview)) {
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
for (int i = 0; i < static_cast<int>(remote_entries.size()); i++) {
const bool selected =
(remote_entries[i].first == selected_server_remote_id_);
if (ImGui::Selectable(remote_entries[i].second.c_str(), selected)) {
selected_server_remote_id_ = remote_entries[i].first;
selected_server_remote_hostname_ = remote_entries[i].second;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::Separator();
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;
ImGui::Text(
"%s",
localization::connection_status[localization_language_index_].c_str());
ImGui::SameLine();
switch (status) {
case ConnectionStatus::Connected:
ImGui::Text(
"%s",
localization::p2p_connected[localization_language_index_].c_str());
break;
case ConnectionStatus::Connecting:
ImGui::Text(
"%s",
localization::p2p_connecting[localization_language_index_].c_str());
break;
case ConnectionStatus::Disconnected:
ImGui::Text("%s",
localization::p2p_disconnected[localization_language_index_]
.c_str());
break;
case ConnectionStatus::Failed:
ImGui::Text(
"%s",
localization::p2p_failed[localization_language_index_].c_str());
break;
case ConnectionStatus::Closed:
ImGui::Text(
"%s",
localization::p2p_closed[localization_language_index_].c_str());
break;
default:
ImGui::Text(
"%s",
localization::p2p_failed[localization_language_index_].c_str());
break;
}
}
ImGui::Separator();
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s", localization::file_transfer[localization_language_index_].c_str());
ImGui::SameLine();
if (ImGui::Button(
localization::select_file[localization_language_index_].c_str())) {
std::string title = localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title);
LOG_INFO("Selected file path: {}", path.c_str());
ProcessSelectedFile(path, nullptr, file_label_, selected_server_remote_id_);
}
if (file_transfer_.file_transfer_window_visible_) {
ImGui::SameLine();
const bool is_sending = file_transfer_.file_sending_.load();
if (is_sending) {
// Simple animation: cycle icon every 0.5s while sending.
static const char* kFileTransferIcons[] = {ICON_FA_ANGLE_UP,
ICON_FA_ANGLES_UP};
const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) %
(static_cast<int>(sizeof(kFileTransferIcons) /
sizeof(kFileTransferIcons[0])));
ImGui::Text("%s", kFileTransferIcons[icon_index]);
} else {
// Completed.
ImGui::Text("%s", ICON_FA_CHECK);
}
if (ImGui::IsItemHovered()) {
const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load();
const uint64_t total_bytes = file_transfer_.file_total_bytes_.load();
const uint32_t rate_bps = file_transfer_.file_send_rate_bps_.load();
float progress = 0.0f;
if (total_bytes > 0) {
progress =
static_cast<float>(sent_bytes) / static_cast<float>(total_bytes);
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
}
std::string current_file_name;
const uint32_t current_file_id = file_transfer_.current_file_id_.load();
if (current_file_id != 0) {
std::lock_guard<std::mutex> lock(
file_transfer_.file_transfer_list_mutex_);
for (const auto& info : file_transfer_.file_transfer_list_) {
if (info.file_id == current_file_id) {
current_file_name = info.file_name;
break;
}
}
}
ImGui::BeginTooltip();
if (server_windows_system_chinese_font_) {
ImGui::PushFont(server_windows_system_chinese_font_);
}
ImGui::SetWindowFontScale(0.5f);
if (!current_file_name.empty()) {
ImGui::Text("%s", current_file_name.c_str());
}
if (total_bytes > 0) {
const std::string sent_str = FormatBytes(sent_bytes);
const std::string total_str = FormatBytes(total_bytes);
ImGui::Text("%s / %s", sent_str.c_str(), total_str.c_str());
}
const float text_height = ImGui::GetTextLineHeight();
char overlay[32];
std::snprintf(overlay, sizeof(overlay), "%.1f%%", progress * 100.0f);
ImGui::ProgressBar(progress, ImVec2(180.0f, text_height), overlay);
BitrateDisplay(rate_bps);
ImGui::SetWindowFontScale(1.0f);
if (server_windows_system_chinese_font_) {
ImGui::PopFont();
}
ImGui::EndTooltip();
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::EndChild();
ImGui::SameLine();
float close_connection_button_width = server_window_width_ * 0.1f;
float close_connection_button_height = remote_client_info_window_height;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_);
ImGui::SetWindowFontScale(font_scale);
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
close_connection_button_height))) {
if (peer_ && !selected_server_remote_id_.empty()) {
LeaveConnection(peer_, selected_server_remote_id_.c_str());
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk

View File

@@ -31,6 +31,34 @@ void Render::DrawConnectionStatusText(
} }
} }
void Render::DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props) {
if (!props->connection_established_ ||
props->connection_status_ != ConnectionStatus::Connected) {
return;
}
bool has_valid_frame = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
has_valid_frame = props->stream_texture_ != nullptr &&
props->video_width_ > 0 && props->video_height_ > 0 &&
props->front_frame_ && !props->front_frame_->empty();
}
if (has_valid_frame) {
return;
}
const std::string& text =
localization::receiving_screen[localization_language_index_];
ImVec2 size = ImGui::GetWindowSize();
ImVec2 text_size = ImGui::CalcTextSize(text.c_str());
ImGui::SetCursorPos(
ImVec2((size.x - text_size.x) * 0.5f, (size.y - text_size.y) * 0.5f));
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str());
}
void Render::CloseTab(decltype(client_properties_)::iterator& it) { void Render::CloseTab(decltype(client_properties_)::iterator& it) {
// std::unique_lock lock(client_properties_mutex_); // std::unique_lock lock(client_properties_mutex_);
if (it != client_properties_.end()) { if (it != client_properties_.end()) {
@@ -117,7 +145,9 @@ int Render::StreamWindow() {
ImGui::SetWindowFontScale(0.6f); ImGui::SetWindowFontScale(0.6f);
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(stream_window_width_, stream_window_height_), ImVec2(stream_window_width_,
stream_window_height_ -
(fullscreen_button_pressed_ ? 0 : title_bar_height_)),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_), ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_),
@@ -138,10 +168,12 @@ int Render::StreamWindow() {
UpdateRenderRect(); UpdateRenderRect();
ControlWindow(props); ControlWindow(props);
// Show file transfer window if needed // Show file transfer window if needed
FileTransferWindow(props); FileTransferWindow(props);
DrawReceivingScreenText(props);
focused_remote_id_ = props->remote_id_; focused_remote_id_ = props->remote_id_;
if (!props->peer_) { if (!props->peer_) {
@@ -151,12 +183,12 @@ int Render::StreamWindow() {
// std::unique_lock unique_lock(client_properties_mutex_); // std::unique_lock unique_lock(client_properties_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase); auto erase_it = client_properties_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) { if (erase_it != client_properties_.end()) {
erase_it = client_properties_.erase(erase_it); // Ensure we flush pending STREAM_REFRESH_EVENT events and
if (client_properties_.empty()) { // clean up peer resources before erasing the entry, otherwise
SDL_Event event; // SDL events may still hold raw pointers to freed
event.type = SDL_EVENT_QUIT; // SubStreamWindowProperties (including video_frame_mutex_),
SDL_PushEvent(&event); // leading to std::system_error when locking.
} CloseTab(erase_it);
} }
} }
// lock.lock(); // lock.lock();
@@ -217,7 +249,9 @@ int Render::StreamWindow() {
if (props->tab_selected_) { if (props->tab_selected_) {
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(stream_window_width_, stream_window_height_), ImVec2(stream_window_width_,
stream_window_height_ -
(fullscreen_button_pressed_ ? 0 : title_bar_height_)),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
@@ -236,10 +270,12 @@ int Render::StreamWindow() {
UpdateRenderRect(); UpdateRenderRect();
ControlWindow(props); ControlWindow(props);
// Show file transfer window if needed // Show file transfer window if needed
FileTransferWindow(props); FileTransferWindow(props);
DrawReceivingScreenText(props);
ImGui::End(); ImGui::End();
if (!props->peer_) { if (!props->peer_) {
@@ -251,12 +287,7 @@ int Render::StreamWindow() {
// std::unique_lock unique_lock(client_properties_mutex_); // std::unique_lock unique_lock(client_properties_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase); auto erase_it = client_properties_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) { if (erase_it != client_properties_.end()) {
client_properties_.erase(erase_it); CloseTab(erase_it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
} }
} }
// lock.lock(); // lock.lock();

View File

@@ -1,5 +1,4 @@
#include <algorithm> #include <algorithm>
#include <cstdlib>
#include <string> #include <string>
#include "layout.h" #include "layout.h"
@@ -77,8 +76,8 @@ int Render::UpdateNotificationWindow() {
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin( ImGui::Begin(
localization::notification[localization_language_index_].c_str(), localization::notification[localization_language_index_].c_str(),
nullptr, nullptr,
@@ -104,6 +103,7 @@ int Render::UpdateNotificationWindow() {
localization::access_website[localization_language_index_] + localization::access_website[localization_language_index_] +
"https://crossdesk.cn"; "https://crossdesk.cn";
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
ImGui::SetCursorPosX(update_notification_window_width * 0.1f);
Hyperlink(download_text, "https://crossdesk.cn", Hyperlink(download_text, "https://crossdesk.cn",
update_notification_window_width); update_notification_window_width);
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
@@ -121,7 +121,7 @@ int Render::UpdateNotificationWindow() {
ImGui::BeginChild( ImGui::BeginChild(
"ScrollableContent", "ScrollableContent",
ImVec2(update_notification_window_width * 0.9f, scrollable_height), ImVec2(update_notification_window_width * 0.9f, scrollable_height),
ImGuiChildFlags_Border, ImGuiWindowFlags_None); ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
// set text wrap position to current available width (accounts for // set text wrap position to current available width (accounts for
// scrollbar) // scrollbar)
@@ -184,14 +184,7 @@ int Render::UpdateNotificationWindow() {
localization::update[localization_language_index_].c_str())) { localization::update[localization_language_index_].c_str())) {
// open download page // open download page
std::string url = "https://crossdesk.cn"; std::string url = "https://crossdesk.cn";
#if defined(_WIN32) OpenUrl(url);
std::string cmd = "start " + url;
#elif defined(__APPLE__)
std::string cmd = "open " + url;
#else
std::string cmd = "xdg-open " + url;
#endif
system(cmd.c_str());
show_update_notification_window_ = false; show_update_notification_window_ = false;
} }

View File

@@ -62,4 +62,4 @@ std::shared_ptr<spdlog::logger> get_logger() {
return g_logger; return g_logger;
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -40,20 +40,6 @@ std::filesystem::path PathManager::GetLogPath() {
#endif #endif
} }
std::filesystem::path PathManager::GetCertPath() {
#ifdef _WIN32
// %APPDATA%\AppName\Certs
return GetKnownFolder(FOLDERID_RoamingAppData) / app_name_ / "certs";
#elif __APPLE__
// $HOME/Library/Application Support/AppName/certs
return GetHome() + "/Library/Application Support/" + app_name_ + "/certs";
#else
// $XDG_CONFIG_HOME/AppName/certs
return GetEnvOrDefault("XDG_CONFIG_HOME", GetHome() + "/.config") /
app_name_ / "certs";
#endif
}
bool PathManager::CreateDirectories(const std::filesystem::path& p) { bool PathManager::CreateDirectories(const std::filesystem::path& p) {
std::error_code ec; std::error_code ec;
bool created = std::filesystem::create_directories(p, ec); bool created = std::filesystem::create_directories(p, ec);

View File

@@ -26,8 +26,6 @@ class PathManager {
std::filesystem::path GetLogPath(); std::filesystem::path GetLogPath();
std::filesystem::path GetCertPath();
bool CreateDirectories(const std::filesystem::path& p); bool CreateDirectories(const std::filesystem::path& p);
private: private:

View File

@@ -138,6 +138,10 @@ int ScreenCapturerX11::SwitchTo(int monitor_index) {
return 0; return 0;
} }
int ScreenCapturerX11::ResetToInitialMonitor() {
monitor_index_ = initial_monitor_index_;
return 0;
}
std::vector<DisplayInfo> ScreenCapturerX11::GetDisplayInfoList() { std::vector<DisplayInfo> ScreenCapturerX11::GetDisplayInfoList() {
return display_info_list_; return display_info_list_;
} }

View File

@@ -42,6 +42,7 @@ class ScreenCapturerX11 : public ScreenCapturer {
int Resume(int monitor_index) override; int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override; int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override; std::vector<DisplayInfo> GetDisplayInfoList() override;
@@ -62,6 +63,7 @@ class ScreenCapturerX11 : public ScreenCapturer {
std::atomic<bool> running_{false}; std::atomic<bool> running_{false};
std::atomic<bool> paused_{false}; std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0}; std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true}; std::atomic<bool> show_cursor_{true};
int fps_ = 60; int fps_ = 60;
cb_desktop_data callback_; cb_desktop_data callback_;

View File

@@ -62,6 +62,13 @@ int ScreenCapturerSck::SwitchTo(int monitor_index) {
return -1; return -1;
} }
int ScreenCapturerSck::ResetToInitialMonitor() {
if (screen_capturer_sck_impl_) {
return screen_capturer_sck_impl_->ResetToInitialMonitor();
}
return -1;
}
std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() { std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() {
if (screen_capturer_sck_impl_) { if (screen_capturer_sck_impl_) {
return screen_capturer_sck_impl_->GetDisplayInfoList(); return screen_capturer_sck_impl_->GetDisplayInfoList();

View File

@@ -33,6 +33,7 @@ class ScreenCapturerSck : public ScreenCapturer {
int Resume(int monitor_index) override; int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override; int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override; std::vector<DisplayInfo> GetDisplayInfoList() override;

View File

@@ -70,6 +70,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
int Resume(int monitor_index) override { return 0; } int Resume(int monitor_index) override { return 0; }
std::vector<DisplayInfo> GetDisplayInfoList() override { return display_info_list_; } std::vector<DisplayInfo> GetDisplayInfoList() override { return display_info_list_; }
int ResetToInitialMonitor() override;
private: private:
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
@@ -113,6 +114,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
// Currently selected display, or 0 if the full desktop is selected. This capturer does not // Currently selected display, or 0 if the full desktop is selected. This capturer does not
// support full-desktop capture, and will fall back to the first display. // support full-desktop capture, and will fall back to the first display.
CGDirectDisplayID current_display_ = 0; CGDirectDisplayID current_display_ = 0;
int initial_monitor_index_ = 0;
}; };
std::string GetDisplayName(CGDirectDisplayID display_id) { std::string GetDisplayName(CGDirectDisplayID display_id) {
@@ -261,6 +263,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
display_id_name_map_[display_id] = name; display_id_name_map_[display_id] = name;
} }
initial_monitor_index_ = 0;
return 0; return 0;
} }
@@ -295,6 +298,25 @@ int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
return 0; return 0;
} }
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
int target = initial_monitor_index_;
if (display_info_list_.empty()) return -1;
CGDirectDisplayID target_display = display_id_map_[target];
if (current_display_ == target_display) return 0;
if (stream_) {
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
std::lock_guard<std::mutex> lock(lock_);
stream_ = nil;
current_display_ = target_display;
StartOrReconfigureCapturer();
}];
} else {
current_display_ = target_display;
StartOrReconfigureCapturer();
}
return 0;
}
int ScreenCapturerSckImpl::Destroy() { int ScreenCapturerSckImpl::Destroy() {
std::lock_guard<std::mutex> lock(lock_); std::lock_guard<std::mutex> lock(lock_);
if (stream_) { if (stream_) {

View File

@@ -31,6 +31,7 @@ class ScreenCapturer {
virtual std::vector<DisplayInfo> GetDisplayInfoList() = 0; virtual std::vector<DisplayInfo> GetDisplayInfoList() = 0;
virtual int SwitchTo(int monitor_index) = 0; virtual int SwitchTo(int monitor_index) = 0;
virtual int ResetToInitialMonitor() = 0;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif

View File

@@ -8,7 +8,7 @@
#define _SCREEN_CAPTURER_FACTORY_H_ #define _SCREEN_CAPTURER_FACTORY_H_
#ifdef _WIN32 #ifdef _WIN32
#include "screen_capturer_wgc.h" #include "screen_capturer_win.h"
#elif __linux__ #elif __linux__
#include "screen_capturer_x11.h" #include "screen_capturer_x11.h"
#elif __APPLE__ #elif __APPLE__
@@ -25,7 +25,7 @@ class ScreenCapturerFactory {
public: public:
ScreenCapturer* Create() { ScreenCapturer* Create() {
#ifdef _WIN32 #ifdef _WIN32
return new ScreenCapturerWgc(); return new ScreenCapturerWin();
#elif __linux__ #elif __linux__
return new ScreenCapturerX11(); return new ScreenCapturerX11();
#elif __APPLE__ #elif __APPLE__
@@ -37,4 +37,4 @@ class ScreenCapturerFactory {
} }
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif

View File

@@ -0,0 +1,356 @@
#include "screen_capturer_dxgi.h"
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::string WideToUtf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
int size_needed = WideCharToMultiByte(
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
size_needed, nullptr, nullptr);
return result;
}
std::string CleanDisplayName(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
return name;
}
} // namespace
ScreenCapturerDxgi::ScreenCapturerDxgi() {}
ScreenCapturerDxgi::~ScreenCapturerDxgi() {
Stop();
Destroy();
}
int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_ = cb;
if (!callback_) {
LOG_ERROR("DXGI: callback is null");
return -1;
}
if (!InitializeDxgi()) {
LOG_ERROR("DXGI: initialize DXGI failed");
return -2;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("DXGI: no displays found");
return -3;
}
monitor_index_ = 0;
initial_monitor_index_ = monitor_index_;
return 0;
}
int ScreenCapturerDxgi::Destroy() {
Stop();
ReleaseDuplication();
outputs_.clear();
d3d_context_.Reset();
d3d_device_.Reset();
dxgi_factory_.Reset();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_width_ = 0;
nv12_height_ = 0;
}
return 0;
}
int ScreenCapturerDxgi::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
return -1;
}
paused_ = false;
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerDxgi::Stop() {
if (!running_) return 0;
running_ = false;
if (thread_.joinable()) thread_.join();
ReleaseDuplication();
return 0;
}
int ScreenCapturerDxgi::Pause(int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerDxgi::Resume(int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
return -1;
}
paused_ = true;
monitor_index_ = monitor_index;
ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
return -2;
}
paused_ = false;
LOG_INFO("DXGI: switched to monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
int ScreenCapturerDxgi::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
if (monitor_index_ == target) return 0;
if (running_) {
paused_ = true;
monitor_index_ = target;
ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) {
paused_ = false;
return -2;
}
paused_ = false;
LOG_INFO("DXGI: reset to initial monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
} else {
monitor_index_ = target;
}
return 0;
}
bool ScreenCapturerDxgi::InitializeDxgi() {
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
D3D_FEATURE_LEVEL feature_levels[] = {
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0};
D3D_FEATURE_LEVEL out_level{};
HRESULT hr = D3D11CreateDevice(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, feature_levels,
ARRAYSIZE(feature_levels), D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
&out_level, d3d_context_.GetAddressOf());
if (FAILED(hr)) {
hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, flags,
feature_levels, ARRAYSIZE(feature_levels),
D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
&out_level, d3d_context_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: D3D11CreateDevice failed, hr={}", (int)hr);
return false;
}
}
hr = CreateDXGIFactory1(
__uuidof(IDXGIFactory1),
reinterpret_cast<void**>(dxgi_factory_.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)hr);
return false;
}
return true;
}
void ScreenCapturerDxgi::EnumerateDisplays() {
display_info_list_.clear();
outputs_.clear();
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
for (UINT a = 0;
dxgi_factory_->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND;
++a) {
Microsoft::WRL::ComPtr<IDXGIOutput> output;
for (UINT o = 0; adapter->EnumOutputs(o, output.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND;
++o) {
DXGI_OUTPUT_DESC desc{};
if (FAILED(output->GetDesc(&desc))) {
continue;
}
std::string name = CleanDisplayName(desc.DeviceName);
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(desc.Monitor, &mi)) {
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)desc.Monitor, name, is_primary,
mi.rcMonitor.left, mi.rcMonitor.top,
mi.rcMonitor.right, mi.rcMonitor.bottom);
// primary first
if (is_primary)
display_info_list_.insert(display_info_list_.begin(), info);
else
display_info_list_.push_back(info);
outputs_.push_back(output);
}
}
}
}
bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)outputs_.size()) return false;
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
HRESULT hr = outputs_[monitor_index]->QueryInterface(
IID_PPV_ARGS(output1.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: Query IDXGIOutput1 failed, hr={}", (int)hr);
return false;
}
duplication_.Reset();
hr = output1->DuplicateOutput(d3d_device_.Get(), duplication_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: DuplicateOutput failed, hr={}", (int)hr);
return false;
}
staging_.Reset();
return true;
}
void ScreenCapturerDxgi::ReleaseDuplication() {
staging_.Reset();
if (duplication_) {
duplication_->ReleaseFrame();
}
duplication_.Reset();
}
void ScreenCapturerDxgi::CaptureLoop() {
const int timeout_ms = 33;
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!duplication_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
DXGI_OUTDUPL_FRAME_INFO frame_info{};
Microsoft::WRL::ComPtr<IDXGIResource> desktop_resource;
HRESULT hr = duplication_->AcquireNextFrame(
timeout_ms, &frame_info, desktop_resource.GetAddressOf());
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
continue;
}
if (FAILED(hr)) {
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
// attempt to recreate duplication
ReleaseDuplication();
CreateDuplicationForMonitor(monitor_index_);
continue;
}
Microsoft::WRL::ComPtr<ID3D11Texture2D> acquired_tex;
if (desktop_resource) {
hr = desktop_resource->QueryInterface(
IID_PPV_ARGS(acquired_tex.GetAddressOf()));
if (FAILED(hr)) {
duplication_->ReleaseFrame();
continue;
}
} else {
duplication_->ReleaseFrame();
continue;
}
D3D11_TEXTURE2D_DESC src_desc{};
acquired_tex->GetDesc(&src_desc);
if (!staging_) {
D3D11_TEXTURE2D_DESC staging_desc = src_desc;
staging_desc.Usage = D3D11_USAGE_STAGING;
staging_desc.BindFlags = 0;
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
staging_desc.MiscFlags = 0;
hr = d3d_device_->CreateTexture2D(&staging_desc, nullptr,
staging_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateTexture2D staging failed, hr={}", (int)hr);
duplication_->ReleaseFrame();
continue;
}
}
d3d_context_->CopyResource(staging_.Get(), acquired_tex.Get());
D3D11_MAPPED_SUBRESOURCE mapped{};
hr = d3d_context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
if (FAILED(hr)) {
duplication_->ReleaseFrame();
continue;
}
int logical_width = static_cast<int>(src_desc.Width);
int even_width = logical_width & ~1;
int even_height = static_cast<int>(src_desc.Height) & ~1;
if (even_width <= 0 || even_height <= 0) {
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
continue;
}
int nv12_size = even_width * even_height * 3 / 2;
if (!nv12_frame_ || nv12_width_ != even_width ||
nv12_height_ != even_height) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[nv12_size];
nv12_width_ = even_width;
nv12_height_ = even_height;
}
libyuv::ARGBToNV12(static_cast<const uint8_t*>(mapped.pData),
static_cast<int>(mapped.RowPitch), nv12_frame_,
even_width, nv12_frame_ + even_width * even_height,
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());
}
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
}
}
} // namespace crossdesk

View File

@@ -0,0 +1,81 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_DXGI_H_
#define _SCREEN_CAPTURER_DXGI_H_
#include <Windows.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <wrl/client.h>
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "rd_log.h"
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerDxgi : public ScreenCapturer {
public:
ScreenCapturerDxgi();
~ScreenCapturerDxgi();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override {
return display_info_list_;
}
private:
bool InitializeDxgi();
void EnumerateDisplays();
bool CreateDuplicationForMonitor(int monitor_index);
void CaptureLoop();
void ReleaseDuplication();
private:
std::vector<DisplayInfo> display_info_list_;
std::vector<Microsoft::WRL::ComPtr<IDXGIOutput>> outputs_;
Microsoft::WRL::ComPtr<IDXGIFactory1> dxgi_factory_;
Microsoft::WRL::ComPtr<ID3D11Device> d3d_device_;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
std::thread thread_;
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0;
int nv12_height_ = 0;
};
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,217 @@
#include "screen_capturer_gdi.h"
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::string WideToUtf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
int size_needed = WideCharToMultiByte(
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
size_needed, nullptr, nullptr);
return result;
}
std::string CleanDisplayName(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
return name;
}
} // namespace
ScreenCapturerGdi::ScreenCapturerGdi() {}
ScreenCapturerGdi::~ScreenCapturerGdi() {
Stop();
Destroy();
}
BOOL CALLBACK ScreenCapturerGdi::EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
LPARAM data) {
auto displays = reinterpret_cast<std::vector<DisplayInfo>*>(data);
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(hMonitor, &mi)) {
std::string name = CleanDisplayName(mi.szDevice);
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left,
mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
if (is_primary)
displays->insert(displays->begin(), info);
else
displays->push_back(info);
}
return TRUE;
}
void ScreenCapturerGdi::EnumerateDisplays() {
display_info_list_.clear();
EnumDisplayMonitors(nullptr, nullptr, EnumMonitorProc,
(LPARAM)&display_info_list_);
}
int ScreenCapturerGdi::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_ = cb;
if (!callback_) {
LOG_ERROR("GDI: callback is null");
return -1;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("GDI: no displays found");
return -2;
}
monitor_index_ = 0;
initial_monitor_index_ = monitor_index_;
return 0;
}
int ScreenCapturerGdi::Destroy() {
Stop();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_width_ = 0;
nv12_height_ = 0;
}
return 0;
}
int ScreenCapturerGdi::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
paused_ = false;
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerGdi::Stop() {
if (!running_) return 0;
running_ = false;
if (thread_.joinable()) thread_.join();
return 0;
}
int ScreenCapturerGdi::Pause(int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerGdi::Resume(int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerGdi::SwitchTo(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("GDI: invalid monitor index {}", monitor_index);
return -1;
}
monitor_index_ = monitor_index;
LOG_INFO("GDI: switched to monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
int ScreenCapturerGdi::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
monitor_index_ = target;
LOG_INFO("GDI: reset to initial monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
void ScreenCapturerGdi::CaptureLoop() {
int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
HDC screen_dc = GetDC(nullptr);
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!screen_dc) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
const auto& di = display_info_list_[monitor_index_];
int left = di.left;
int top = di.top;
int width = di.width & ~1;
int height = di.height & ~1;
if (width <= 0 || height <= 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
HDC mem_dc = CreateCompatibleDC(screen_dc);
HBITMAP dib =
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
HGDIOBJ old = SelectObject(mem_dc, dib);
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
SRCCOPY | CAPTUREBLT);
if (show_cursor_) {
CURSORINFO ci{};
ci.cbSize = sizeof(CURSORINFO);
if (GetCursorInfo(&ci) && ci.flags == CURSOR_SHOWING && ci.hCursor) {
POINT pt = ci.ptScreenPos;
int cx = pt.x - left;
int cy = pt.y - top;
if (cx >= -64 && cy >= -64 && cx < width + 64 && cy < height + 64) {
DrawIconEx(mem_dc, cx, cy, ci.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
}
}
}
int stride_argb = width * 4;
int nv12_size = width * height * 3 / 2;
if (!nv12_frame_ || nv12_width_ != width || nv12_height_ != height) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[nv12_size];
nv12_width_ = width;
nv12_height_ = height;
}
libyuv::ARGBToNV12(static_cast<const uint8_t*>(bits), stride_argb,
nv12_frame_, width, nv12_frame_ + width * height, width,
width, height);
if (callback_) {
callback_(nv12_frame_, nv12_size, width, height, di.name.c_str());
}
SelectObject(mem_dc, old);
DeleteObject(dib);
DeleteDC(mem_dc);
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
}
ReleaseDC(nullptr, screen_dc);
}
} // namespace crossdesk

View File

@@ -0,0 +1,68 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_GDI_H_
#define _SCREEN_CAPTURER_GDI_H_
#include <Windows.h>
#include <atomic>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "rd_log.h"
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerGdi : public ScreenCapturer {
public:
ScreenCapturerGdi();
~ScreenCapturerGdi();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override {
return display_info_list_;
}
private:
static BOOL CALLBACK EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
LPARAM data);
void EnumerateDisplays();
void CaptureLoop();
private:
std::vector<DisplayInfo> display_info_list_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
std::thread thread_;
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0;
int nv12_height_ = 0;
};
} // namespace crossdesk
#endif

View File

@@ -56,8 +56,6 @@ BOOL WINAPI EnumMonitorProc(HMONITOR hmonitor, [[maybe_unused]] HDC hdc,
} }
} }
if (monitor_info_.dwFlags == DISPLAY_DEVICE_MIRRORING_DRIVER) return true;
return true; return true;
} }
@@ -149,6 +147,7 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
LOG_INFO("Default on monitor {}:{}", monitor_index_, LOG_INFO("Default on monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name); display_info_list_[monitor_index_].name);
initial_monitor_index_ = monitor_index_;
return 0; return 0;
} }
@@ -165,6 +164,8 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
return 4; return 4;
} }
bool any_started = false;
int last_error = 0;
for (int i = 0; i < sessions_.size(); i++) { for (int i = 0; i < sessions_.size(); i++) {
if (sessions_[i].inited_ == false) { if (sessions_[i].inited_ == false) {
LOG_ERROR("Session {} not inited", i); LOG_ERROR("Session {} not inited", i);
@@ -174,17 +175,27 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
if (sessions_[i].running_) { if (sessions_[i].running_) {
LOG_ERROR("Session {} is already running", i); LOG_ERROR("Session {} is already running", i);
} else { } else {
sessions_[i].session_->Start(show_cursor); int ret = sessions_[i].session_->Start(show_cursor);
if (ret != 0) {
LOG_ERROR("Session {} start failed, ret={}", i, ret);
last_error = ret;
continue;
}
if (i != 0) { if (i != 0) {
sessions_[i].session_->Pause(); sessions_[i].session_->Pause();
sessions_[i].paused_ = true; sessions_[i].paused_ = true;
} }
sessions_[i].running_ = true; sessions_[i].running_ = true;
any_started = true;
} }
running_ = true; running_ = running_ || any_started;
} }
if (!any_started) {
LOG_ERROR("WGC: no session started successfully");
return last_error != 0 ? last_error : -1;
}
return 0; return 0;
} }
@@ -257,6 +268,26 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
return 0; return 0;
} }
int ScreenCapturerWgc::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
if (initial_monitor_index_ < 0 ||
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
return -1;
}
if (monitor_index_ == initial_monitor_index_) {
return 0;
}
if (running_) {
Pause(monitor_index_);
}
monitor_index_ = initial_monitor_index_;
LOG_INFO("Reset to initial monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name);
if (running_) {
Resume(monitor_index_);
}
return 0;
}
void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame, void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
int id) { int id) {
if (!running_ || !on_data_) { if (!running_ || !on_data_) {

View File

@@ -34,6 +34,7 @@ class ScreenCapturerWgc : public ScreenCapturer,
std::vector<DisplayInfo> GetDisplayInfoList() { return display_info_list_; } std::vector<DisplayInfo> GetDisplayInfoList() { return display_info_list_; }
int SwitchTo(int monitor_index); int SwitchTo(int monitor_index);
int ResetToInitialMonitor() override;
void OnFrame(const WgcSession::wgc_session_frame& frame, int id); void OnFrame(const WgcSession::wgc_session_frame& frame, int id);
@@ -45,6 +46,7 @@ class ScreenCapturerWgc : public ScreenCapturer,
MONITORINFOEX monitor_info_; MONITORINFOEX monitor_info_;
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
int monitor_index_ = 0; int monitor_index_ = 0;
int initial_monitor_index_ = 0;
private: private:
class WgcSessionInfo { class WgcSessionInfo {
@@ -57,8 +59,8 @@ class ScreenCapturerWgc : public ScreenCapturer,
std::vector<WgcSessionInfo> sessions_; std::vector<WgcSessionInfo> sessions_;
std::atomic_bool running_; std::atomic_bool running_{false};
std::atomic_bool inited_; std::atomic_bool inited_{false};
int fps_ = 60; int fps_ = 60;

View File

@@ -0,0 +1,300 @@
#include "screen_capturer_win.h"
#include <Windows.h>
#include <cmath>
#include <filesystem>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "rd_log.h"
#include "screen_capturer_dxgi.h"
#include "screen_capturer_gdi.h"
#include "wgc_plugin_api.h"
namespace crossdesk {
namespace {
class WgcPluginCapturer final : public ScreenCapturer {
public:
using CreateFn = ScreenCapturer* (*)();
using DestroyFn = void (*)(ScreenCapturer*);
static std::unique_ptr<ScreenCapturer> Create() {
std::filesystem::path plugin_path;
wchar_t module_path[MAX_PATH] = {0};
const DWORD len = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
if (len == 0 || len >= MAX_PATH) {
return nullptr;
}
plugin_path =
std::filesystem::path(module_path).parent_path() / L"wgc_plugin.dll";
HMODULE module = LoadLibraryW(plugin_path.c_str());
if (!module) {
return nullptr;
}
auto create_fn = reinterpret_cast<CreateFn>(
GetProcAddress(module, "CrossDeskCreateWgcCapturer"));
auto destroy_fn = reinterpret_cast<DestroyFn>(
GetProcAddress(module, "CrossDeskDestroyWgcCapturer"));
if (!create_fn || !destroy_fn) {
FreeLibrary(module);
return nullptr;
}
ScreenCapturer* impl = create_fn();
if (!impl) {
FreeLibrary(module);
return nullptr;
}
return std::unique_ptr<ScreenCapturer>(
new WgcPluginCapturer(module, impl, destroy_fn));
}
~WgcPluginCapturer() override {
if (impl_) {
destroy_fn_(impl_);
impl_ = nullptr;
}
if (module_) {
FreeLibrary(module_);
module_ = nullptr;
}
}
int Init(const int fps, cb_desktop_data cb) override {
return impl_ ? impl_->Init(fps, std::move(cb)) : -1;
}
int Destroy() override { return impl_ ? impl_->Destroy() : 0; }
int Start(bool show_cursor) override {
return impl_ ? impl_->Start(show_cursor) : -1;
}
int Stop() override { return impl_ ? impl_->Stop() : 0; }
int Pause(int monitor_index) override {
return impl_ ? impl_->Pause(monitor_index) : -1;
}
int Resume(int monitor_index) override {
return impl_ ? impl_->Resume(monitor_index) : -1;
}
std::vector<DisplayInfo> GetDisplayInfoList() override {
return impl_ ? impl_->GetDisplayInfoList() : std::vector<DisplayInfo>{};
}
int SwitchTo(int monitor_index) override {
return impl_ ? impl_->SwitchTo(monitor_index) : -1;
}
int ResetToInitialMonitor() override {
return impl_ ? impl_->ResetToInitialMonitor() : -1;
}
private:
WgcPluginCapturer(HMODULE module, ScreenCapturer* impl, DestroyFn destroy_fn)
: module_(module), impl_(impl), destroy_fn_(destroy_fn) {}
HMODULE module_ = nullptr;
ScreenCapturer* impl_ = nullptr;
DestroyFn destroy_fn_ = nullptr;
};
} // namespace
ScreenCapturerWin::ScreenCapturerWin() {}
ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
cb_orig_ = cb;
cb_ = [this](unsigned char* data, int size, int w, int h,
const char* display_name) {
std::string mapped_name;
{
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(display_name);
if (it != label_alias_.end())
mapped_name = it->second;
else
mapped_name = display_name;
}
{
std::lock_guard<std::mutex> lock(alias_mutex_);
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
return;
}
}
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
};
int ret = -1;
impl_ = WgcPluginCapturer::Create();
impl_is_wgc_plugin_ = (impl_ != nullptr);
ret = impl_ ? impl_->Init(fps_, cb_) : -1;
if (ret == 0) {
LOG_INFO("Windows capturer: using WGC plugin");
BuildCanonicalFromImpl();
return 0;
}
LOG_WARN("Windows capturer: WGC plugin init failed (ret={}), try DXGI", ret);
impl_.reset();
impl_is_wgc_plugin_ = false;
impl_ = std::make_unique<ScreenCapturerDxgi>();
impl_is_wgc_plugin_ = false;
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
LOG_INFO("Windows capturer: using DXGI Desktop Duplication");
BuildCanonicalFromImpl();
return 0;
}
LOG_WARN("Windows capturer: DXGI init failed (ret={}), fallback to GDI", ret);
impl_.reset();
impl_ = std::make_unique<ScreenCapturerGdi>();
impl_is_wgc_plugin_ = false;
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
LOG_INFO("Windows capturer: using GDI BitBlt");
BuildCanonicalFromImpl();
return 0;
}
LOG_ERROR("Windows capturer: all implementations failed, ret={}", ret);
impl_.reset();
return -1;
}
int ScreenCapturerWin::Destroy() {
if (impl_) {
impl_->Destroy();
impl_.reset();
impl_is_wgc_plugin_ = false;
}
{
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
handle_to_canonical_.clear();
canonical_labels_.clear();
}
return 0;
}
int ScreenCapturerWin::Start(bool show_cursor) {
if (!impl_) return -1;
int ret = impl_->Start(show_cursor);
if (ret == 0) return 0;
LOG_WARN("Windows capturer: Start failed (ret={}), trying fallback", ret);
auto try_init_start = [&](std::unique_ptr<ScreenCapturer> cand) -> bool {
int r = cand->Init(fps_, cb_);
if (r != 0) return false;
int s = cand->Start(show_cursor);
if (s == 0) {
impl_ = std::move(cand);
impl_is_wgc_plugin_ = false;
RebuildAliasesFromImpl();
return true;
}
return false;
};
if (impl_is_wgc_plugin_) {
if (try_init_start(std::make_unique<ScreenCapturerDxgi>())) {
LOG_INFO("Windows capturer: fallback to DXGI");
return 0;
}
if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
LOG_INFO("Windows capturer: fallback to GDI");
return 0;
}
} else if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get())) {
if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
LOG_INFO("Windows capturer: fallback to GDI");
return 0;
}
}
LOG_ERROR("Windows capturer: all fallbacks failed to start");
return ret;
}
int ScreenCapturerWin::Stop() {
if (!impl_) return 0;
return impl_->Stop();
}
int ScreenCapturerWin::Pause(int monitor_index) {
if (!impl_) return -1;
return impl_->Pause(monitor_index);
}
int ScreenCapturerWin::Resume(int monitor_index) {
if (!impl_) return -1;
return impl_->Resume(monitor_index);
}
int ScreenCapturerWin::SwitchTo(int monitor_index) {
if (!impl_) return -1;
return impl_->SwitchTo(monitor_index);
}
int ScreenCapturerWin::ResetToInitialMonitor() {
if (!impl_) return -1;
return impl_->ResetToInitialMonitor();
}
std::vector<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() {
if (!impl_) return {};
return impl_->GetDisplayInfoList();
}
void ScreenCapturerWin::BuildCanonicalFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_);
handle_to_canonical_.clear();
label_alias_.clear();
canonical_displays_ = impl_->GetDisplayInfoList();
canonical_labels_.clear();
for (const auto& di : canonical_displays_) {
handle_to_canonical_[di.handle] = di.name;
canonical_labels_.insert(di.name);
}
}
void ScreenCapturerWin::RebuildAliasesFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
auto current = impl_->GetDisplayInfoList();
auto similar = [&](const DisplayInfo& a, const DisplayInfo& b) {
int dl = std::abs(a.left - b.left);
int dt = std::abs(a.top - b.top);
int dw = std::abs(a.width - b.width);
int dh = std::abs(a.height - b.height);
return dl <= 10 && dt <= 10 && dw <= 20 && dh <= 20;
};
for (const auto& di : current) {
std::string canonical;
auto it = handle_to_canonical_.find(di.handle);
if (it != handle_to_canonical_.end()) {
canonical = it->second;
} else {
for (const auto& c : canonical_displays_) {
if (similar(di, c) || (di.is_primary && c.is_primary)) {
canonical = c.name;
break;
}
}
}
if (!canonical.empty() && canonical != di.name) {
label_alias_[di.name] = canonical;
}
}
}
} // namespace crossdesk

View File

@@ -0,0 +1,56 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_WIN_H_
#define _SCREEN_CAPTURER_WIN_H_
#include <memory>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerWin : public ScreenCapturer {
public:
ScreenCapturerWin();
~ScreenCapturerWin();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
std::unique_ptr<ScreenCapturer> impl_;
bool impl_is_wgc_plugin_ = false;
int fps_ = 60;
cb_desktop_data cb_;
cb_desktop_data cb_orig_;
std::unordered_map<void*, std::string> handle_to_canonical_;
std::unordered_map<std::string, std::string> label_alias_;
std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_;
std::unordered_set<std::string> canonical_labels_;
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl();
};
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,29 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WGC_PLUGIN_API_H_
#define _WGC_PLUGIN_API_H_
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturer;
}
#if defined(_WIN32) && defined(CROSSDESK_WGC_PLUGIN_BUILD)
#define CROSSDESK_WGC_PLUGIN_API __declspec(dllexport)
#else
#define CROSSDESK_WGC_PLUGIN_API
#endif
extern "C" {
CROSSDESK_WGC_PLUGIN_API crossdesk::ScreenCapturer*
CrossDeskCreateWgcCapturer();
CROSSDESK_WGC_PLUGIN_API void CrossDeskDestroyWgcCapturer(
crossdesk::ScreenCapturer* capturer);
}
#endif

View File

@@ -0,0 +1,13 @@
#include "screen_capturer_wgc.h"
#include "wgc_plugin_api.h"
extern "C" {
crossdesk::ScreenCapturer* CrossDeskCreateWgcCapturer() {
return new crossdesk::ScreenCapturerWgc();
}
void CrossDeskDestroyWgcCapturer(crossdesk::ScreenCapturer* capturer) {
delete capturer;
}
}

View File

@@ -4,13 +4,14 @@
#include <atomic> #include <atomic>
#include <functional> #include <functional>
#include <iostream>
#include <memory> #include <memory>
#define CHECK_INIT \ #include "rd_log.h"
if (!is_initialized_) { \
std::cout << "AE_NEED_INIT" << std::endl; \ #define CHECK_INIT \
return 4; \ if (!is_initialized_) { \
LOG_ERROR("AE_NEED_INIT"); \
return 4; \
} }
#define CHECK_CLOSED \ #define CHECK_CLOSED \
@@ -64,6 +65,7 @@ int WgcSessionImpl::Start(bool show_cursor) {
CHECK_INIT; CHECK_INIT;
try { try {
last_show_cursor_ = show_cursor;
if (!capture_session_) { if (!capture_session_) {
auto current_size = capture_item_.Size(); auto current_size = capture_item_.Size();
capture_framepool_ = capture_framepool_ =
@@ -89,13 +91,12 @@ int WgcSessionImpl::Start(bool show_cursor) {
// we need to test the performance later // we need to test the performance later
// loop_ = std::thread(std::bind(&WgcSessionImpl::message_func, this)); // loop_ = std::thread(std::bind(&WgcSessionImpl::message_func, this));
capture_session_.StartCapture();
capture_session_.IsCursorCaptureEnabled(show_cursor); capture_session_.IsCursorCaptureEnabled(show_cursor);
capture_session_.StartCapture();
error = 0; error = 0;
} catch (winrt::hresult_error) { } catch (winrt::hresult_error) {
std::cout << "AE_WGC_CREATE_CAPTURER_FAILED" << std::endl; LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86; return 86;
} catch (...) { } catch (...) {
return 86; return 86;
@@ -246,8 +247,15 @@ void WgcSessionImpl::OnFrame(
auto frame_captured = auto frame_captured =
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface()); GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
if (!d3d11_texture_mapped_ || is_new_size) if (!d3d11_texture_mapped_ || is_new_size) {
CreateMappedTexture(frame_captured); HRESULT tex_hr = CreateMappedTexture(frame_captured);
if (FAILED(tex_hr)) {
OutputDebugStringW(
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
.c_str());
return;
}
}
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(), d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
frame_captured.get()); frame_captured.get());
@@ -262,6 +270,7 @@ void WgcSessionImpl::OnFrame(
if (FAILED(hr)) { if (FAILED(hr)) {
OutputDebugStringW( OutputDebugStringW(
(L"map resource failed: " + std::to_wstring(hr)).c_str()); (L"map resource failed: " + std::to_wstring(hr)).c_str());
return;
} }
// copy data from map_result.pData // copy data from map_result.pData
@@ -290,14 +299,31 @@ void WgcSessionImpl::OnFrame(
void WgcSessionImpl::OnClosed( void WgcSessionImpl::OnClosed(
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&, winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable const&) { winrt::Windows::Foundation::IInspectable const&) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed"); std::lock_guard locker(lock_);
try {
CleanUp();
is_initialized_ = false;
if (Initialize() == 0) {
int ret = Start(last_show_cursor_);
if (ret == 0) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
} else {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: recover Start failed");
}
} else {
OutputDebugStringW(
L"WgcSessionImpl::OnClosed: recover Initialize failed");
}
} catch (...) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: exception during recover");
}
} }
int WgcSessionImpl::Initialize() { int WgcSessionImpl::Initialize() {
if (is_initialized_) return 0; if (is_initialized_) return 0;
if (!(d3d11_direct_device_ = CreateD3D11Device())) { if (!(d3d11_direct_device_ = CreateD3D11Device())) {
std::cout << "AE_D3D_CREATE_DEVICE_FAILED" << std::endl; LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
return 1; return 1;
} }
@@ -313,7 +339,7 @@ int WgcSessionImpl::Initialize() {
d3d11_device->GetImmediateContext(d3d11_device_context_.put()); d3d11_device->GetImmediateContext(d3d11_device_context_.put());
} catch (winrt::hresult_error) { } catch (winrt::hresult_error) {
std::cout << "AE_WGC_CREATE_CAPTURER_FAILED" << std::endl; LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86; return 86;
} catch (...) { } catch (...) {
return 86; return 86;
@@ -378,4 +404,4 @@ LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
// ::CloseWindow(hwnd_); // ::CloseWindow(hwnd_);
// ::DestroyWindow(hwnd_); // ::DestroyWindow(hwnd_);
// } // }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -79,6 +79,7 @@ class WgcSessionImpl : public WgcSession {
bool is_initialized_ = false; bool is_initialized_ = false;
bool is_running_ = false; bool is_running_ = false;
bool is_paused_ = false; bool is_paused_ = false;
bool last_show_cursor_ = false;
wgc_session_observer* observer_ = nullptr; wgc_session_observer* observer_ = nullptr;
@@ -116,4 +117,4 @@ class WgcSessionImpl : public WgcSession {
// return result; // return result;
// } // }
} // namespace crossdesk } // namespace crossdesk
#endif #endif

View File

@@ -452,11 +452,17 @@ static void MonitorThreadFunc() {
LOG_INFO("Clipboard event monitoring started (Linux XFixes)"); LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
XEvent event; XEvent event;
constexpr int kEventPollIntervalMs = 20;
while (g_monitoring.load()) { while (g_monitoring.load()) {
XNextEvent(g_x11_display, &event); // Avoid blocking on XNextEvent so StopMonitoring() can stop quickly.
if (event.type == event_base + XFixesSelectionNotify) { while (g_monitoring.load() && XPending(g_x11_display) > 0) {
HandleClipboardChange(); XNextEvent(g_x11_display, &event);
if (event.type == event_base + XFixesSelectionNotify) {
HandleClipboardChange();
}
} }
std::this_thread::sleep_for(
std::chrono::milliseconds(kEventPollIntervalMs));
} }
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0); XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0);

View File

@@ -97,6 +97,14 @@ class FileReceiver {
const std::filesystem::path& OutputDir() const { return output_dir_; } const std::filesystem::path& OutputDir() const { return output_dir_; }
void SetOutputDir(const std::filesystem::path& dir) {
output_dir_ = dir;
if (!output_dir_.empty()) {
std::error_code ec;
std::filesystem::create_directories(output_dir_, ec);
}
}
private: private:
static std::filesystem::path GetDefaultDesktopPath(); static std::filesystem::path GetDefaultDesktopPath();

View File

@@ -28,6 +28,7 @@ if is_mode("debug") then
add_defines("CROSSDESK_DEBUG") add_defines("CROSSDESK_DEBUG")
end end
add_requireconfs("*.python", {version = "3.12", override = true, configs = {pgo = false}})
add_requires("spdlog 1.14.1", {system = false}) add_requires("spdlog 1.14.1", {system = false})
add_requires("imgui v1.92.1-docking", {configs = {sdl3 = true, sdl3_renderer = true}}) add_requires("imgui v1.92.1-docking", {configs = {sdl3 = true, sdl3_renderer = true}})
add_requires("openssl3 3.3.2", {system = false}) add_requires("openssl3 3.3.2", {system = false})
@@ -37,9 +38,9 @@ add_requires("tinyfiledialogs 3.15.1")
if is_os("windows") then if is_os("windows") then
add_requires("libyuv", "miniaudio 0.11.21") add_requires("libyuv", "miniaudio 0.11.21")
add_links("Shell32", "windowsapp", "dwmapi", "User32", "kernel32", add_links("Shell32", "dwmapi", "User32", "kernel32",
"SDL3-static", "gdi32", "winmm", "setupapi", "version", "SDL3-static", "gdi32", "winmm", "setupapi", "version",
"Imm32", "iphlpapi") "Imm32", "iphlpapi", "d3d11", "dxgi")
add_cxflags("/WX") add_cxflags("/WX")
set_runtimes("MT") set_runtimes("MT")
elseif is_os("linux") then elseif is_os("linux") then
@@ -70,6 +71,9 @@ target("common")
set_kind("object") set_kind("object")
add_deps("rd_log") add_deps("rd_log")
add_files("src/common/*.cpp") add_files("src/common/*.cpp")
if is_os("macosx") then
add_files("src/common/*.mm")
end
add_includedirs("src/common", {public = true}) add_includedirs("src/common", {public = true})
target("path_manager") target("path_manager")
@@ -85,7 +89,9 @@ target("screen_capturer")
add_includedirs("src/screen_capturer", {public = true}) add_includedirs("src/screen_capturer", {public = true})
if is_os("windows") then if is_os("windows") then
add_packages("libyuv") add_packages("libyuv")
add_files("src/screen_capturer/windows/*.cpp") add_files("src/screen_capturer/windows/screen_capturer_dxgi.cpp",
"src/screen_capturer/windows/screen_capturer_gdi.cpp",
"src/screen_capturer/windows/screen_capturer_win.cpp")
add_includedirs("src/screen_capturer/windows", {public = true}) add_includedirs("src/screen_capturer/windows", {public = true})
elseif is_os("macosx") then elseif is_os("macosx") then
add_files("src/screen_capturer/macosx/*.cpp", add_files("src/screen_capturer/macosx/*.cpp",
@@ -196,8 +202,26 @@ target("gui")
add_files("src/gui/windows/*.mm") add_files("src/gui/windows/*.mm")
end end
if is_os("windows") then
target("wgc_plugin")
set_kind("shared")
add_packages("libyuv")
add_deps("rd_log")
add_defines("CROSSDESK_WGC_PLUGIN_BUILD=1")
add_links("windowsapp")
add_files("src/screen_capturer/windows/screen_capturer_wgc.cpp",
"src/screen_capturer/windows/wgc_session_impl.cpp",
"src/screen_capturer/windows/wgc_plugin_entry.cpp")
add_includedirs("src/common", "src/screen_capturer",
"src/screen_capturer/windows")
end
target("crossdesk") target("crossdesk")
set_kind("binary") set_kind("binary")
add_deps("rd_log", "common", "gui") add_deps("rd_log", "common", "gui")
add_files("src/app/*.cpp") add_files("src/app/*.cpp")
add_includedirs("src/app", {public = true}) add_includedirs("src/app", {public = true})
if is_os("windows") then
add_deps("wgc_plugin")
add_files("scripts/windows/crossdesk.rc")
end