Compare commits

...

26 Commits

Author SHA1 Message Date
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
54 changed files with 4179 additions and 3200 deletions

View File

@@ -15,82 +15,28 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
# Linux amd64
build-linux-amd64:
name: Build on Ubuntu 22.04 amd64
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
build-linux:
name: Build Linux (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-22.04
image: crossdesk/ubuntu20.04:latest
package_script: ./scripts/linux/pkg_amd64.sh
- arch: arm64
runner: ubuntu-22.04-arm
image: crossdesk/ubuntu20.04-arm64v8:latest
package_script: ./scripts/linux/pkg_arm64.sh
container:
image: ${{ matrix.image }}
options: --user root
steps:
- name: Extract version number
id: version
run: |
VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}"
@@ -98,15 +44,16 @@ jobs:
- 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
@@ -123,12 +70,6 @@ jobs:
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 ${{ matrix.package_script }}
@@ -192,12 +133,6 @@ jobs:
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
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
run: |
chmod +x ${{ matrix.package_script }}
@@ -301,29 +236,37 @@ jobs:
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
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
shell: pwsh
run: |
cd "${{ github.workspace }}\scripts\windows"
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
uses: actions/upload-artifact@v4
with:
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
- name: Upload portable artifact
uses: actions/upload-artifact@v4
with:
name: crossdesk-win-x64-portable-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip
release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
needs:
[build-linux-amd64, build-linux-arm64, build-macos, build-windows-x64]
[build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest
steps:
@@ -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-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-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
- name: List release files
run: ls -lh release/
@@ -416,6 +360,10 @@ jobs:
"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"
},
"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": {
"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"

View File

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

View File

@@ -214,7 +214,7 @@ sudo docker run -d \
**注意**
- **服务器需开放端口COTURN_PORT/udpCOTURN_PORT/tcpMIN_PORT-MAX_PORT/udpCROSSDESK_SERVER_PORT/tcp。**
- 如果不挂载 volume容器删除后数据会丢失
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
@@ -232,16 +232,30 @@ sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
### 客户端
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>
<img width="600" height="140" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
2. 点击`自托管服务器配置`按钮。<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>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><br><br>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
4. 后续如果自托管服务器被重置或因其他原因导致证书更换,可以点击`重置证书指纹`按钮重置客户端保存的证书指纹。<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><br><br>
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
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 客户端
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。

View File

@@ -222,7 +222,7 @@ sudo docker run -d \
**Notes**
- **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.
- 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`.
- 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
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>
<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>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><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.
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
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}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
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"
chmod +x "$BIN_DIR/$PKG_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
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
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/$APP_NAME || 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
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done
@@ -85,32 +81,9 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash
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
EOF
chmod +x "$DEBIAN_DIR/postinst"
dpkg-deb --build "$DEB_DIR"

View File

@@ -15,21 +15,18 @@ DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
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"
chmod +x "$BIN_DIR/$PKG_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
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
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/$APP_NAME || 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
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done
@@ -84,30 +80,6 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash
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
EOF

View File

@@ -11,9 +11,6 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -98,11 +95,6 @@ IDENTIFIER="cn.crossdesk.app"
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
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 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then
@@ -140,21 +132,45 @@ EOF
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 \
--package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
"${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}
echo "PKG package created successfully."

View File

@@ -11,9 +11,6 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -98,11 +95,6 @@ IDENTIFIER="cn.crossdesk.app"
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
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 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then
@@ -140,21 +132,45 @@ EOF
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 \
--package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
"${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}
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
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
; Certificate path
!define CERT_FILE "${__FILEDIR__}\..\..\certs\crossdesk.cn_root.crt"
; Compression settings
SetCompressor /FINAL lzma
@@ -49,7 +46,7 @@ ShowInstDetails show
Section "MainSection"
; Check if CrossDesk is running
StrCpy $1 "crossdesk.exe"
StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
@@ -75,10 +72,7 @@ installApp:
SetOverwrite ifnewer
; Main application executable path
File /oname=crossdesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
; Copy icon file to installation directory
File "${MUI_ICON}"
File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
; Write uninstall information
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}" "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}" "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}" "NoRepair" 1
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR"
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
; 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
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
SectionEnd
Section "Uninstall"
; Check if CrossDesk is running
StrCpy $1 "crossdesk.exe"
StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
@@ -137,7 +121,7 @@ cancelUninstall:
uninstallApp:
; Delete main executable and uninstaller
Delete "$INSTDIR\crossdesk.exe"
Delete "$INSTDIR\CrossDesk.exe"
Delete "$INSTDIR\uninstall.exe"
; Recursively delete installation directory
@@ -160,5 +144,5 @@ SectionEnd
; ------ Functions ------
Function LaunchApp
Exec "$INSTDIR\crossdesk.exe"
Exec "$INSTDIR\CrossDesk.exe"
FunctionEnd

View File

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

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

@@ -5,11 +5,8 @@
namespace crossdesk {
ConfigCenter::ConfigCenter(const std::string& config_path,
const std::string& cert_file_path)
: config_path_(config_path),
cert_file_path_(cert_file_path),
cert_file_path_default_(cert_file_path) {
ConfigCenter::ConfigCenter(const std::string& config_path)
: config_path_(config_path) {
ini_.SetUnicode(true);
Load();
}
@@ -70,71 +67,6 @@ int ConfigCenter::Load() {
} else {
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_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
@@ -142,6 +74,15 @@ int ConfigCenter::Load() {
enable_minimize_to_tray_ = ini_.GetBoolValue(
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;
}
@@ -165,19 +106,6 @@ int ConfigCenter::Save() {
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "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_);
@@ -185,6 +113,9 @@ int ConfigCenter::Save() {
ini_.SetBoolValue(section_, "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());
if (rc < 0) {
return -1;
@@ -270,15 +201,6 @@ int ConfigCenter::SetSrtp(bool enable_srtp) {
}
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;
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
@@ -310,67 +232,6 @@ int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
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) {
enable_self_hosted_ = enable_self_hosted;
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
@@ -397,45 +258,12 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
coturn_server_port_ = static_cast<int>(
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_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "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());
@@ -523,16 +351,6 @@ int ConfigCenter::GetSignalServerPort() const { return signal_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 {
return signal_server_host_default_;
}
@@ -545,10 +363,6 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
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::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
@@ -556,4 +370,19 @@ bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
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

@@ -21,9 +21,7 @@ class ConfigCenter {
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
public:
explicit ConfigCenter(
const std::string& config_path = "config.ini",
const std::string& cert_file_path = "crossdesk.cn_root.crt");
explicit ConfigCenter(const std::string& config_path = "config.ini");
~ConfigCenter();
// write config
@@ -37,15 +35,11 @@ class ConfigCenter {
int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_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 SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
int SetFileTransferSavePath(const std::string& path);
// read config
@@ -59,17 +53,14 @@ class ConfigCenter {
std::string GetSignalServerHost() const;
int GetSignalServerPort() const;
int GetCoturnServerPort() const;
std::string GetCertFilePath() const;
std::string GetCertFingerprint() const;
std::string GetDefaultCertFingerprint() const;
std::string GetDefaultServerHost() const;
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
std::string GetDefaultCertFilePath() const;
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
std::string GetFileTransferSavePath() const;
int Load();
int Save();
@@ -80,7 +71,7 @@ class ConfigCenter {
const char* section_ = "Settings";
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_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
@@ -92,16 +83,11 @@ class ConfigCenter {
int server_port_default_ = 9099;
int coturn_server_port_ = 0;
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_minimize_to_tray_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
std::string file_transfer_save_path_ = "";
};
} // namespace crossdesk
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -112,13 +112,8 @@ 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 = {
@@ -203,11 +198,25 @@ static std::vector<std::string> file_transfer = {
reinterpret_cast<const char*>(u8"文件传输:"), "File Transfer:"};
static std::vector<std::string> connection_status = {
reinterpret_cast<const char*>(u8"连接状态:"), "Connection Status:"};
#if _WIN32
static std::vector<std::string> file_transfer_save_path = {
reinterpret_cast<const char*>(u8"文件接收保存路径:"),
"File Transfer Save Path:"};
static std::vector<std::string> browse = {
reinterpret_cast<const char*>(u8"浏览"), "Browse"};
static std::vector<std::string> default_desktop = {
reinterpret_cast<const char*>(u8"桌面"), "Desktop"};
static std::vector<std::string> minimize_to_tray = {
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
"Minimize to system tray when exit:"};
static std::vector<std::string> resolution = {
reinterpret_cast<const char*>(u8"分辨率"), "Res"};
static std::vector<std::string> connection_mode = {
reinterpret_cast<const char*>(u8"连接模式"), "Mode"};
static std::vector<std::string> connection_mode_direct = {
reinterpret_cast<const char*>(u8"直连"), "Direct"};
static std::vector<std::string> connection_mode_relay = {
reinterpret_cast<const char*>(u8"中继"), "Relay"};
#if _WIN32
static std::vector<LPCWSTR> exit_program = {L"退出", L"Exit"};
#endif
#ifdef __APPLE__

View File

@@ -101,7 +101,7 @@ int Render::LocalWindow() {
ImGuiCol_WindowBg,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f - (float)time_duration));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
@@ -180,7 +180,7 @@ int Render::LocalWindow() {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::Begin("ResetPasswordWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |

View File

@@ -9,7 +9,7 @@ int Render::RecentConnectionsWindow() {
ImGuiIO& io = ImGui::GetIO();
float recent_connection_window_width = io.DisplaySize.x;
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),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
@@ -256,7 +256,7 @@ int Render::ConfirmDeleteConnection() {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::Begin("ConfirmDeleteConnectionWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |

View File

@@ -195,7 +195,7 @@ 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_height_ = title_bar_height_ * 1.3f;
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;
if (!props->peer_) {
LOG_INFO("Create peer [{}] instance failed", props->local_id_);

View File

@@ -298,7 +298,7 @@ SDL_HitTestResult Render::HitTestCallback(SDL_Window* window,
float mouse_grab_padding = render->title_bar_button_width_ * 0.16f;
if (area->y < render->title_bar_button_width_ &&
area->y > mouse_grab_padding &&
area->x < window_width - render->title_bar_button_width_ * 4.0f &&
area->x < window_width - render->title_bar_button_width_ * 3.0f &&
area->x > mouse_grab_padding) {
return SDL_HITTEST_DRAGGABLE;
}
@@ -492,6 +492,16 @@ int Render::LoadSettingsFromCacheFile() {
enable_daemon_ = config_center_->IsEnableDaemon();
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray();
// File transfer save path
{
std::string saved_path = config_center_->GetFileTransferSavePath();
strncpy(file_transfer_save_path_buf_, saved_path.c_str(),
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) - 1] =
'\0';
file_transfer_save_path_last_ = saved_path;
}
language_button_value_last_ = language_button_value_;
video_quality_button_value_last_ = video_quality_button_value_;
video_encode_format_button_value_last_ = video_encode_format_button_value_;
@@ -672,13 +682,11 @@ int Render::CreateConnectionPeer() {
std::string signal_server_ip;
int signal_server_port;
int coturn_server_port;
std::string tls_cert_fingerprint;
if (config_center_->IsSelfHosted()) {
signal_server_ip = config_center_->GetSignalServerHost();
signal_server_port = config_center_->GetSignalServerPort();
coturn_server_port = config_center_->GetCoturnServerPort();
tls_cert_fingerprint = config_center_->GetCertFingerprint();
std::string current_self_hosted_ip = config_center_->GetSignalServerHost();
bool use_cached_id = false;
@@ -739,7 +747,6 @@ int Render::CreateConnectionPeer() {
signal_server_ip = config_center_->GetDefaultServerHost();
signal_server_port = config_center_->GetDefaultSignalServerPort();
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
tls_cert_fingerprint = config_center_->GetDefaultCertFingerprint();
params_.user_id = client_id_with_password_;
}
@@ -763,7 +770,6 @@ int Render::CreateConnectionPeer() {
} else {
coturn_server_port_self_[0] = '\0';
}
tls_cert_path_self_ = config_center_->GetCertFilePath();
// peer config
strncpy((char*)params_.signal_server_ip, signal_server_ip.c_str(),
@@ -784,30 +790,6 @@ int Render::CreateConnectionPeer() {
strncpy((char*)params_.turn_server_password, "crossdeskpw",
sizeof(params_.turn_server_password) - 1);
params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0';
strncpy(params_.tls_cert_fingerprint, tls_cert_fingerprint.c_str(),
sizeof(params_.tls_cert_fingerprint) - 1);
params_.tls_cert_fingerprint[sizeof(params_.tls_cert_fingerprint) - 1] = '\0';
if (config_center_->IsSelfHosted()) {
params_.on_cert_fingerprint = [](const char* fingerprint, void* user_data) {
Render* render = static_cast<Render*>(user_data);
if (render && render->config_center_) {
render->config_center_->SetCertFingerprint(fingerprint);
LOG_INFO("Saved self-hosted certificate fingerprint: {}", fingerprint);
}
};
params_.fingerprint_user_data = this;
} else {
params_.on_cert_fingerprint = [](const char* fingerprint, void* user_data) {
Render* render = static_cast<Render*>(user_data);
if (render && render->config_center_) {
render->config_center_->SetDefaultCertFingerprint(fingerprint);
LOG_INFO("Saved default server certificate fingerprint: {}",
fingerprint);
}
};
params_.fingerprint_user_data = this;
}
strncpy(params_.log_path, dll_log_path_.c_str(),
sizeof(params_.log_path) - 1);
@@ -911,7 +893,7 @@ void Render::UpdateInteractions() {
mouse_controller_is_started_ = false;
}
if (start_keyboard_capturer_ && foucs_on_stream_window_) {
if (start_keyboard_capturer_ && focus_on_stream_window_) {
if (!keyboard_capturer_is_started_) {
StartKeyboardCapturer();
keyboard_capturer_is_started_ = true;
@@ -932,9 +914,10 @@ int Render::CreateMainWindow() {
ImGui::SetCurrentContext(main_ctx_);
if (!SDL_CreateWindowAndRenderer(
"Remote Desk", (int)main_window_width_, (int)main_window_height_,
"CrossDesk Main Window", (int)main_window_width_,
(int)main_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_HIDDEN,
SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT,
&main_window_, &main_renderer_)) {
LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError());
return -1;
@@ -964,6 +947,8 @@ int Render::CreateMainWindow() {
// for window region action
SDL_SetWindowHitTest(main_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&main_windows_system_chinese_font_);
ImGuiStyle& style = ImGui::GetStyle();
@@ -1020,9 +1005,10 @@ int Render::CreateStreamWindow() {
ImGui::SetCurrentContext(stream_ctx_);
if (!SDL_CreateWindowAndRenderer(
"Stream window", (int)stream_window_width_,
"CrossDesk Stream Window", (int)stream_window_width_,
(int)stream_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&stream_window_, &stream_renderer_)) {
LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}",
SDL_GetError());
@@ -1036,6 +1022,8 @@ int Render::CreateStreamWindow() {
// for window region action
SDL_SetWindowHitTest(stream_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&stream_windows_system_chinese_font_);
ImGuiStyle& style = ImGui::GetStyle();
@@ -1091,12 +1079,12 @@ int Render::CreateServerWindow() {
return -1;
}
ImGui::SetCurrentContext(server_ctx_);
if (!SDL_CreateWindowAndRenderer("Server window", (int)server_window_width_,
(int)server_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY |
SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&server_window_, &server_renderer_)) {
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Server Window", (int)server_window_width_,
(int)server_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&server_window_, &server_renderer_)) {
LOG_ERROR("Error creating server_window_ and server_renderer_: {}",
SDL_GetError());
return -1;
@@ -1308,6 +1296,8 @@ int Render::DrawMainWindow() {
ImGuiIO& io = ImGui::GetIO();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y),
ImGuiCond_Always);
@@ -1316,6 +1306,7 @@ int Render::DrawMainWindow() {
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
ImGui::PopStyleColor();
ImGui::PopStyleVar();
TitleBar(true);
@@ -1336,6 +1327,7 @@ int Render::DrawMainWindow() {
ImGui::Render();
SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0);
SDL_RenderClear(main_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_);
SDL_RenderPresent(main_renderer_);
@@ -1385,6 +1377,7 @@ int Render::DrawStreamWindow() {
ImGui::Render();
SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255);
SDL_RenderClear(stream_renderer_);
// std::shared_lock lock(client_properties_mutex_);
@@ -1411,24 +1404,20 @@ int Render::DrawServerWindow() {
LOG_ERROR("Server context is null");
return -1;
}
if (server_window_) {
int w = 0;
int h = 0;
SDL_GetWindowSize(server_window_, &w, &h);
if (w > 0 && h > 0) {
server_window_width_ = (float)w;
server_window_height_ = (float)h;
}
}
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGuiIO& io = ImGui::GetIO();
server_window_width_ = io.DisplaySize.x;
server_window_height_ = io.DisplaySize.y;
ServerWindow();
ImGui::Render();
SDL_SetRenderDrawColor(server_renderer_, 255, 255, 255, 255);
SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0);
SDL_RenderClear(server_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_);
SDL_RenderPresent(server_renderer_);
@@ -1458,13 +1447,11 @@ int Render::Run() {
path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (path_manager_) {
cert_path_ =
(path_manager_->GetCertPath() / "crossdesk.cn_root.crt").string();
exec_log_path_ = path_manager_->GetLogPath().string();
dll_log_path_ = path_manager_->GetLogPath().string();
cache_path_ = path_manager_->GetCachePath().string();
config_center_ =
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini", cert_path_);
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
strncpy(signal_server_ip_self_,
config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
@@ -1478,8 +1465,6 @@ int Render::Run() {
} else {
signal_server_port_self_[0] = '\0';
}
strncpy(cert_file_path_, cert_path_.c_str(), sizeof(cert_file_path_) - 1);
cert_file_path_[sizeof(cert_file_path_) - 1] = '\0';
} else {
std::cerr << "Failed to create PathManager" << std::endl;
return -1;
@@ -2073,7 +2058,8 @@ void Render::UpdateRenderRect() {
(int)(render_area_width * video_ratio_reverse)};
} else if (render_area_width > render_area_height * video_ratio) {
props->stream_render_rect_ = {
(int)abs(render_area_width - render_area_height * video_ratio) / 2,
(int)abs(render_area_width - render_area_height * video_ratio) / 2 +
(int)props->render_window_x_,
(int)props->render_window_y_, (int)(render_area_height * video_ratio),
(int)render_area_height};
} else {
@@ -2198,7 +2184,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
case SDL_EVENT_WINDOW_FOCUS_GAINED:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
foucs_on_stream_window_ = true;
focus_on_stream_window_ = true;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = true;
@@ -2208,7 +2194,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
case SDL_EVENT_WINDOW_FOCUS_LOST:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
foucs_on_stream_window_ = false;
focus_on_stream_window_ = false;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = false;
@@ -2222,7 +2208,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL:
if (foucs_on_stream_window_) {
if (focus_on_stream_window_) {
ProcessMouseEvent(event);
}
break;

View File

@@ -226,7 +226,6 @@ class Render {
int RecentConnectionsWindow();
int SettingWindow();
int SelfHostedServerWindow();
int ShowSimpleFileBrowser();
int ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props);
int ControlBar(std::shared_ptr<SubStreamWindowProperties>& props);
int AboutWindow();
@@ -381,7 +380,6 @@ class Render {
ConfigCenter::LANGUAGE localization_language_ =
ConfigCenter::LANGUAGE::CHINESE;
std::unique_ptr<PathManager> path_manager_;
std::string cert_path_;
std::string exec_log_path_;
std::string dll_log_path_;
std::string cache_path_;
@@ -436,7 +434,7 @@ class Render {
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = 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;
@@ -624,16 +622,15 @@ class Render {
char self_hosted_id_[17] = "";
char self_hosted_user_id_[17] = "";
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_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = false;
bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478";
char cert_file_path_[256] = "";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
@@ -649,10 +646,11 @@ class Render {
bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = 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_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
std::string tls_cert_path_self_ = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_ = "";

View File

@@ -104,11 +104,10 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
}
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_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
@@ -154,8 +153,11 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
(float)(last_mouse_event.button.y - props->stream_render_rect_.y) /
render_height;
std::string msg = remote_action.to_json();
if (props->control_bar_hovered_) {
continue;
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
@@ -321,6 +323,14 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
std::string remote_user_id = std::string(user_id, user_id_size);
static FileReceiver receiver;
// 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;
@@ -440,9 +450,9 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
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)()));
const double capped =
(std::min)(bps, static_cast<double>(
(std::numeric_limits<uint32_t>::max)()));
estimated_rate_bps = static_cast<uint32_t>(capped);
}
}
@@ -558,13 +568,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
remote_action.i.host_name, remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]",
render->connection_host_names_[remote_id]);
for (int i = 0; i < remote_action.i.display_num; i++) {
render->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
FreeRemoteAction(remote_action);
}
} else {
@@ -613,10 +616,6 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
render->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
render->signal_connected_ = false;
} else if (SignalStatus::SignalFingerprintMismatch == status) {
render->signal_connected_ = false;
LOG_ERROR("[{}] signal server fingerprint mismatch", client_id);
render->config_center_->ClearDefaultCertFingerprint();
}
} else {
if (client_id.rfind("C-", 0) != 0) {
@@ -644,9 +643,6 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
props->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalFingerprintMismatch == status) {
props->signal_connected_ = false;
LOG_ERROR("[{}] signal server fingerprint mismatch", remote_id);
}
}
}

View File

@@ -141,7 +141,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
if (props->control_bar_expand_) {
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);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
@@ -359,13 +359,11 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SameLine();
}
float expand_button_pos_x =
props->control_bar_expand_ ? (props->is_control_bar_in_left_
? props->control_window_width_ * 1.917f
: props->control_window_width_ * 0.03f)
: (props->is_control_bar_in_left_
? props->control_window_width_ * 1.02f
: props->control_window_width_ * 0.23f);
float expand_button_pos_x = props->control_bar_expand_
? (props->is_control_bar_in_left_
? props->control_window_width_ * 0.917f
: props->control_window_width_ * 0.03f)
: props->control_window_width_ * 0.11f;
ImGui::SetCursorPosX(expand_button_pos_x);
@@ -397,9 +395,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
}
int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SetCursorPos(ImVec2(props->is_control_bar_in_left_
? props->control_window_width_ * 1.02f
: props->control_window_width_ * 0.02f,
ImGui::SetCursorPos(ImVec2(props->control_window_width_ * 0.048f,
props->control_window_min_height_));
ImGui::SetWindowFontScale(0.5f);
if (ImGui::BeginTable("NetTrafficStats", 4, ImGuiTableFlags_BordersH,
@@ -460,12 +456,33 @@ int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
LossRateDisplay(props->net_traffic_stats_.total_inbound_stats.loss_rate);
ImGui::TableNextColumn();
ImGui::Text("FPS");
ImGui::Text("FPS:");
ImGui::TableNextColumn();
ImGui::Text("%d", props->fps_);
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::SetWindowFontScale(1.0f);

View File

@@ -9,15 +9,17 @@ int Render::StatusBar() {
float status_bar_width = io.DisplaySize.x;
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;
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT)),
ImGuiCond_Always);
ImGui::BeginChild(
"StatusBar", ImVec2(status_bar_width, status_bar_height),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBringToFrontOnFocus);
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("StatusBar", ImVec2(status_bar_width, status_bar_height),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor(2);
ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f,
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f));
@@ -40,7 +42,6 @@ int Render::StatusBar() {
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor();
ImGui::EndChild();
return 0;
}

View File

@@ -2,6 +2,7 @@
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "rounded_corner_button.h"
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
@@ -23,7 +24,7 @@ int Render::TitleBar(bool main_window) {
if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
title_bar_width = io.DisplaySize.x;
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f);
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;
@@ -45,7 +46,7 @@ int Render::TitleBar(bool main_window) {
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::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(main_window ? "MainTitleBar" : "StreamTitleBar",
@@ -285,8 +286,20 @@ int Render::TitleBar(bool main_window) {
float xmark_pos_y = title_bar_button_height * 0.5f;
float xmark_size = title_bar_button_width * 0.33f;
std::string close_button = "##xmark"; // ICON_FA_XMARK;
if (ImGui::Button(close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
bool close_button_clicked = false;
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 (enable_minimize_to_tray_) {
tray_->MinimizeToTray();
@@ -299,6 +312,7 @@ int Render::TitleBar(bool main_window) {
}
#endif
}
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,

View File

@@ -1,6 +1,10 @@
#include <cstdlib>
#include <string>
#if defined(_WIN32)
#include <windows.h>
#endif
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
@@ -24,13 +28,34 @@ void Render::Hyperlink(const std::string& label, const std::string& url,
ImGui::EndTooltip();
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
#if defined(_WIN32)
std::string cmd = "start " + url;
int wide_len =
MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
if (wide_len > 0) {
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)) {
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
}
}
#elif defined(__APPLE__)
std::string cmd = "open " + url;
#else
std::string cmd = "xdg-open " + url;
#endif
#if !defined(_WIN32)
system(cmd.c_str()); // open browser
#endif
}
}
}
@@ -52,7 +77,7 @@ int Render::AboutWindow() {
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::Begin(

View File

@@ -18,7 +18,7 @@ bool Render::ConnectionStatusWindow(
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |

View File

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

View File

@@ -97,7 +97,7 @@ int Render::FileTransferWindow(
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
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_TitleBgActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));

View File

@@ -2,6 +2,7 @@
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "tinyfiledialogs.h"
namespace crossdesk {
@@ -15,28 +16,28 @@ int Render::SettingWindow() {
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
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(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f));
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f));
#else
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(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.8f));
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f));
#endif
} else {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
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(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f));
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f));
#else
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(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.8f));
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f));
#endif
}
@@ -49,7 +50,7 @@ int Render::SettingWindow() {
int settings_items_offset = 0;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::Begin(localization::settings[localization_language_index_].c_str(),
@@ -330,10 +331,14 @@ int Render::SettingWindow() {
ImGui::EndTooltip();
}
}
#if _WIN32
ImGui::Separator();
{
#ifndef _WIN32
ImGui::BeginDisabled();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
#endif
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
@@ -349,8 +354,67 @@ int Render::SettingWindow() {
ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_);
}
#ifndef _WIN32
ImGui::PopStyleColor();
ImGui::EndDisabled();
#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_) {
ImGui::EndDisabled();
}
@@ -469,6 +533,10 @@ int Render::SettingWindow() {
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
#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;
// Recreate peer instance
@@ -516,6 +584,13 @@ int Render::SettingWindow() {
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;
}
ImGui::SetWindowFontScale(0.5f);

View File

@@ -14,7 +14,8 @@ int Render::MainWindow() {
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * (TITLE_BAR_HEIGHT)),
ImGuiCond_Always);
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(
"DeskWindow",
ImVec2(local_remote_window_width, local_remote_window_height),
@@ -22,7 +23,7 @@ int Render::MainWindow() {
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
ImGui::PopStyleColor(2);
LocalWindow();

View File

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

View File

@@ -28,98 +28,6 @@ std::vector<std::string> GetRootEntries() {
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() {
ImGuiIO& io = ImGui::GetIO();
if (show_self_hosted_server_config_window_) {
@@ -128,12 +36,12 @@ int Render::SelfHostedServerWindow() {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.298f, io.DisplaySize.y * 0.25f));
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 {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.27f, io.DisplaySize.y * 0.3f));
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;
@@ -143,7 +51,7 @@ int Render::SelfHostedServerWindow() {
{
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::Begin(localization::self_hosted_server_settings
@@ -212,35 +120,6 @@ int Render::SelfHostedServerWindow() {
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_) {
ImGui::EndDisabled();
}
@@ -263,7 +142,6 @@ int Render::SelfHostedServerWindow() {
config_center_->SetServerHost(signal_server_ip_self_);
config_center_->SetServerPort(atoi(signal_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_,
sizeof(signal_server_ip_) - 1);
signal_server_ip_[sizeof(signal_server_ip_) - 1] = '\0';
@@ -273,9 +151,6 @@ int Render::SelfHostedServerWindow() {
strncpy(coturn_server_port_, coturn_server_port_self_,
sizeof(coturn_server_port_) - 1);
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;
}
@@ -306,7 +181,6 @@ int Render::SelfHostedServerWindow() {
} else {
coturn_server_port_self_[0] = '\0';
}
tls_cert_path_self_ = config_center_->GetCertFilePath();
}
ImGui::SetWindowFontScale(1.0f);

View File

@@ -8,6 +8,7 @@
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "rounded_corner_button.h"
namespace crossdesk {
@@ -48,17 +49,19 @@ int Render::ServerWindow() {
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::Begin("##server_window", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
server_window_title_bar_height_ = title_bar_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_WindowRounding, 6.0f);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(
"ServerTitleBar",
@@ -83,9 +86,13 @@ int Render::ServerWindow() {
const char* icon =
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
std::string toggle_label = std::string(icon) + "##server_toggle";
if (ImGui::Button(toggle_label.c_str(),
ImVec2(server_title_bar_button_width,
server_title_bar_button_height))) {
bool toggle_clicked = RoundedCornerButton(
toggle_label.c_str(),
ImVec2(server_title_bar_button_width, server_title_bar_button_height),
8.5f, ImDrawFlags_RoundCornersTopLeft, true, IM_COL32(0, 0, 0, 0),
IM_COL32(0, 0, 0, 25), IM_COL32(255, 255, 255, 255));
if (toggle_clicked) {
if (server_window_) {
int w = 0;
int h = 0;
@@ -114,7 +121,7 @@ int Render::ServerWindow() {
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
RemoteClientInfoWindow();

View File

@@ -117,7 +117,9 @@ int Render::StreamWindow() {
ImGui::SetWindowFontScale(0.6f);
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);
ImGui::SetNextWindowPos(
ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_),
@@ -138,7 +140,7 @@ int Render::StreamWindow() {
UpdateRenderRect();
ControlWindow(props);
// Show file transfer window if needed
FileTransferWindow(props);
@@ -151,12 +153,12 @@ int Render::StreamWindow() {
// std::unique_lock unique_lock(client_properties_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) {
erase_it = client_properties_.erase(erase_it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
// Ensure we flush pending STREAM_REFRESH_EVENT events and
// clean up peer resources before erasing the entry, otherwise
// SDL events may still hold raw pointers to freed
// SubStreamWindowProperties (including video_frame_mutex_),
// leading to std::system_error when locking.
CloseTab(erase_it);
}
}
// lock.lock();
@@ -217,7 +219,9 @@ int Render::StreamWindow() {
if (props->tab_selected_) {
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);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
@@ -236,10 +240,10 @@ int Render::StreamWindow() {
UpdateRenderRect();
ControlWindow(props);
// Show file transfer window if needed
FileTransferWindow(props);
ImGui::End();
if (!props->peer_) {
@@ -251,12 +255,7 @@ int Render::StreamWindow() {
// std::unique_lock unique_lock(client_properties_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) {
client_properties_.erase(erase_it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
CloseTab(erase_it);
}
}
// lock.lock();

View File

@@ -77,7 +77,7 @@ int Render::UpdateNotificationWindow() {
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::Begin(
localization::notification[localization_language_index_].c_str(),

View File

@@ -40,20 +40,6 @@ std::filesystem::path PathManager::GetLogPath() {
#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) {
std::error_code ec;
bool created = std::filesystem::create_directories(p, ec);

View File

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

View File

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

View File

@@ -0,0 +1,333 @@
#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;
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;
}
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,79 @@
/*
* @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;
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};
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,206 @@
#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;
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;
}
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,66 @@
/*
* @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;
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};
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;
}
@@ -165,6 +163,8 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
return 4;
}
bool any_started = false;
int last_error = 0;
for (int i = 0; i < sessions_.size(); i++) {
if (sessions_[i].inited_ == false) {
LOG_ERROR("Session {} not inited", i);
@@ -174,17 +174,27 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
if (sessions_[i].running_) {
LOG_ERROR("Session {} is already running", i);
} 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) {
sessions_[i].session_->Pause();
sessions_[i].paused_ = 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;
}
@@ -327,4 +337,4 @@ void ScreenCapturerWgc::CleanUp() {
sessions_.clear();
}
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -57,8 +57,8 @@ class ScreenCapturerWgc : public ScreenCapturer,
std::vector<WgcSessionInfo> sessions_;
std::atomic_bool running_;
std::atomic_bool inited_;
std::atomic_bool running_{false};
std::atomic_bool inited_{false};
int fps_ = 60;
@@ -72,4 +72,4 @@ class ScreenCapturerWgc : public ScreenCapturer,
std::mutex frame_mutex_;
};
} // namespace crossdesk
#endif
#endif

View File

@@ -0,0 +1,198 @@
#include "screen_capturer_win.h"
#include <cmath>
#include <memory>
#include <string>
#include <vector>
#include "rd_log.h"
#include "screen_capturer_dxgi.h"
#include "screen_capturer_gdi.h"
#include "screen_capturer_wgc.h"
namespace crossdesk {
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_ = std::make_unique<ScreenCapturerWgc>();
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
LOG_INFO("Windows capturer: using WGC");
BuildCanonicalFromImpl();
return 0;
}
LOG_WARN("Windows capturer: WGC init failed (ret={}), try DXGI", ret);
impl_.reset();
impl_ = std::make_unique<ScreenCapturerDxgi>();
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>();
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();
}
{
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);
RebuildAliasesFromImpl();
return true;
}
return false;
};
if (dynamic_cast<ScreenCapturerWgc*>(impl_.get())) {
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);
}
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,54 @@
/*
* @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;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
std::unique_ptr<ScreenCapturer> impl_;
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

@@ -7,6 +7,8 @@
#include <iostream>
#include <memory>
#include "rd_log.h"
#define CHECK_INIT \
if (!is_initialized_) { \
std::cout << "AE_NEED_INIT" << std::endl; \
@@ -64,6 +66,7 @@ int WgcSessionImpl::Start(bool show_cursor) {
CHECK_INIT;
try {
last_show_cursor_ = show_cursor;
if (!capture_session_) {
auto current_size = capture_item_.Size();
capture_framepool_ =
@@ -89,13 +92,12 @@ int WgcSessionImpl::Start(bool show_cursor) {
// we need to test the performance later
// loop_ = std::thread(std::bind(&WgcSessionImpl::message_func, this));
capture_session_.StartCapture();
capture_session_.IsCursorCaptureEnabled(show_cursor);
capture_session_.StartCapture();
error = 0;
} catch (winrt::hresult_error) {
std::cout << "AE_WGC_CREATE_CAPTURER_FAILED" << std::endl;
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86;
} catch (...) {
return 86;
@@ -246,8 +248,15 @@ void WgcSessionImpl::OnFrame(
auto frame_captured =
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
if (!d3d11_texture_mapped_ || is_new_size)
CreateMappedTexture(frame_captured);
if (!d3d11_texture_mapped_ || is_new_size) {
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(),
frame_captured.get());
@@ -262,6 +271,7 @@ void WgcSessionImpl::OnFrame(
if (FAILED(hr)) {
OutputDebugStringW(
(L"map resource failed: " + std::to_wstring(hr)).c_str());
return;
}
// copy data from map_result.pData
@@ -290,7 +300,24 @@ void WgcSessionImpl::OnFrame(
void WgcSessionImpl::OnClosed(
winrt::Windows::Graphics::Capture::GraphicsCaptureItem 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() {
@@ -313,7 +340,7 @@ int WgcSessionImpl::Initialize() {
d3d11_device->GetImmediateContext(d3d11_device_context_.put());
} catch (winrt::hresult_error) {
std::cout << "AE_WGC_CREATE_CAPTURER_FAILED" << std::endl;
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
return 86;
} catch (...) {
return 86;
@@ -378,4 +405,4 @@ LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
// ::CloseWindow(hwnd_);
// ::DestroyWindow(hwnd_);
// }
} // namespace crossdesk
} // namespace crossdesk

View File

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

View File

@@ -97,6 +97,14 @@ class FileReceiver {
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:
static std::filesystem::path GetDefaultDesktopPath();

View File

@@ -39,7 +39,7 @@ if is_os("windows") then
add_requires("libyuv", "miniaudio 0.11.21")
add_links("Shell32", "windowsapp", "dwmapi", "User32", "kernel32",
"SDL3-static", "gdi32", "winmm", "setupapi", "version",
"Imm32", "iphlpapi")
"Imm32", "iphlpapi", "d3d11", "dxgi")
add_cxflags("/WX")
set_runtimes("MT")
elseif is_os("linux") then
@@ -203,4 +203,7 @@ target("crossdesk")
set_kind("binary")
add_deps("rd_log", "common", "gui")
add_files("src/app/*.cpp")
add_includedirs("src/app", {public = true})
add_includedirs("src/app", {public = true})
if is_os("windows") then
add_files("scripts/windows/crossdesk.rc")
end