mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-03-22 15:59:07 +08:00
Compare commits
89 Commits
v1.2.0-202
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7f62c5fe0 | ||
|
|
2bbddbca6b | ||
|
|
f0f8f27f4c | ||
|
|
262af263f2 | ||
|
|
38b7775b1b | ||
|
|
56c0bca62f | ||
|
|
4b1b09fd5b | ||
|
|
1d6425bbf4 | ||
|
|
5ec6552d25 | ||
|
|
79e4a0790a | ||
|
|
1d3cac54ab | ||
|
|
2f26334775 | ||
|
|
9270d528e3 | ||
|
|
91db3a7e34 | ||
|
|
d017561e54 | ||
|
|
8e8a85bae3 | ||
|
|
bea89e9111 | ||
|
|
499ce0190a | ||
|
|
91bde91238 | ||
|
|
3e31ba102d | ||
|
|
263c5eefd3 | ||
|
|
b230b851e4 | ||
|
|
ff32477ffe | ||
|
|
c6c60decdb | ||
|
|
7505adeca8 | ||
|
|
754f1fba88 | ||
|
|
8be46b870a | ||
|
|
81cb1d6c0b | ||
|
|
319416f1b7 | ||
|
|
d679c6251b | ||
|
|
a14baafda7 | ||
|
|
cfdc7d3106 | ||
|
|
33d51b8ce5 | ||
|
|
b13dac2093 | ||
|
|
a605c95e5a | ||
|
|
9a5553a636 | ||
|
|
ef02403da6 | ||
|
|
adfab363c1 | ||
|
|
123d4cf595 | ||
|
|
19feb8ff49 | ||
|
|
9223bf9d2d | ||
|
|
11b5f87841 | ||
|
|
cea59fb453 | ||
|
|
3a179bf480 | ||
|
|
b9c53024f1 | ||
|
|
62b37ad698 | ||
|
|
de56cd5d3b | ||
|
|
8d9d78185a | ||
|
|
b10a6512fe | ||
|
|
a94da8802f | ||
|
|
4e6f82d00c | ||
|
|
5e2ad99ec0 | ||
|
|
8ab50ea362 | ||
|
|
25e9958a69 | ||
|
|
410ea8b96b | ||
|
|
e656664cad | ||
|
|
0e6cee0961 | ||
|
|
42506b8c1d | ||
|
|
e35365d162 | ||
|
|
bf1c0f796d | ||
|
|
547532b28c | ||
|
|
a91e23abf6 | ||
|
|
2b324f636b | ||
|
|
103b8372e4 | ||
|
|
f7f1724bf1 | ||
|
|
5d70e11f17 | ||
|
|
fb7ae90d46 | ||
|
|
957792a7a0 | ||
|
|
2e8ce6a2f0 | ||
|
|
9927a56b78 | ||
|
|
db3da52f83 | ||
|
|
19a7c6978a | ||
|
|
b5e9ba03a1 | ||
|
|
cb5f8b91ad | ||
|
|
f627f60f1a | ||
|
|
e9fce5b8b8 | ||
|
|
a7820a79db | ||
|
|
b6a52dbcd4 | ||
|
|
7bbd10a50c | ||
|
|
ee08b231db | ||
|
|
619e54dc0e | ||
|
|
9b69596af1 | ||
|
|
f6e169c013 | ||
|
|
fd242d50c1 | ||
|
|
d6d8ecd6c5 | ||
|
|
669fac7f50 | ||
|
|
92d670916e | ||
|
|
0155413c12 | ||
|
|
8468be6532 |
138
.github/workflows/build.yml
vendored
138
.github/workflows/build.yml
vendored
@@ -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,20 +44,21 @@ 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
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -123,19 +70,13 @@ 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 }}
|
||||
${{ matrix.package_script }} ${LEGAL_VERSION}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}
|
||||
path: ${{ github.workspace }}/crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}.deb
|
||||
@@ -171,7 +112,7 @@ jobs:
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.xmake/packages
|
||||
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.sha }}
|
||||
@@ -182,7 +123,7 @@ jobs:
|
||||
run: brew install xmake
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize submodules
|
||||
run: git submodule update --init --recursive
|
||||
@@ -192,19 +133,13 @@ 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 }}
|
||||
${{ matrix.package_script }} ${VERSION_NUM}
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}
|
||||
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
|
||||
@@ -234,7 +169,7 @@ jobs:
|
||||
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: D:\xmake_global\.xmake\packages
|
||||
key: ${{ runner.os }}-xmake-deps-intel-${{ github.sha }}
|
||||
@@ -286,7 +221,7 @@ jobs:
|
||||
Copy-Item $source $target -Force
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize submodules
|
||||
run: git submodule update --init --recursive
|
||||
@@ -301,37 +236,45 @@ 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
|
||||
uses: actions/upload-artifact@v6
|
||||
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@v6
|
||||
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:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
@@ -359,6 +302,7 @@ jobs:
|
||||
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||
cp artifacts/crossdesk-linux-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"
|
||||
|
||||
24
.github/workflows/update-version-json.yml
vendored
24
.github/workflows/update-version-json.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
# Xmake cache
|
||||
.xmake/
|
||||
build/
|
||||
certs/
|
||||
|
||||
# MacOS Cache
|
||||
.DS_Store
|
||||
|
||||
34
README.md
34
README.md
@@ -181,7 +181,7 @@ sudo docker run -d \
|
||||
-e MAX_PORT=xxxxx \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.3
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
上述命令中,用户需注意的参数如下:
|
||||
@@ -208,13 +208,13 @@ sudo docker run -d \
|
||||
-e MAX_PORT=60000 \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.3
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- **服务器需开放端口:COTURN_PORT/udp,COTURN_PORT/tcp,MIN_PORT-MAX_PORT/udp,CROSSDESK_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)。
|
||||
|
||||
33
README_EN.md
33
README_EN.md
@@ -189,7 +189,7 @@ sudo docker run -d \
|
||||
-e MAX_PORT=xxxxx \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.3
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
The parameters you need to pay attention to are as follows:
|
||||
@@ -216,13 +216,13 @@ sudo docker run -d \
|
||||
-e MAX_PORT=60000 \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.3
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
**Notes**
|
||||
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORT–MAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
|
||||
- If you don’t 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)。
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
2
scripts/windows/crossdesk.rc
Normal file
2
scripts/windows/crossdesk.rc
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -34,9 +34,16 @@
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
Daemon* Daemon::instance_ = nullptr;
|
||||
volatile std::sig_atomic_t Daemon::stop_requested_ = 0;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr int kRestartDelayMs = 1000;
|
||||
#ifndef _WIN32
|
||||
constexpr int kWaitPollIntervalMs = 200;
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
// get executable file path
|
||||
static std::string GetExecutablePath() {
|
||||
#ifdef _WIN32
|
||||
@@ -66,33 +73,35 @@ static std::string GetExecutablePath() {
|
||||
return "";
|
||||
}
|
||||
|
||||
Daemon::Daemon(const std::string& name)
|
||||
: name_(name)
|
||||
#ifdef _WIN32
|
||||
,
|
||||
running_(false)
|
||||
#else
|
||||
,
|
||||
running_(true)
|
||||
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
|
||||
|
||||
void Daemon::stop() {
|
||||
running_.store(false);
|
||||
#ifndef _WIN32
|
||||
stop_requested_ = 1;
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
void Daemon::stop() { running_ = false; }
|
||||
|
||||
bool Daemon::isRunning() const { return running_; }
|
||||
bool Daemon::isRunning() const {
|
||||
#ifndef _WIN32
|
||||
return running_.load() && (stop_requested_ == 0);
|
||||
#else
|
||||
return running_.load();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Daemon::start(MainLoopFunc loop) {
|
||||
#ifdef _WIN32
|
||||
running_ = true;
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#elif __APPLE__
|
||||
// macOS: Use child process monitoring (like Windows) to preserve GUI
|
||||
running_ = true;
|
||||
stop_requested_ = 0;
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#else
|
||||
// linux: Daemonize first, then run with restart monitoring
|
||||
instance_ = this;
|
||||
stop_requested_ = 0;
|
||||
|
||||
// check if running from terminal before fork
|
||||
bool from_terminal =
|
||||
@@ -134,29 +143,13 @@ bool Daemon::start(MainLoopFunc loop) {
|
||||
}
|
||||
|
||||
// set up signal handlers
|
||||
signal(SIGTERM, [](int) {
|
||||
if (instance_) instance_->stop();
|
||||
});
|
||||
signal(SIGINT, [](int) {
|
||||
if (instance_) instance_->stop();
|
||||
});
|
||||
signal(SIGTERM, [](int) { stop_requested_ = 1; });
|
||||
signal(SIGINT, [](int) { stop_requested_ = 1; });
|
||||
|
||||
// ignore SIGPIPE
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
// set up SIGCHLD handler to reap zombie processes
|
||||
struct sigaction sa_chld;
|
||||
sa_chld.sa_handler = [](int) {
|
||||
// reap zombie processes
|
||||
while (waitpid(-1, nullptr, WNOHANG) > 0) {
|
||||
// continue until no more zombie children
|
||||
}
|
||||
};
|
||||
sigemptyset(&sa_chld.sa_mask);
|
||||
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
|
||||
sigaction(SIGCHLD, &sa_chld, nullptr);
|
||||
|
||||
running_ = true;
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#endif
|
||||
}
|
||||
@@ -204,8 +197,7 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
restart_count++;
|
||||
std::cerr << "Exception caught, restarting... (attempt "
|
||||
<< restart_count << ")" << std::endl;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -237,27 +229,41 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
if (!success) {
|
||||
std::cerr << "Failed to create child process, error: " << GetLastError()
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
restart_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
while (isRunning()) {
|
||||
DWORD wait_result = WaitForSingleObject(pi.hProcess, 200);
|
||||
if (wait_result == WAIT_OBJECT_0) {
|
||||
break;
|
||||
}
|
||||
if (wait_result == WAIT_FAILED) {
|
||||
std::cerr << "Failed waiting child process, error: " << GetLastError()
|
||||
<< std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRunning()) {
|
||||
TerminateProcess(pi.hProcess, 1);
|
||||
WaitForSingleObject(pi.hProcess, 3000);
|
||||
}
|
||||
|
||||
DWORD exit_code = 0;
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
GetExitCodeProcess(pi.hProcess, &exit_code);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
if (exit_code == 0) {
|
||||
if (!isRunning() || exit_code == 0) {
|
||||
break; // normal exit
|
||||
}
|
||||
restart_count++;
|
||||
std::cerr << "Child process exited with code " << exit_code
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
#else
|
||||
// linux: use fork + exec to create child process
|
||||
pid_t pid = fork();
|
||||
@@ -266,21 +272,39 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
_exit(1); // exec failed
|
||||
} else if (pid > 0) {
|
||||
int status = 0;
|
||||
pid_t waited_pid = waitpid(pid, &status, 0);
|
||||
pid_t waited_pid = -1;
|
||||
while (isRunning()) {
|
||||
waited_pid = waitpid(pid, &status, WNOHANG);
|
||||
if (waited_pid == pid) {
|
||||
break;
|
||||
}
|
||||
if (waited_pid < 0 && errno != EINTR) {
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(kWaitPollIntervalMs));
|
||||
}
|
||||
|
||||
if (!isRunning() && waited_pid != pid) {
|
||||
kill(pid, SIGTERM);
|
||||
waited_pid = waitpid(pid, &status, 0);
|
||||
}
|
||||
|
||||
if (waited_pid < 0) {
|
||||
if (!isRunning()) {
|
||||
break;
|
||||
}
|
||||
restart_count++;
|
||||
std::cerr << "waitpid failed, errno: " << errno
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WIFEXITED(status)) {
|
||||
int exit_code = WEXITSTATUS(status);
|
||||
if (exit_code == 0) {
|
||||
if (!isRunning() || exit_code == 0) {
|
||||
break; // normal exit
|
||||
}
|
||||
restart_count++;
|
||||
@@ -288,6 +312,9 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
if (!isRunning()) {
|
||||
break;
|
||||
}
|
||||
restart_count++;
|
||||
std::cerr << "Child process crashed with signal " << WTERMSIG(status)
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
@@ -298,12 +325,10 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
"(attempt "
|
||||
<< restart_count << ")" << std::endl;
|
||||
}
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
} else {
|
||||
std::cerr << "Failed to fork child process" << std::endl;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
restart_count++;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
#ifndef _DAEMON_H_
|
||||
#define _DAEMON_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000
|
||||
|
||||
class Daemon {
|
||||
public:
|
||||
using MainLoopFunc = std::function<void()>;
|
||||
@@ -28,12 +28,10 @@ class Daemon {
|
||||
std::string name_;
|
||||
bool runWithRestart(MainLoopFunc loop);
|
||||
|
||||
#ifdef _WIN32
|
||||
bool running_;
|
||||
#else
|
||||
static Daemon* instance_;
|
||||
volatile bool running_;
|
||||
#ifndef _WIN32
|
||||
static volatile std::sig_atomic_t stop_requested_;
|
||||
#endif
|
||||
std::atomic<bool> running_;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ std::string GetHostName() {
|
||||
#ifdef _WIN32
|
||||
WSADATA wsaData;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
|
||||
std::cerr << "WSAStartup failed." << std::endl;
|
||||
LOG_ERROR("WSAStartup failed");
|
||||
return "";
|
||||
}
|
||||
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
|
||||
|
||||
63
src/common/rounded_corner_button.cpp
Normal file
63
src/common/rounded_corner_button.cpp
Normal 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
|
||||
20
src/common/rounded_corner_button.h
Normal file
20
src/common/rounded_corner_button.h
Normal 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
|
||||
25
src/common/window_util_mac.h
Normal file
25
src/common/window_util_mac.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-01-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_UTIL_MAC_H_
|
||||
#define _WINDOW_UTIL_MAC_H_
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Best-effort: keep an SDL window above normal windows on macOS.
|
||||
// No-op on non-macOS builds.
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
|
||||
|
||||
// Best-effort: exclude an SDL window from the Window menu and window cycling.
|
||||
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
|
||||
// affects the Window menu and Cmd-` window cycling.
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
64
src/common/window_util_mac.mm
Normal file
64
src/common/window_util_mac.mm
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "window_util_mac.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
|
||||
if (!window) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
|
||||
return nil;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
void* cocoa_window_ptr =
|
||||
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
|
||||
if (!cocoa_window_ptr) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge NSWindow*)cocoa_window_ptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)always_on_top;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep above normal windows.
|
||||
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
|
||||
[ns_window setLevel:level];
|
||||
|
||||
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
|
||||
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)excluded;
|
||||
return;
|
||||
}
|
||||
|
||||
[ns_window setExcludedFromWindowsMenu:excluded];
|
||||
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
|
||||
behavior |= NSWindowCollectionBehaviorTransient;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -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();
|
||||
}
|
||||
@@ -23,8 +20,14 @@ int ConfigCenter::Load() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
language_ = static_cast<LANGUAGE>(
|
||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_)));
|
||||
const long language_value =
|
||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
|
||||
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
|
||||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
|
||||
language_ = LANGUAGE::ENGLISH;
|
||||
} else {
|
||||
language_ = static_cast<LANGUAGE>(language_value);
|
||||
}
|
||||
|
||||
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
|
||||
section_, "video_quality", static_cast<long>(video_quality_)));
|
||||
@@ -70,71 +73,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 +80,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 +112,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 +119,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 +207,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 +238,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 +264,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 +357,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 +369,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 +376,19 @@ bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
|
||||
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||
} // namespace crossdesk
|
||||
|
||||
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
|
||||
file_transfer_save_path_ = path;
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string ConfigCenter::GetFileTransferSavePath() const {
|
||||
return file_transfer_save_path_;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -15,15 +15,13 @@ namespace crossdesk {
|
||||
|
||||
class ConfigCenter {
|
||||
public:
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1 };
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
|
||||
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
|
||||
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
|
||||
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
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -10,7 +13,7 @@ static void* g_user_ptr = nullptr;
|
||||
|
||||
static int KeyboardEventHandler(Display* display, XEvent* event) {
|
||||
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
|
||||
KeySym keySym = XKeycodeToKeysym(display, event->xkey.keycode, 0);
|
||||
KeySym keySym = XLookupKeysym(&event->xkey, 0);
|
||||
int key_code = XKeysymToKeycode(display, keySym);
|
||||
bool is_key_down = (event->xkey.type == KeyPress);
|
||||
|
||||
@@ -21,7 +24,9 @@ static int KeyboardEventHandler(Display* display, XEvent* event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
|
||||
KeyboardCapturer::KeyboardCapturer()
|
||||
: display_(nullptr), root_(0), running_(false) {
|
||||
XInitThreads();
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
LOG_ERROR("Failed to open X display.");
|
||||
@@ -29,35 +34,87 @@ KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
|
||||
}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {
|
||||
Unhook();
|
||||
|
||||
if (display_) {
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
if (!display_) {
|
||||
LOG_ERROR("Display not initialized.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
|
||||
XSelectInput(display_, DefaultRootWindow(display_),
|
||||
KeyPressMask | KeyReleaseMask);
|
||||
|
||||
while (running_) {
|
||||
XEvent event;
|
||||
XNextEvent(display_, &event);
|
||||
KeyboardEventHandler(display_, &event);
|
||||
if (running_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
root_ = DefaultRootWindow(display_);
|
||||
XSelectInput(display_, root_, KeyPressMask | KeyReleaseMask);
|
||||
XFlush(display_);
|
||||
|
||||
running_ = true;
|
||||
const int x11_fd = ConnectionNumber(display_);
|
||||
event_thread_ = std::thread([this, x11_fd]() {
|
||||
while (running_) {
|
||||
while (running_ && XPending(display_) > 0) {
|
||||
XEvent event;
|
||||
XNextEvent(display_, &event);
|
||||
KeyboardEventHandler(display_, &event);
|
||||
}
|
||||
|
||||
if (!running_) {
|
||||
break;
|
||||
}
|
||||
|
||||
struct pollfd pfd = {x11_fd, POLLIN, 0};
|
||||
int poll_ret = poll(&pfd, 1, 50);
|
||||
if (poll_ret < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
LOG_ERROR("poll for X11 events failed.");
|
||||
running_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (poll_ret == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
|
||||
LOG_ERROR("poll got invalid X11 event fd state.");
|
||||
running_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((pfd.revents & POLLIN) == 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Unhook() {
|
||||
running_ = false;
|
||||
|
||||
if (event_thread_.joinable()) {
|
||||
event_thread_.join();
|
||||
}
|
||||
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
|
||||
running_ = false;
|
||||
|
||||
if (display_) {
|
||||
XSelectInput(display_, DefaultRootWindow(display_), 0);
|
||||
if (display_ && root_ != 0) {
|
||||
XSelectInput(display_, root_, 0);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -28,7 +31,8 @@ class KeyboardCapturer : public DeviceController {
|
||||
private:
|
||||
Display* display_;
|
||||
Window root_;
|
||||
bool running_;
|
||||
std::atomic<bool> running_;
|
||||
std::thread event_thread_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,222 +1,156 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "localization_data.h"
|
||||
|
||||
#if _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
struct LanguageOption {
|
||||
std::string code;
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
namespace crossdesk {
|
||||
class LocalizedString {
|
||||
public:
|
||||
constexpr explicit LocalizedString(const char* key) : key_(key) {}
|
||||
const std::string& operator[](int language_index) const;
|
||||
|
||||
private:
|
||||
const char* key_;
|
||||
};
|
||||
|
||||
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
|
||||
static const std::vector<LanguageOption> kSupportedLanguages = {
|
||||
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
|
||||
{"en-US", "English"},
|
||||
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
|
||||
return kSupportedLanguages;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
namespace localization {
|
||||
inline int ClampLanguageIndex(int language_index) {
|
||||
if (language_index >= 0 &&
|
||||
language_index < static_cast<int>(GetSupportedLanguages().size())) {
|
||||
return language_index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static std::vector<std::string> local_desktop = {
|
||||
reinterpret_cast<const char*>(u8"本桌面"), "Local Desktop"};
|
||||
static std::vector<std::string> local_id = {
|
||||
reinterpret_cast<const char*>(u8"本机ID"), "Local ID"};
|
||||
static std::vector<std::string> local_id_copied_to_clipboard = {
|
||||
reinterpret_cast<const char*>(u8"已复制到剪贴板"), "Copied to clipboard"};
|
||||
static std::vector<std::string> password = {
|
||||
reinterpret_cast<const char*>(u8"密码"), "Password"};
|
||||
static std::vector<std::string> max_password_len = {
|
||||
reinterpret_cast<const char*>(u8"最大6个字符"), "Max 6 chars"};
|
||||
using TranslationTable =
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<std::string, std::string>>;
|
||||
|
||||
static std::vector<std::string> remote_desktop = {
|
||||
reinterpret_cast<const char*>(u8"控制远程桌面"), "Control Remote Desktop"};
|
||||
static std::vector<std::string> remote_id = {
|
||||
reinterpret_cast<const char*>(u8"对端ID"), "Remote ID"};
|
||||
static std::vector<std::string> connect = {
|
||||
reinterpret_cast<const char*>(u8"连接"), "Connect"};
|
||||
static std::vector<std::string> recent_connections = {
|
||||
reinterpret_cast<const char*>(u8"近期连接"), "Recent Connections"};
|
||||
static std::vector<std::string> disconnect = {
|
||||
reinterpret_cast<const char*>(u8"断开连接"), "Disconnect"};
|
||||
static std::vector<std::string> fullscreen = {
|
||||
reinterpret_cast<const char*>(u8"全屏"), " Fullscreen"};
|
||||
static std::vector<std::string> show_net_traffic_stats = {
|
||||
reinterpret_cast<const char*>(u8"显示流量统计"), "Show Net Traffic Stats"};
|
||||
static std::vector<std::string> hide_net_traffic_stats = {
|
||||
reinterpret_cast<const char*>(u8"隐藏流量统计"), "Hide Net Traffic Stats"};
|
||||
static std::vector<std::string> video = {
|
||||
reinterpret_cast<const char*>(u8"视频"), "Video"};
|
||||
static std::vector<std::string> audio = {
|
||||
reinterpret_cast<const char*>(u8"音频"), "Audio"};
|
||||
static std::vector<std::string> data = {reinterpret_cast<const char*>(u8"数据"),
|
||||
"Data"};
|
||||
static std::vector<std::string> total = {
|
||||
reinterpret_cast<const char*>(u8"总计"), "Total"};
|
||||
static std::vector<std::string> in = {reinterpret_cast<const char*>(u8"输入"),
|
||||
"In"};
|
||||
static std::vector<std::string> out = {reinterpret_cast<const char*>(u8"输出"),
|
||||
"Out"};
|
||||
static std::vector<std::string> loss_rate = {
|
||||
reinterpret_cast<const char*>(u8"丢包率"), "Loss Rate"};
|
||||
static std::vector<std::string> exit_fullscreen = {
|
||||
reinterpret_cast<const char*>(u8"退出全屏"), "Exit fullscreen"};
|
||||
static std::vector<std::string> control_mouse = {
|
||||
reinterpret_cast<const char*>(u8"控制"), "Control"};
|
||||
static std::vector<std::string> release_mouse = {
|
||||
reinterpret_cast<const char*>(u8"释放"), "Release"};
|
||||
static std::vector<std::string> audio_capture = {
|
||||
reinterpret_cast<const char*>(u8"声音"), "Audio"};
|
||||
static std::vector<std::string> mute = {
|
||||
reinterpret_cast<const char*>(u8" 静音"), " Mute"};
|
||||
static std::vector<std::string> settings = {
|
||||
reinterpret_cast<const char*>(u8"设置"), "Settings"};
|
||||
static std::vector<std::string> language = {
|
||||
reinterpret_cast<const char*>(u8"语言:"), "Language:"};
|
||||
static std::vector<std::string> language_zh = {
|
||||
reinterpret_cast<const char*>(u8"中文"), "Chinese"};
|
||||
static std::vector<std::string> language_en = {
|
||||
reinterpret_cast<const char*>(u8"英文"), "English"};
|
||||
static std::vector<std::string> video_quality = {
|
||||
reinterpret_cast<const char*>(u8"视频质量:"), "Video Quality:"};
|
||||
static std::vector<std::string> video_frame_rate = {
|
||||
reinterpret_cast<const char*>(u8"画面采集帧率:"),
|
||||
"Video Capture Frame Rate:"};
|
||||
static std::vector<std::string> video_quality_high = {
|
||||
reinterpret_cast<const char*>(u8"高"), "High"};
|
||||
static std::vector<std::string> video_quality_medium = {
|
||||
reinterpret_cast<const char*>(u8"中"), "Medium"};
|
||||
static std::vector<std::string> video_quality_low = {
|
||||
reinterpret_cast<const char*>(u8"低"), "Low"};
|
||||
static std::vector<std::string> video_encode_format = {
|
||||
reinterpret_cast<const char*>(u8"视频编码格式:"), "Video Encode Format:"};
|
||||
static std::vector<std::string> av1 = {reinterpret_cast<const char*>(u8"AV1"),
|
||||
"AV1"};
|
||||
static std::vector<std::string> h264 = {
|
||||
reinterpret_cast<const char*>(u8"H.264"), "H.264"};
|
||||
static std::vector<std::string> enable_hardware_video_codec = {
|
||||
reinterpret_cast<const char*>(u8"启用硬件编解码器:"),
|
||||
"Enable Hardware Video Codec:"};
|
||||
static std::vector<std::string> enable_turn = {
|
||||
reinterpret_cast<const char*>(u8"启用中继服务:"), "Enable TURN Service:"};
|
||||
static std::vector<std::string> enable_srtp = {
|
||||
reinterpret_cast<const char*>(u8"启用SRTP:"), "Enable SRTP:"};
|
||||
static std::vector<std::string> self_hosted_server_config = {
|
||||
reinterpret_cast<const char*>(u8"自托管服务器配置"),
|
||||
"Self-Hosted Server Config"};
|
||||
static std::vector<std::string> self_hosted_server_settings = {
|
||||
reinterpret_cast<const char*>(u8"自托管服务器设置"),
|
||||
"Self-Hosted Server Settings"};
|
||||
static std::vector<std::string> self_hosted_server_address = {
|
||||
reinterpret_cast<const char*>(u8"服务器地址:"), "Server Address:"};
|
||||
static std::vector<std::string> self_hosted_server_port = {
|
||||
reinterpret_cast<const char*>(u8"信令服务端口:"), "Signal Service Port:"};
|
||||
static std::vector<std::string> self_hosted_server_coturn_server_port = {
|
||||
reinterpret_cast<const char*>(u8"中继服务端口:"), "Relay Service Port:"};
|
||||
static std::vector<std::string> self_hosted_server_certificate_path = {
|
||||
reinterpret_cast<const char*>(u8"证书文件路径:"), "Certificate File Path:"};
|
||||
static std::vector<std::string> select_a_file = {
|
||||
reinterpret_cast<const char*>(u8"请选择文件"), "Please select a file"};
|
||||
static std::vector<std::string> reset_cert_fingerprint = {
|
||||
reinterpret_cast<const char*>(u8"重置证书指纹"),
|
||||
"Reset Certificate Fingerprint"};
|
||||
static std::vector<std::string> ok = {reinterpret_cast<const char*>(u8"确认"),
|
||||
"OK"};
|
||||
static std::vector<std::string> cancel = {
|
||||
reinterpret_cast<const char*>(u8"取消"), "Cancel"};
|
||||
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
|
||||
const TranslationRow& row) {
|
||||
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
|
||||
{"en-US", row.en},
|
||||
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
|
||||
}
|
||||
|
||||
static std::vector<std::string> new_password = {
|
||||
reinterpret_cast<const char*>(u8"请输入六位密码:"),
|
||||
"Please input a six-char password:"};
|
||||
inline TranslationTable BuildTranslationTable() {
|
||||
TranslationTable table;
|
||||
for (const auto& row : kTranslationRows) {
|
||||
table[row.key] = MakeLocalizedValues(row);
|
||||
}
|
||||
|
||||
static std::vector<std::string> input_password = {
|
||||
reinterpret_cast<const char*>(u8"请输入密码:"), "Please input password:"};
|
||||
static std::vector<std::string> validate_password = {
|
||||
reinterpret_cast<const char*>(u8"验证密码中..."), "Validate password ..."};
|
||||
static std::vector<std::string> reinput_password = {
|
||||
reinterpret_cast<const char*>(u8"请重新输入密码"),
|
||||
"Please input password again"};
|
||||
|
||||
static std::vector<std::string> remember_password = {
|
||||
reinterpret_cast<const char*>(u8"记住密码"), "Remember password"};
|
||||
|
||||
static std::vector<std::string> signal_connected = {
|
||||
reinterpret_cast<const char*>(u8"已连接服务器"), "Connected"};
|
||||
static std::vector<std::string> signal_disconnected = {
|
||||
reinterpret_cast<const char*>(u8"未连接服务器"), "Disconnected"};
|
||||
|
||||
static std::vector<std::string> p2p_connected = {
|
||||
reinterpret_cast<const char*>(u8"对等连接已建立"), "P2P Connected"};
|
||||
static std::vector<std::string> p2p_disconnected = {
|
||||
reinterpret_cast<const char*>(u8"对等连接已断开"), "P2P Disconnected"};
|
||||
static std::vector<std::string> p2p_connecting = {
|
||||
reinterpret_cast<const char*>(u8"正在建立对等连接..."),
|
||||
"P2P Connecting ..."};
|
||||
static std::vector<std::string> p2p_failed = {
|
||||
reinterpret_cast<const char*>(u8"对等连接失败"), "P2P Failed"};
|
||||
static std::vector<std::string> p2p_closed = {
|
||||
reinterpret_cast<const char*>(u8"对等连接已关闭"), "P2P closed"};
|
||||
|
||||
static std::vector<std::string> no_such_id = {
|
||||
reinterpret_cast<const char*>(u8"无此ID"), "No such ID"};
|
||||
|
||||
static std::vector<std::string> about = {
|
||||
reinterpret_cast<const char*>(u8"关于"), "About"};
|
||||
static std::vector<std::string> notification = {
|
||||
reinterpret_cast<const char*>(u8"通知"), "Notification"};
|
||||
static std::vector<std::string> new_version_available = {
|
||||
reinterpret_cast<const char*>(u8"新版本可用"), "New Version Available"};
|
||||
static std::vector<std::string> version = {
|
||||
reinterpret_cast<const char*>(u8"版本"), "Version"};
|
||||
static std::vector<std::string> release_date = {
|
||||
reinterpret_cast<const char*>(u8"发布日期: "), "Release Date: "};
|
||||
static std::vector<std::string> access_website = {
|
||||
reinterpret_cast<const char*>(u8"访问官网: "), "Access Website: "};
|
||||
static std::vector<std::string> update = {
|
||||
reinterpret_cast<const char*>(u8"更新"), "Update"};
|
||||
|
||||
static std::vector<std::string> confirm_delete_connection = {
|
||||
reinterpret_cast<const char*>(u8"确认删除此连接"),
|
||||
"Confirm to delete this connection"};
|
||||
|
||||
static std::vector<std::string> enable_autostart = {
|
||||
reinterpret_cast<const char*>(u8"开机自启:"), "Auto Start:"};
|
||||
static std::vector<std::string> enable_daemon = {
|
||||
reinterpret_cast<const char*>(u8"启用守护进程:"), "Enable Daemon:"};
|
||||
static std::vector<std::string> takes_effect_after_restart = {
|
||||
reinterpret_cast<const char*>(u8"重启后生效"),
|
||||
"Takes effect after restart"};
|
||||
static std::vector<std::string> select_file = {
|
||||
reinterpret_cast<const char*>(u8"选择文件"), "Select File"};
|
||||
static std::vector<std::string> file_transfer_progress = {
|
||||
reinterpret_cast<const char*>(u8"文件传输进度"), "File Transfer Progress"};
|
||||
static std::vector<std::string> queued = {
|
||||
reinterpret_cast<const char*>(u8"队列中"), "Queued"};
|
||||
static std::vector<std::string> sending = {
|
||||
reinterpret_cast<const char*>(u8"正在传输"), "Sending"};
|
||||
static std::vector<std::string> completed = {
|
||||
reinterpret_cast<const char*>(u8"已完成"), "Completed"};
|
||||
static std::vector<std::string> failed = {
|
||||
reinterpret_cast<const char*>(u8"失败"), "Failed"};
|
||||
|
||||
#if _WIN32
|
||||
static std::vector<std::string> minimize_to_tray = {
|
||||
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
|
||||
"Minimize to system tray when exit:"};
|
||||
static std::vector<LPCWSTR> exit_program = {L"退出", L"Exit"};
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
static std::vector<std::string> request_permissions = {
|
||||
reinterpret_cast<const char*>(u8"权限请求"), "Request Permissions"};
|
||||
static std::vector<std::string> screen_recording_permission = {
|
||||
reinterpret_cast<const char*>(u8"屏幕录制权限"),
|
||||
"Screen Recording Permission"};
|
||||
static std::vector<std::string> accessibility_permission = {
|
||||
reinterpret_cast<const char*>(u8"辅助功能权限"),
|
||||
"Accessibility Permission"};
|
||||
static std::vector<std::string> permission_required_message = {
|
||||
reinterpret_cast<const char*>(u8"该应用需要授权以下权限:"),
|
||||
"The application requires the following permissions:"};
|
||||
#endif
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
return table;
|
||||
}
|
||||
|
||||
inline const TranslationTable& GetTranslationTable() {
|
||||
static const TranslationTable table = BuildTranslationTable();
|
||||
return table;
|
||||
}
|
||||
|
||||
inline const std::string& GetTranslatedText(const std::string& key,
|
||||
int language_index) {
|
||||
static const std::string kEmptyText = "";
|
||||
|
||||
const auto& table = GetTranslationTable();
|
||||
const auto key_it = table.find(key);
|
||||
if (key_it == table.end()) {
|
||||
return kEmptyText;
|
||||
}
|
||||
|
||||
const auto& localized_values = key_it->second;
|
||||
const std::string& language_code =
|
||||
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
|
||||
|
||||
const auto exact_it = localized_values.find(language_code);
|
||||
if (exact_it != localized_values.end()) {
|
||||
return exact_it->second;
|
||||
}
|
||||
|
||||
const auto english_it = localized_values.find("en-US");
|
||||
if (english_it != localized_values.end()) {
|
||||
return english_it->second;
|
||||
}
|
||||
|
||||
const auto chinese_it = localized_values.find("zh-CN");
|
||||
if (chinese_it != localized_values.end()) {
|
||||
return chinese_it->second;
|
||||
}
|
||||
|
||||
return kEmptyText;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline const std::string& LocalizedString::operator[](
|
||||
int language_index) const {
|
||||
return detail::GetTranslatedText(key_, language_index);
|
||||
}
|
||||
|
||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
|
||||
inline const LocalizedString name(#name);
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||
|
||||
#if _WIN32
|
||||
inline const wchar_t* GetExitProgramLabel(int language_index) {
|
||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||
const int normalized_index = detail::ClampLanguageIndex(language_index);
|
||||
std::wstring& cached_text = cache[normalized_index];
|
||||
if (!cached_text.empty()) {
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
const std::string& utf8_text =
|
||||
detail::GetTranslatedText("exit_program", normalized_index);
|
||||
if (utf8_text.empty()) {
|
||||
cached_text = L"Exit";
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
int wide_length =
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
|
||||
if (wide_length <= 0) {
|
||||
cached_text = L"Exit";
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
cached_text.resize(static_cast<size_t>(wide_length - 1));
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
|
||||
wide_length);
|
||||
return cached_text.c_str();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
|
||||
166
src/gui/assets/localization/localization_data.h
Normal file
166
src/gui/assets/localization/localization_data.h
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_DATA_H_
|
||||
#define _LOCALIZATION_DATA_H_
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct TranslationRow {
|
||||
const char* key;
|
||||
const char* zh;
|
||||
const char* en;
|
||||
const char* ru;
|
||||
};
|
||||
|
||||
// Single source of truth for all UI strings.
|
||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||
u8"Скопировано в буфер обмена") \
|
||||
X(password, u8"密码", "Password", u8"Пароль") \
|
||||
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
|
||||
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
||||
u8"Удаленный рабочий стол") \
|
||||
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
|
||||
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
||||
X(recent_connections, u8"近期连接", "Recent Connections", \
|
||||
u8"Недавние подключения") \
|
||||
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
||||
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
|
||||
X(show_net_traffic_stats, u8"显示流量统计", "Show Net Traffic Stats", \
|
||||
u8"Показать статистику трафика") \
|
||||
X(hide_net_traffic_stats, u8"隐藏流量统计", "Hide Net Traffic Stats", \
|
||||
u8"Скрыть статистику трафика") \
|
||||
X(video, u8"视频", "Video", u8"Видео") \
|
||||
X(audio, u8"音频", "Audio", u8"Аудио") \
|
||||
X(data, u8"数据", "Data", u8"Данные") \
|
||||
X(total, u8"总计", "Total", u8"Итого") \
|
||||
X(in, u8"输入", "In", u8"Вход") \
|
||||
X(out, u8"输出", "Out", u8"Выход") \
|
||||
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
|
||||
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
|
||||
u8"Выйти из полноэкранного режима") \
|
||||
X(control_mouse, u8"控制", "Control", u8"Управление") \
|
||||
X(release_mouse, u8"释放", "Release", u8"Освободить") \
|
||||
X(audio_capture, u8"声音", "Audio", u8"Звук") \
|
||||
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_frame_rate, u8"画面采集帧率:", \
|
||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||
X(video_quality_high, u8"高", "High", u8"Высокое") \
|
||||
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
|
||||
X(video_quality_low, u8"低", "Low", u8"Низкое") \
|
||||
X(video_encode_format, u8"视频编码格式:", \
|
||||
"Video Encode Format:", u8"Формат кодека видео:") \
|
||||
X(av1, u8"AV1", "AV1", "AV1") \
|
||||
X(h264, u8"H.264", "H.264", "H.264") \
|
||||
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
|
||||
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
|
||||
X(enable_turn, u8"启用中继服务:", \
|
||||
"Enable TURN Service:", u8"Включить TURN-сервис:") \
|
||||
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
|
||||
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
|
||||
u8"Конфигурация self-hosted") \
|
||||
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
|
||||
u8"Настройки self-hosted") \
|
||||
X(self_hosted_server_address, u8"服务器地址:", \
|
||||
"Server Address:", u8"Адрес сервера:") \
|
||||
X(self_hosted_server_port, u8"信令服务端口:", \
|
||||
"Signal Service Port:", u8"Порт сигнального сервиса:") \
|
||||
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
|
||||
"Relay Service Port:", u8"Порт реле-сервиса:") \
|
||||
X(ok, u8"确认", "OK", u8"ОК") \
|
||||
X(cancel, u8"取消", "Cancel", u8"Отмена") \
|
||||
X(new_password, u8"请输入六位密码:", \
|
||||
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
|
||||
X(input_password, u8"请输入密码:", \
|
||||
"Please input password:", u8"Введите пароль:") \
|
||||
X(validate_password, u8"验证密码中...", "Validate password ...", \
|
||||
u8"Проверка пароля...") \
|
||||
X(reinput_password, u8"请重新输入密码", "Please input password again", \
|
||||
u8"Повторно введите пароль") \
|
||||
X(remember_password, u8"记住密码", "Remember password", \
|
||||
u8"Запомнить пароль") \
|
||||
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
||||
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
||||
u8"Нет подключения к серверу") \
|
||||
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
||||
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
||||
u8"P2P отключено") \
|
||||
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
||||
u8"Подключение P2P...") \
|
||||
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
||||
u8"Получение изображения...") \
|
||||
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
||||
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
||||
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
||||
X(about, u8"关于", "About", u8"О программе") \
|
||||
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
||||
X(new_version_available, u8"新版本可用", "New Version Available", \
|
||||
u8"Доступна новая версия") \
|
||||
X(version, u8"版本", "Version", u8"Версия") \
|
||||
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
||||
X(access_website, u8"访问官网: ", \
|
||||
"Access Website: ", u8"Официальный сайт: ") \
|
||||
X(update, u8"更新", "Update", u8"Обновить") \
|
||||
X(confirm_delete_connection, u8"确认删除此连接", \
|
||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
|
||||
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
|
||||
u8"Вступит в силу после перезапуска") \
|
||||
X(select_file, u8"选择文件", "Select File", u8"Выбрать файл") \
|
||||
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
|
||||
u8"Прогресс передачи файлов") \
|
||||
X(queued, u8"队列中", "Queued", u8"В очереди") \
|
||||
X(sending, u8"正在传输", "Sending", u8"Передача") \
|
||||
X(completed, u8"已完成", "Completed", u8"Завершено") \
|
||||
X(failed, u8"失败", "Failed", u8"Ошибка") \
|
||||
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
|
||||
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
|
||||
X(connection_status, u8"连接状态:", \
|
||||
"Connection Status:", u8"Состояние соединения:") \
|
||||
X(file_transfer_save_path, u8"文件接收保存路径:", \
|
||||
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
|
||||
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
||||
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
|
||||
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
|
||||
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
||||
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
||||
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
||||
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
|
||||
X(online, u8"在线", "Online", u8"Онлайн") \
|
||||
X(offline, u8"离线", "Offline", u8"Офлайн") \
|
||||
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
|
||||
X(request_permissions, u8"权限请求", "Request Permissions", \
|
||||
u8"Запрос разрешений") \
|
||||
X(screen_recording_permission, u8"屏幕录制权限", \
|
||||
"Screen Recording Permission", u8"Разрешение на запись экрана") \
|
||||
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
|
||||
u8"Разрешение специальных возможностей") \
|
||||
X(permission_required_message, u8"该应用需要授权以下权限:", \
|
||||
"The application requires the following permissions:", \
|
||||
u8"Для работы приложения требуются следующие разрешения:") \
|
||||
X(exit_program, u8"退出", "Exit", u8"Выход")
|
||||
|
||||
inline constexpr TranslationRow kTranslationRows[] = {
|
||||
#define CROSSDESK_DECLARE_TRANSLATION_ROW(name, zh, en, ru) {#name, zh, en, ru},
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_TRANSLATION_ROW)
|
||||
#undef CROSSDESK_DECLARE_TRANSLATION_ROW
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
36
src/gui/device_presence.h
Normal file
36
src/gui/device_presence.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-28
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DEVICE_PRESENCE_H_
|
||||
#define _DEVICE_PRESENCE_H_
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
class DevicePresence {
|
||||
public:
|
||||
void SetOnline(const std::string& device_id, bool online) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
cache_[device_id] = online;
|
||||
}
|
||||
|
||||
bool IsOnline(const std::string& device_id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return cache_.count(device_id) > 0 && cache_.at(device_id);
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
cache_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, bool> cache_;
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -17,7 +17,7 @@ int Render::LocalWindow() {
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
|
||||
ImGuiCond_Always);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
|
||||
ImGui::BeginChild("LocalDesktopWindow",
|
||||
@@ -42,11 +42,11 @@ int Render::LocalWindow() {
|
||||
ImGuiCond_Always);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255,
|
||||
242.0f / 255, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
|
||||
ImGui::BeginChild(
|
||||
"LocalDesktopPanel",
|
||||
ImVec2(local_window_width * 0.8f, local_window_height * 0.43f),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -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, window_rounding_);
|
||||
ImGui::Begin("ConnectionStatusWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
@@ -177,10 +177,11 @@ int Render::LocalWindow() {
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding,
|
||||
window_rounding_ * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
|
||||
ImGui::Begin("ResetPasswordWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
@@ -299,4 +300,4 @@ int Render::LocalWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -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));
|
||||
@@ -17,7 +17,7 @@ int Render::RecentConnectionsWindow() {
|
||||
ImGui::BeginChild(
|
||||
"RecentConnectionsWindow",
|
||||
ImVec2(recent_connection_window_width, recent_connection_window_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -64,7 +64,7 @@ int Render::ShowRecentConnections() {
|
||||
ImGui::BeginChild(
|
||||
"RecentConnectionsContainer",
|
||||
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
|
||||
@@ -122,6 +122,8 @@ int Render::ShowRecentConnections() {
|
||||
it.second.remote_host_name = "unknown";
|
||||
}
|
||||
|
||||
bool online = device_presence_.IsOnline(it.second.remote_id);
|
||||
|
||||
ImVec2 image_screen_pos = ImVec2(
|
||||
ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f,
|
||||
ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f);
|
||||
@@ -132,6 +134,29 @@ int Render::ShowRecentConnections() {
|
||||
ImGui::Image(
|
||||
(ImTextureID)(intptr_t)it.second.texture,
|
||||
ImVec2(recent_connection_image_width, recent_connection_image_height));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
std::string display_host_name_with_presence =
|
||||
it.second.remote_host_name + " " +
|
||||
(online ? localization::online[localization_language_index_]
|
||||
: localization::offline[localization_language_index_]);
|
||||
ImGui::Text("%s", display_host_name_with_presence.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
ImVec2 circle_pos =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 0.07f,
|
||||
image_screen_pos.y + recent_connection_image_height * 0.12f);
|
||||
ImU32 fill_color =
|
||||
online ? IM_COL32(0, 255, 0, 255) : IM_COL32(140, 140, 140, 255);
|
||||
ImU32 border_color = IM_COL32(255, 255, 255, 255);
|
||||
float dot_radius = recent_connection_image_height * 0.06f;
|
||||
draw_list->AddCircleFilled(circle_pos, dot_radius * 1.25f, border_color,
|
||||
100);
|
||||
draw_list->AddCircleFilled(circle_pos, dot_radius, fill_color, 100);
|
||||
|
||||
// remote id display button
|
||||
{
|
||||
@@ -155,14 +180,6 @@ int Render::ShowRecentConnections() {
|
||||
ImGui::Text("%s", it.second.remote_id.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::Text("%s", it.second.remote_host_name.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
|
||||
@@ -242,6 +259,9 @@ int Render::ShowRecentConnections() {
|
||||
if (show_confirm_delete_connection_) {
|
||||
ConfirmDeleteConnection();
|
||||
}
|
||||
if (show_offline_warning_window_) {
|
||||
OfflineWarningWindow();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -253,10 +273,10 @@ int Render::ConfirmDeleteConnection() {
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
|
||||
ImGui::Begin("ConfirmDeleteConnectionWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
@@ -299,4 +319,45 @@ int Render::ConfirmDeleteConnection() {
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
int Render::OfflineWarningWindow() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
|
||||
ImGui::Begin("OfflineWarningWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto window_width = ImGui::GetWindowSize().x;
|
||||
auto window_height = ImGui::GetWindowSize().y;
|
||||
|
||||
ImGui::SetCursorPosX(window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(window_height * 0.67f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_offline_warning_window_ = false;
|
||||
}
|
||||
|
||||
auto text_width = ImGui::CalcTextSize(offline_warning_text_.c_str()).x;
|
||||
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(window_height * 0.2f);
|
||||
ImGui::Text("%s", offline_warning_text_.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -18,7 +18,7 @@ int Render::RemoteWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
|
||||
ImGuiCond_Always);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
|
||||
ImGui::BeginChild("RemoteDesktopWindow",
|
||||
@@ -48,7 +48,7 @@ int Render::RemoteWindow() {
|
||||
ImGui::BeginChild(
|
||||
"RemoteDesktopWindow_1",
|
||||
ImVec2(remote_window_width * 0.8f, remote_window_height * 0.43f),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -165,7 +165,21 @@ static int InputTextCallback(ImGuiInputTextCallbackData* data) {
|
||||
}
|
||||
|
||||
int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password) {
|
||||
bool remember_password, bool bypass_presence_check) {
|
||||
if (!bypass_presence_check && !device_presence_.IsOnline(remote_id)) {
|
||||
int ret =
|
||||
RequestSingleDevicePresence(remote_id, password, remember_password);
|
||||
if (ret != 0) {
|
||||
offline_warning_text_ =
|
||||
localization::device_offline[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
|
||||
} else {
|
||||
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG_INFO("Connect to [{}]", remote_id);
|
||||
focused_remote_id_ = remote_id;
|
||||
|
||||
@@ -195,7 +209,10 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->control_window_min_width_ = title_bar_height_ * 0.65f;
|
||||
props->control_window_min_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;
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
show_connection_status_window_ = true;
|
||||
|
||||
if (!props->peer_) {
|
||||
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
|
||||
@@ -207,6 +224,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
|
||||
@@ -252,4 +270,4 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
1162
src/gui/render.cpp
1162
src/gui/render.cpp
File diff suppressed because it is too large
Load Diff
217
src/gui/render.h
217
src/gui/render.h
@@ -25,6 +25,7 @@
|
||||
#include "IconsFontAwesome6.h"
|
||||
#include "config_center.h"
|
||||
#include "device_controller_factory.h"
|
||||
#include "device_presence.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_sdl3.h"
|
||||
#include "imgui_impl_sdlrenderer3.h"
|
||||
@@ -42,12 +43,48 @@
|
||||
namespace crossdesk {
|
||||
class Render {
|
||||
public:
|
||||
struct FileTransferState {
|
||||
std::atomic<bool> file_sending_ = false;
|
||||
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||
std::atomic<uint64_t> file_total_bytes_ = 0;
|
||||
std::atomic<uint32_t> file_send_rate_bps_ = 0;
|
||||
std::mutex file_transfer_mutex_;
|
||||
std::chrono::steady_clock::time_point file_send_start_time_;
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
std::filesystem::path file_path;
|
||||
std::string file_label;
|
||||
std::string remote_id;
|
||||
};
|
||||
std::queue<QueuedFile> file_send_queue_;
|
||||
std::mutex file_queue_mutex_;
|
||||
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
|
||||
|
||||
struct FileTransferInfo {
|
||||
std::string file_name;
|
||||
std::filesystem::path file_path;
|
||||
uint64_t file_size = 0;
|
||||
FileTransferStatus status = FileTransferStatus::Queued;
|
||||
uint64_t sent_bytes = 0;
|
||||
uint32_t file_id = 0;
|
||||
uint32_t rate_bps = 0;
|
||||
};
|
||||
std::vector<FileTransferInfo> file_transfer_list_;
|
||||
std::mutex file_transfer_list_mutex_;
|
||||
};
|
||||
|
||||
struct SubStreamWindowProperties {
|
||||
Params params_;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::string audio_label_ = "control_audio";
|
||||
std::string data_label_ = "control_data";
|
||||
std::string data_label_ = "data";
|
||||
std::string file_label_ = "file";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_feedback_label_ = "file_feedback";
|
||||
std::string clipboard_label_ = "clipboard";
|
||||
std::string local_id_ = "";
|
||||
@@ -58,7 +95,7 @@ class Render {
|
||||
bool connection_established_ = false;
|
||||
bool rejoin_ = false;
|
||||
bool net_traffic_stats_button_pressed_ = false;
|
||||
bool mouse_control_button_pressed_ = true;
|
||||
bool enable_mouse_control_ = true;
|
||||
bool mouse_controller_is_started_ = false;
|
||||
bool audio_capture_button_pressed_ = true;
|
||||
bool control_mouse_ = true;
|
||||
@@ -87,8 +124,13 @@ class Render {
|
||||
float mouse_diff_control_bar_pos_y_ = 0;
|
||||
double control_bar_button_pressed_time_ = 0;
|
||||
double net_traffic_stats_button_pressed_time_ = 0;
|
||||
unsigned char* dst_buffer_ = nullptr;
|
||||
size_t dst_buffer_capacity_ = 0;
|
||||
// Double-buffered NV12 frame storage. Written by decode callback thread,
|
||||
// consumed by SDL main thread.
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
bool render_rect_dirty_ = false;
|
||||
bool stream_cleanup_pending_ = false;
|
||||
float mouse_pos_x_ = 0;
|
||||
float mouse_pos_y_ = 0;
|
||||
float mouse_pos_x_last_ = 0;
|
||||
@@ -128,38 +170,10 @@ class Render {
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
XNetTrafficStats net_traffic_stats_;
|
||||
|
||||
// File transfer progress
|
||||
std::atomic<bool> file_sending_ = false;
|
||||
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||
std::atomic<uint64_t> file_total_bytes_ = 0;
|
||||
std::atomic<uint32_t> file_send_rate_bps_ = 0;
|
||||
std::mutex file_transfer_mutex_;
|
||||
std::chrono::steady_clock::time_point file_send_start_time_;
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
std::filesystem::path file_path;
|
||||
std::string file_label;
|
||||
};
|
||||
std::queue<QueuedFile> file_send_queue_;
|
||||
std::mutex file_queue_mutex_;
|
||||
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
|
||||
|
||||
struct FileTransferInfo {
|
||||
std::string file_name;
|
||||
std::filesystem::path file_path;
|
||||
uint64_t file_size = 0;
|
||||
FileTransferStatus status = FileTransferStatus::Queued;
|
||||
uint64_t sent_bytes = 0;
|
||||
uint32_t file_id = 0;
|
||||
uint32_t rate_bps = 0;
|
||||
};
|
||||
std::vector<FileTransferInfo> file_transfer_list_;
|
||||
std::mutex file_transfer_list_mutex_;
|
||||
using QueuedFile = FileTransferState::QueuedFile;
|
||||
using FileTransferStatus = FileTransferState::FileTransferStatus;
|
||||
using FileTransferInfo = FileTransferState::FileTransferInfo;
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -179,7 +193,10 @@ class Render {
|
||||
void UpdateLabels();
|
||||
void UpdateInteractions();
|
||||
void HandleRecentConnections();
|
||||
void HandleConnectionStatusChange();
|
||||
void HandlePendingPresenceProbe();
|
||||
void HandleStreamWindow();
|
||||
void HandleServerWindow();
|
||||
void Cleanup();
|
||||
void CleanupFactories();
|
||||
void CleanupPeer(std::shared_ptr<SubStreamWindowProperties> props);
|
||||
@@ -189,18 +206,29 @@ class Render {
|
||||
void UpdateRenderRect();
|
||||
void ProcessSdlEvent(const SDL_Event& event);
|
||||
|
||||
void ProcessFileDropEvent(const SDL_Event& event);
|
||||
|
||||
void ProcessSelectedFile(
|
||||
const std::string& path,
|
||||
const std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label, const std::string& remote_id = "");
|
||||
|
||||
std::shared_ptr<SubStreamWindowProperties>
|
||||
GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id);
|
||||
|
||||
private:
|
||||
int CreateStreamRenderWindow();
|
||||
int TitleBar(bool main_window);
|
||||
int MainWindow();
|
||||
int UpdateNotificationWindow();
|
||||
int StreamWindow();
|
||||
int ServerWindow();
|
||||
int RemoteClientInfoWindow();
|
||||
int LocalWindow();
|
||||
int RemoteWindow();
|
||||
int RecentConnectionsWindow();
|
||||
int SettingWindow();
|
||||
int SelfHostedServerWindow();
|
||||
int ShowSimpleFileBrowser();
|
||||
int ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
int ControlBar(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
int AboutWindow();
|
||||
@@ -208,26 +236,37 @@ class Render {
|
||||
bool ConnectionStatusWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
int ShowRecentConnections();
|
||||
bool OpenUrl(const std::string& url);
|
||||
void Hyperlink(const std::string& label, const std::string& url,
|
||||
const float window_width);
|
||||
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
std::string OpenFileDialog(std::string title);
|
||||
|
||||
private:
|
||||
int ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password);
|
||||
bool remember_password, bool bypass_presence_check = false);
|
||||
int RequestSingleDevicePresence(const std::string& remote_id,
|
||||
const char* password, bool remember_password);
|
||||
int CreateMainWindow();
|
||||
int DestroyMainWindow();
|
||||
int CreateStreamWindow();
|
||||
int DestroyStreamWindow();
|
||||
int SetupFontAndStyle(bool main_window);
|
||||
int CreateServerWindow();
|
||||
int DestroyServerWindow();
|
||||
int SetupFontAndStyle(ImFont** system_chinese_font_out);
|
||||
int DestroyMainWindowContext();
|
||||
int DestroyStreamWindowContext();
|
||||
int DestroyServerWindowContext();
|
||||
int DrawMainWindow();
|
||||
int DrawStreamWindow();
|
||||
int DrawServerWindow();
|
||||
int ConfirmDeleteConnection();
|
||||
int OfflineWarningWindow();
|
||||
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void DrawConnectionStatusText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void DrawReceivingScreenText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
#ifdef __APPLE__
|
||||
int RequestPermissionWindow();
|
||||
bool CheckScreenRecordingPermission();
|
||||
@@ -256,14 +295,17 @@ class Render {
|
||||
static void OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data);
|
||||
|
||||
static void OnSignalMessageCb(const char* message, size_t size,
|
||||
void* user_data);
|
||||
|
||||
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data);
|
||||
|
||||
static void NetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data);
|
||||
static void OnNetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data);
|
||||
|
||||
static SDL_HitTestResult HitTestCallback(SDL_Window* window,
|
||||
const SDL_Point* area, void* data);
|
||||
@@ -304,7 +346,8 @@ class Render {
|
||||
// File transfer helper functions
|
||||
void StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
|
||||
const std::filesystem::path& file_path,
|
||||
const std::string& file_label);
|
||||
const std::string& file_label,
|
||||
const std::string& remote_id = "");
|
||||
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
|
||||
|
||||
int AudioDeviceInit();
|
||||
@@ -349,7 +392,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_;
|
||||
@@ -372,9 +414,12 @@ class Render {
|
||||
// recent connections
|
||||
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
|
||||
recent_connections_;
|
||||
std::vector<std::string> recent_connection_ids_;
|
||||
int recent_connection_image_width_ = 160;
|
||||
int recent_connection_image_height_ = 90;
|
||||
uint32_t recent_connection_image_save_time_ = 0;
|
||||
DevicePresence device_presence_;
|
||||
bool need_to_send_recent_connections_ = true;
|
||||
|
||||
// main window render
|
||||
SDL_Window* main_window_ = nullptr;
|
||||
@@ -382,6 +427,7 @@ class Render {
|
||||
ImGuiContext* main_ctx_ = nullptr;
|
||||
ImFont* main_windows_system_chinese_font_ = nullptr;
|
||||
ImFont* stream_windows_system_chinese_font_ = nullptr;
|
||||
ImFont* server_windows_system_chinese_font_ = nullptr;
|
||||
bool exit_ = false;
|
||||
const int sdl_refresh_ms_ = 16; // ~60 FPS
|
||||
#if _WIN32
|
||||
@@ -399,11 +445,14 @@ class Render {
|
||||
bool screen_capturer_is_started_ = false;
|
||||
bool start_speaker_capturer_ = false;
|
||||
bool speaker_capturer_is_started_ = false;
|
||||
bool start_keyboard_capturer_ = true;
|
||||
bool start_keyboard_capturer_ = false;
|
||||
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;
|
||||
bool audio_capture_ = false;
|
||||
int main_window_width_real_ = 720;
|
||||
int main_window_height_real_ = 540;
|
||||
@@ -453,7 +502,7 @@ class Render {
|
||||
bool just_created_ = false;
|
||||
std::string controlled_remote_id_ = "";
|
||||
std::string focused_remote_id_ = "";
|
||||
bool need_to_send_host_info_ = false;
|
||||
std::string remote_client_id_ = "";
|
||||
SDL_Event last_mouse_event;
|
||||
SDL_AudioStream* output_stream_;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
@@ -480,6 +529,44 @@ class Render {
|
||||
float stream_window_dpi_scaling_w_ = 1.0f;
|
||||
float stream_window_dpi_scaling_h_ = 1.0f;
|
||||
|
||||
// server window render
|
||||
SDL_Window* server_window_ = nullptr;
|
||||
SDL_Renderer* server_renderer_ = nullptr;
|
||||
ImGuiContext* server_ctx_ = nullptr;
|
||||
|
||||
// server window properties
|
||||
bool need_to_create_server_window_ = false;
|
||||
bool need_to_destroy_server_window_ = false;
|
||||
bool server_window_created_ = false;
|
||||
bool server_window_inited_ = false;
|
||||
int server_window_width_default_ = 250;
|
||||
int server_window_height_default_ = 150;
|
||||
float server_window_width_ = 250;
|
||||
float server_window_height_ = 150;
|
||||
float server_window_title_bar_height_ = 30.0f;
|
||||
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
float server_window_dpi_scaling_h_ = 1.0f;
|
||||
float window_rounding_ = 6.0f;
|
||||
float window_rounding_default_ = 6.0f;
|
||||
|
||||
// server window collapsed mode
|
||||
bool server_window_collapsed_ = false;
|
||||
bool server_window_collapsed_dragging_ = false;
|
||||
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_collapsed_drag_start_win_x_ = 0;
|
||||
int server_window_collapsed_drag_start_win_y_ = 0;
|
||||
|
||||
// server window drag normal mode
|
||||
bool server_window_dragging_ = false;
|
||||
float server_window_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_drag_start_win_x_ = 0;
|
||||
int server_window_drag_start_win_y_ = 0;
|
||||
|
||||
bool label_inited_ = false;
|
||||
bool connect_button_pressed_ = false;
|
||||
bool password_validating_ = false;
|
||||
@@ -496,11 +583,14 @@ class Render {
|
||||
bool fullscreen_button_pressed_ = false;
|
||||
bool focus_on_input_widget_ = true;
|
||||
bool is_client_mode_ = false;
|
||||
bool is_server_mode_ = false;
|
||||
bool reload_recent_connections_ = true;
|
||||
bool show_confirm_delete_connection_ = false;
|
||||
bool show_offline_warning_window_ = false;
|
||||
bool delete_connection_ = false;
|
||||
bool is_tab_bar_hovered_ = false;
|
||||
std::string delete_connection_name_ = "";
|
||||
std::string offline_warning_text_ = "";
|
||||
bool re_enter_remote_id_ = false;
|
||||
double copy_start_time_ = 0;
|
||||
SignalStatus signal_status_ = SignalStatus::SignalClosed;
|
||||
@@ -522,6 +612,10 @@ class Render {
|
||||
std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
|
||||
file_id_to_props_;
|
||||
std::shared_mutex file_id_to_props_mutex_;
|
||||
|
||||
// Map file_id to FileTransferState for global file transfer (props == null)
|
||||
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
|
||||
std::shared_mutex file_id_to_transfer_state_mutex_;
|
||||
SDL_AudioDeviceID input_dev_;
|
||||
SDL_AudioDeviceID output_dev_;
|
||||
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
|
||||
@@ -535,8 +629,8 @@ class Render {
|
||||
uint64_t last_frame_time_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
uint64_t new_version_icon_last_trigger_time_ = 0;
|
||||
uint64_t new_version_icon_render_start_time_ = 0;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
double new_version_icon_render_start_time_ = 0.0;
|
||||
#ifdef __APPLE__
|
||||
bool show_request_permission_window_ = true;
|
||||
#endif
|
||||
@@ -547,16 +641,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;
|
||||
@@ -572,10 +665,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_ = "";
|
||||
@@ -596,6 +690,17 @@ class Render {
|
||||
|
||||
/* ------ server mode ------ */
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
std::unordered_map<std::string, std::string> connection_host_names_;
|
||||
std::string selected_server_remote_id_ = "";
|
||||
std::string selected_server_remote_hostname_ = "";
|
||||
std::mutex pending_presence_probe_mutex_;
|
||||
bool pending_presence_probe_ = false;
|
||||
bool pending_presence_result_ready_ = false;
|
||||
bool pending_presence_online_ = false;
|
||||
std::string pending_presence_remote_id_ = "";
|
||||
std::string pending_presence_password_ = "";
|
||||
bool pending_presence_remember_password_ = false;
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "device_controller.h"
|
||||
@@ -19,6 +21,65 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
void Render::OnSignalMessageCb(const char* message, size_t size,
|
||||
void* user_data) {
|
||||
Render* render = (Render*)user_data;
|
||||
if (!render || !message || size == 0) {
|
||||
return;
|
||||
}
|
||||
std::string s(message, size);
|
||||
auto j = nlohmann::json::parse(s, nullptr, false);
|
||||
if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) {
|
||||
return;
|
||||
}
|
||||
std::string type = j["type"].get<std::string>();
|
||||
if (type == "presence") {
|
||||
if (j.contains("devices") && j["devices"].is_array()) {
|
||||
for (auto& dev : j["devices"]) {
|
||||
if (!dev.is_object()) {
|
||||
continue;
|
||||
}
|
||||
if (!dev.contains("id") || !dev["id"].is_string()) {
|
||||
continue;
|
||||
}
|
||||
if (!dev.contains("online") || !dev["online"].is_boolean()) {
|
||||
continue;
|
||||
}
|
||||
std::string id = dev["id"].get<std::string>();
|
||||
bool online = dev["online"].get<bool>();
|
||||
render->device_presence_.SetOnline(id, online);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
render->pending_presence_probe_mutex_);
|
||||
if (render->pending_presence_probe_ &&
|
||||
render->pending_presence_remote_id_ == id) {
|
||||
render->pending_presence_result_ready_ = true;
|
||||
render->pending_presence_online_ = online;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type == "presence_update") {
|
||||
if (j.contains("id") && j["id"].is_string() && j.contains("online") &&
|
||||
j["online"].is_boolean()) {
|
||||
std::string id = j["id"].get<std::string>();
|
||||
bool online = j["online"].get<bool>();
|
||||
if (!id.empty()) {
|
||||
render->device_presence_.SetOnline(id, online);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
render->pending_presence_probe_mutex_);
|
||||
if (render->pending_presence_probe_ &&
|
||||
render->pending_presence_remote_id_ == id) {
|
||||
render->pending_presence_result_ready_ = true;
|
||||
render->pending_presence_online_ = online;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Render::SendKeyCommand(int key_code, bool is_down) {
|
||||
RemoteAction remote_action;
|
||||
remote_action.type = ControlType::keyboard;
|
||||
@@ -29,17 +90,16 @@ int Render::SendKeyCommand(int key_code, bool is_down) {
|
||||
}
|
||||
remote_action.k.key_value = key_code;
|
||||
|
||||
if (!controlled_remote_id_.empty()) {
|
||||
// std::shared_lock lock(client_properties_mutex_);
|
||||
if (client_properties_.find(controlled_remote_id_) !=
|
||||
client_properties_.end()) {
|
||||
auto props = client_properties_[controlled_remote_id_];
|
||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
||||
std::string target_id = controlled_remote_id_.empty() ? focused_remote_id_
|
||||
: controlled_remote_id_;
|
||||
if (!target_id.empty()) {
|
||||
if (client_properties_.find(target_id) != client_properties_.end()) {
|
||||
auto props = client_properties_[target_id];
|
||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
||||
props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
if (props->peer_) {
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
}
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,11 +163,10 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
}
|
||||
|
||||
if (props->control_bar_hovered_ || props->display_selectable_hovered_) {
|
||||
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());
|
||||
}
|
||||
@@ -153,8 +212,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());
|
||||
}
|
||||
@@ -237,31 +299,31 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||
render->client_properties_.find(remote_id)->second.get();
|
||||
|
||||
if (props->connection_established_) {
|
||||
if (!props->dst_buffer_) {
|
||||
props->dst_buffer_capacity_ = video_frame->size;
|
||||
props->dst_buffer_ = new unsigned char[video_frame->size];
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
|
||||
if (props->dst_buffer_capacity_ < video_frame->size) {
|
||||
delete props->dst_buffer_;
|
||||
props->dst_buffer_capacity_ = video_frame->size;
|
||||
props->dst_buffer_ = new unsigned char[video_frame->size];
|
||||
}
|
||||
if (!props->back_frame_) {
|
||||
props->back_frame_ =
|
||||
std::make_shared<std::vector<unsigned char>>(video_frame->size);
|
||||
}
|
||||
if (props->back_frame_->size() != video_frame->size) {
|
||||
props->back_frame_->resize(video_frame->size);
|
||||
}
|
||||
|
||||
memcpy(props->dst_buffer_, video_frame->data, video_frame->size);
|
||||
bool need_to_update_render_rect = false;
|
||||
if (props->video_width_ != props->video_width_last_ ||
|
||||
props->video_height_ != props->video_height_last_) {
|
||||
need_to_update_render_rect = true;
|
||||
props->video_width_last_ = props->video_width_;
|
||||
props->video_height_last_ = props->video_height_;
|
||||
}
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data,
|
||||
video_frame->size);
|
||||
|
||||
if (need_to_update_render_rect) {
|
||||
render->UpdateRenderRect();
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
if (size_changed) {
|
||||
props->render_rect_dirty_ = true;
|
||||
}
|
||||
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
@@ -320,7 +382,28 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
std::string remote_user_id = std::string(user_id, user_id_size);
|
||||
|
||||
static FileReceiver receiver;
|
||||
receiver.SetOnSendAck([render](const FileTransferAck& ack) -> int {
|
||||
// Update output directory from config
|
||||
std::string configured_path =
|
||||
render->config_center_->GetFileTransferSavePath();
|
||||
if (!configured_path.empty()) {
|
||||
receiver.SetOutputDir(std::filesystem::u8path(configured_path));
|
||||
} else if (receiver.OutputDir().empty()) {
|
||||
receiver = FileReceiver(); // re-init with default desktop path
|
||||
}
|
||||
receiver.SetOnSendAck([render,
|
||||
remote_user_id](const FileTransferAck& ack) -> int {
|
||||
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
|
||||
if (is_server_sending) {
|
||||
auto props =
|
||||
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
|
||||
if (props) {
|
||||
PeerPtr* peer = props->peer_;
|
||||
return SendReliableDataFrame(
|
||||
peer, reinterpret_cast<const char*>(&ack),
|
||||
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return SendReliableDataFrame(
|
||||
render->peer_, reinterpret_cast<const char*>(&ack),
|
||||
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
|
||||
@@ -330,6 +413,13 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
return;
|
||||
} else if (source_id == render->clipboard_label_) {
|
||||
if (size > 0) {
|
||||
std::string remote_user_id(user_id, user_id_size);
|
||||
auto props =
|
||||
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
|
||||
if (props && !props->enable_mouse_control_) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string clipboard_text(data, size);
|
||||
if (!Clipboard::SetText(clipboard_text)) {
|
||||
LOG_ERROR("Failed to set clipboard content from remote");
|
||||
@@ -361,42 +451,100 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
}
|
||||
}
|
||||
|
||||
Render::FileTransferState* state = nullptr;
|
||||
if (!props) {
|
||||
LOG_WARN("FileTransferAck: no props found for file_id={}", ack.file_id);
|
||||
return;
|
||||
{
|
||||
std::shared_lock lock(render->file_id_to_transfer_state_mutex_);
|
||||
auto it = render->file_id_to_transfer_state_.find(ack.file_id);
|
||||
if (it != render->file_id_to_transfer_state_.end()) {
|
||||
state = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
LOG_WARN("FileTransferAck: no props/state found for file_id={}",
|
||||
ack.file_id);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
state = &props->file_transfer_;
|
||||
}
|
||||
|
||||
// Update progress based on ACK
|
||||
props->file_sent_bytes_ = ack.acked_offset;
|
||||
props->file_total_bytes_ = ack.total_size;
|
||||
state->file_sent_bytes_ = ack.acked_offset;
|
||||
state->file_total_bytes_ = ack.total_size;
|
||||
|
||||
uint32_t rate_bps = 0;
|
||||
{
|
||||
uint32_t data_channel_bitrate =
|
||||
props->net_traffic_stats_.data_outbound_stats.bitrate;
|
||||
if (props) {
|
||||
uint32_t data_channel_bitrate =
|
||||
props->net_traffic_stats_.data_outbound_stats.bitrate;
|
||||
|
||||
if (data_channel_bitrate > 0 && props->file_sending_.load()) {
|
||||
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
|
||||
if (data_channel_bitrate > 0 && state->file_sending_.load()) {
|
||||
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
|
||||
|
||||
uint32_t current_rate = props->file_send_rate_bps_.load();
|
||||
if (current_rate > 0) {
|
||||
// 70% old + 30% new for smoother display
|
||||
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
|
||||
uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
if (current_rate > 0) {
|
||||
// 70% old + 30% new for smoother display
|
||||
rate_bps =
|
||||
static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
|
||||
}
|
||||
} else {
|
||||
rate_bps = state->file_send_rate_bps_.load();
|
||||
}
|
||||
} else {
|
||||
rate_bps = props->file_send_rate_bps_.load();
|
||||
// Global transfer: no per-connection bitrate available.
|
||||
// Estimate send rate from ACKed bytes delta over time.
|
||||
const uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
uint32_t estimated_rate_bps = 0;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
uint64_t last_bytes = 0;
|
||||
std::chrono::steady_clock::time_point last_time;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
|
||||
last_bytes = state->file_send_last_bytes_;
|
||||
last_time = state->file_send_last_update_time_;
|
||||
}
|
||||
|
||||
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
|
||||
const uint64_t delta_bytes = ack.acked_offset - last_bytes;
|
||||
const double delta_seconds =
|
||||
std::chrono::duration<double>(now - last_time).count();
|
||||
|
||||
if (delta_seconds > 0.0 && delta_bytes > 0) {
|
||||
const double bps =
|
||||
(static_cast<double>(delta_bytes) * 8.0) / delta_seconds;
|
||||
if (bps > 0.0) {
|
||||
const double capped =
|
||||
(std::min)(bps, static_cast<double>(
|
||||
(std::numeric_limits<uint32_t>::max)()));
|
||||
estimated_rate_bps = static_cast<uint32_t>(capped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (estimated_rate_bps > 0 && current_rate > 0) {
|
||||
// 70% old + 30% new for smoother display
|
||||
rate_bps = static_cast<uint32_t>(current_rate * 0.7 +
|
||||
estimated_rate_bps * 0.3);
|
||||
} else if (estimated_rate_bps > 0) {
|
||||
rate_bps = estimated_rate_bps;
|
||||
} else {
|
||||
rate_bps = current_rate;
|
||||
}
|
||||
}
|
||||
|
||||
props->file_send_rate_bps_ = rate_bps;
|
||||
props->file_send_last_bytes_ = ack.acked_offset;
|
||||
state->file_send_rate_bps_ = rate_bps;
|
||||
state->file_send_last_bytes_ = ack.acked_offset;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
props->file_send_last_update_time_ = now;
|
||||
state->file_send_last_update_time_ = now;
|
||||
}
|
||||
|
||||
// Update file transfer list: update progress and rate
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
|
||||
for (auto& info : props->file_transfer_list_) {
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto& info : state->file_transfer_list_) {
|
||||
if (info.file_id == ack.file_id) {
|
||||
info.sent_bytes = ack.acked_offset;
|
||||
info.file_size = ack.total_size;
|
||||
@@ -410,8 +558,8 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
if ((ack.flags & 0x01) != 0) {
|
||||
// Transfer completed - receiver has finished receiving the file
|
||||
// Reopen window if it was closed by user
|
||||
props->file_transfer_window_visible_ = true;
|
||||
props->file_sending_ = false; // Mark sending as finished
|
||||
state->file_transfer_window_visible_ = true;
|
||||
state->file_sending_ = false; // Mark sending as finished
|
||||
LOG_INFO(
|
||||
"File transfer completed via ACK, file_id={}, total_size={}, "
|
||||
"acked_offset={}",
|
||||
@@ -419,11 +567,11 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
|
||||
// Update file transfer list: mark as completed
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
|
||||
for (auto& info : props->file_transfer_list_) {
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto& info : state->file_transfer_list_) {
|
||||
if (info.file_id == ack.file_id) {
|
||||
info.status =
|
||||
SubStreamWindowProperties::FileTransferStatus::Completed;
|
||||
Render::FileTransferState::FileTransferStatus::Completed;
|
||||
info.sent_bytes = ack.total_size;
|
||||
break;
|
||||
}
|
||||
@@ -432,9 +580,15 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
|
||||
// Unregister file_id mapping after completion
|
||||
{
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render->file_id_to_props_mutex_);
|
||||
render->file_id_to_props_.erase(ack.file_id);
|
||||
if (props) {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render->file_id_to_props_mutex_);
|
||||
render->file_id_to_props_.erase(ack.file_id);
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render->file_id_to_transfer_state_mutex_);
|
||||
render->file_id_to_transfer_state_.erase(ack.file_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Process next file in queue
|
||||
@@ -456,24 +610,32 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(render->client_properties_mutex_);
|
||||
if (render->client_properties_.find(remote_id) !=
|
||||
render->client_properties_.end()) {
|
||||
// local
|
||||
auto props = render->client_properties_.find(remote_id)->second;
|
||||
if (remote_action.type == ControlType::host_infomation &&
|
||||
props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
|
||||
if (remote_action.type == ControlType::host_infomation) {
|
||||
if (render->client_properties_.find(remote_id) !=
|
||||
render->client_properties_.end()) {
|
||||
// client mode
|
||||
auto props = render->client_properties_.find(remote_id)->second;
|
||||
if (props && props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
|
||||
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
props->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]));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
props->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 {
|
||||
// server mode
|
||||
render->connection_host_names_[remote_id] = std::string(
|
||||
remote_action.i.host_name, remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]",
|
||||
render->connection_host_names_[remote_id]);
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
FreeRemoteAction(remote_action);
|
||||
} else {
|
||||
// remote
|
||||
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
|
||||
@@ -511,6 +673,7 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
render->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalConnected == status) {
|
||||
render->signal_connected_ = true;
|
||||
render->need_to_send_recent_connections_ = true;
|
||||
LOG_INFO("[{}] connected to signal server", client_id);
|
||||
} else if (SignalStatus::SignalFailed == status) {
|
||||
render->signal_connected_ = false;
|
||||
@@ -568,6 +731,49 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = render->display_info_list_.size();
|
||||
remote_action.i.display_list =
|
||||
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||
remote_action.i.left =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
render->display_info_list_[i].name);
|
||||
remote_action.i.display_list[i] =
|
||||
(char*)malloc(render->display_info_list_[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
render->display_info_list_[i].name.c_str(),
|
||||
render->display_info_list_[i].name.length());
|
||||
remote_action.i
|
||||
.display_list[i][render->display_info_list_[i].name.length()] =
|
||||
'\0';
|
||||
remote_action.i.left[i] = render->display_info_list_[i].left;
|
||||
remote_action.i.top[i] = render->display_info_list_[i].top;
|
||||
remote_action.i.right[i] = render->display_info_list_[i].right;
|
||||
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||
host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
||||
render->control_data_label_.c_str());
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
|
||||
if (!render->need_to_create_stream_window_ &&
|
||||
!render->client_properties_.empty()) {
|
||||
render->need_to_create_stream_window_ = true;
|
||||
@@ -577,19 +783,32 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
0, (int)render->title_bar_height_,
|
||||
(int)render->stream_window_width_,
|
||||
(int)(render->stream_window_height_ - render->title_bar_height_)};
|
||||
render->start_keyboard_capturer_ = true;
|
||||
break;
|
||||
}
|
||||
case ConnectionStatus::Disconnected:
|
||||
case ConnectionStatus::Failed:
|
||||
case ConnectionStatus::Closed: {
|
||||
props->connection_established_ = false;
|
||||
props->mouse_control_button_pressed_ = false;
|
||||
if (props->dst_buffer_ && props->stream_texture_) {
|
||||
memset(props->dst_buffer_, 0, props->dst_buffer_capacity_);
|
||||
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_,
|
||||
props->texture_width_);
|
||||
props->enable_mouse_control_ = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = true;
|
||||
}
|
||||
render->CleanSubStreamWindowProperties(props);
|
||||
|
||||
SDL_Event event;
|
||||
event.type = render->STREAM_REFRESH_EVENT;
|
||||
event.user.data1 = props.get();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
render->focus_on_stream_window_ = false;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -622,15 +841,55 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
render->need_to_send_host_info_ = true;
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = render->display_info_list_.size();
|
||||
remote_action.i.display_list =
|
||||
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||
remote_action.i.left =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
render->display_info_list_[i].name);
|
||||
remote_action.i.display_list[i] =
|
||||
(char*)malloc(render->display_info_list_[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
render->display_info_list_[i].name.c_str(),
|
||||
render->display_info_list_[i].name.length());
|
||||
remote_action.i
|
||||
.display_list[i][render->display_info_list_[i].name.length()] =
|
||||
'\0';
|
||||
remote_action.i.left[i] = render->display_info_list_[i].left;
|
||||
remote_action.i.top[i] = render->display_info_list_[i].top;
|
||||
remote_action.i.right[i] = render->display_info_list_[i].right;
|
||||
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||
host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(render->peer_, msg.data(), msg.size(),
|
||||
render->control_data_label_.c_str());
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
|
||||
render->need_to_create_server_window_ = true;
|
||||
render->is_server_mode_ = true;
|
||||
render->start_screen_capturer_ = true;
|
||||
render->start_speaker_capturer_ = true;
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
#else
|
||||
render->remote_client_id_ = remote_id;
|
||||
render->start_mouse_controller_ = true;
|
||||
#endif
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
return kv.first.find("web") != std::string::npos;
|
||||
@@ -640,6 +899,8 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
break;
|
||||
}
|
||||
case ConnectionStatus::Disconnected:
|
||||
case ConnectionStatus::Failed:
|
||||
case ConnectionStatus::Closed: {
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
@@ -647,11 +908,13 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
kv.second == ConnectionStatus::Failed ||
|
||||
kv.second == ConnectionStatus::Disconnected;
|
||||
})) {
|
||||
render->need_to_destroy_server_window_ = true;
|
||||
render->is_server_mode_ = false;
|
||||
render->start_screen_capturer_ = false;
|
||||
render->start_speaker_capturer_ = false;
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
render->need_to_send_host_info_ = false;
|
||||
render->remote_client_id_ = "";
|
||||
if (props) props->connection_established_ = false;
|
||||
if (render->audio_capture_) {
|
||||
render->StopSpeakerCapturer();
|
||||
@@ -659,6 +922,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
}
|
||||
|
||||
render->connection_status_.erase(remote_id);
|
||||
render->connection_host_names_.erase(remote_id);
|
||||
if (render->screen_capturer_) {
|
||||
render->screen_capturer_->ResetToInitialMonitor();
|
||||
}
|
||||
}
|
||||
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
@@ -676,11 +943,11 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
}
|
||||
}
|
||||
|
||||
void Render::NetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data) {
|
||||
void Render::OnNetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data) {
|
||||
Render* render = (Render*)user_data;
|
||||
if (!render) {
|
||||
return;
|
||||
|
||||
@@ -15,18 +15,6 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::string OpenFileDialog(std::string title) {
|
||||
const char* path = tinyfd_openFileDialog(title.c_str(),
|
||||
"", // default path
|
||||
0, // number of filters
|
||||
nullptr, // filters
|
||||
nullptr, // filter description
|
||||
0 // no multiple selection
|
||||
);
|
||||
|
||||
return path ? path : "";
|
||||
}
|
||||
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
@@ -53,16 +41,107 @@ int LossRateDisplay(float loss_rate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Render::OpenFileDialog(std::string title) {
|
||||
const char* path = tinyfd_openFileDialog(title.c_str(),
|
||||
"", // default path
|
||||
0, // number of filters
|
||||
nullptr, // filters
|
||||
nullptr, // filter description
|
||||
0 // no multiple selection
|
||||
);
|
||||
|
||||
return path ? path : "";
|
||||
}
|
||||
|
||||
void Render::ProcessSelectedFile(
|
||||
const std::string& path,
|
||||
const std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label, const std::string& remote_id) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState* file_transfer_state =
|
||||
props ? &props->file_transfer_ : &file_transfer_;
|
||||
|
||||
LOG_INFO("Selected file: {}", path.c_str());
|
||||
|
||||
std::filesystem::path file_path = std::filesystem::u8path(path);
|
||||
|
||||
// Get file size
|
||||
std::error_code ec;
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||
file_size = 0;
|
||||
}
|
||||
|
||||
// Add file to transfer list
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_state->file_transfer_list_mutex_);
|
||||
FileTransferState::FileTransferInfo info;
|
||||
info.file_name = file_path.filename().u8string();
|
||||
info.file_path = file_path; // Store full path for precise matching
|
||||
info.file_size = file_size;
|
||||
info.status = FileTransferState::FileTransferStatus::Queued;
|
||||
info.sent_bytes = 0;
|
||||
info.file_id = 0;
|
||||
info.rate_bps = 0;
|
||||
file_transfer_state->file_transfer_list_.push_back(info);
|
||||
}
|
||||
file_transfer_state->file_transfer_window_visible_ = true;
|
||||
|
||||
if (file_transfer_state->file_sending_.load()) {
|
||||
// Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(file_transfer_state->file_queue_mutex_);
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
queued_file.remote_id = remote_id;
|
||||
file_transfer_state->file_send_queue_.push(queued_file);
|
||||
queue_size = file_transfer_state->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO("File added to queue: {} ({} files in queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
} else {
|
||||
StartFileTransfer(props, file_path, file_label, remote_id);
|
||||
|
||||
if (file_transfer_state->file_sending_.load()) {
|
||||
} else {
|
||||
// Failed to start (race condition: another file started between
|
||||
// check and call) Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_state->file_queue_mutex_);
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
queued_file.remote_id = remote_id;
|
||||
file_transfer_state->file_send_queue_.push(queued_file);
|
||||
queue_size = file_transfer_state->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO(
|
||||
"File added to queue after race condition: {} ({} files in "
|
||||
"queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
float button_width = title_bar_height_ * 0.8f;
|
||||
float button_height = title_bar_height_ * 0.8f;
|
||||
float line_padding = title_bar_height_ * 0.12f;
|
||||
float line_thickness = title_bar_height_ * 0.07f;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
if (props->control_bar_expand_) {
|
||||
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();
|
||||
@@ -95,8 +174,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
remote_action.d = i;
|
||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
props->display_selectable_hovered_ = ImGui::IsWindowHovered();
|
||||
@@ -119,24 +198,21 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
float mouse_y = ImGui::GetCursorScreenPos().y;
|
||||
float disable_mouse_x = mouse_x + line_padding;
|
||||
float disable_mouse_y = mouse_y + line_padding;
|
||||
std::string mouse = props->mouse_control_button_pressed_
|
||||
? ICON_FA_COMPUTER_MOUSE
|
||||
: ICON_FA_COMPUTER_MOUSE;
|
||||
std::string mouse = ICON_FA_COMPUTER_MOUSE;
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(mouse.c_str(), ImVec2(button_width, button_height))) {
|
||||
if (props->connection_established_) {
|
||||
start_keyboard_capturer_ = !start_keyboard_capturer_;
|
||||
props->control_mouse_ = !props->control_mouse_;
|
||||
props->mouse_control_button_pressed_ =
|
||||
!props->mouse_control_button_pressed_;
|
||||
props->enable_mouse_control_ = !props->enable_mouse_control_;
|
||||
props->mouse_control_button_label_ =
|
||||
props->mouse_control_button_pressed_
|
||||
props->enable_mouse_control_
|
||||
? localization::release_mouse[localization_language_index_]
|
||||
: localization::control_mouse[localization_language_index_];
|
||||
}
|
||||
}
|
||||
|
||||
if (!props->mouse_control_button_pressed_) {
|
||||
if (!props->enable_mouse_control_) {
|
||||
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
|
||||
ImVec2(mouse_x + button_width - line_padding,
|
||||
mouse_y + button_height - line_padding),
|
||||
@@ -176,8 +252,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
remote_action.type = ControlType::audio_capture;
|
||||
remote_action.a = props->audio_capture_button_pressed_;
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,71 +280,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
std::string title =
|
||||
localization::select_file[localization_language_index_];
|
||||
std::string path = OpenFileDialog(title);
|
||||
if (!path.empty()) {
|
||||
LOG_INFO("Selected file: {}", path.c_str());
|
||||
|
||||
std::filesystem::path file_path = std::filesystem::path(path);
|
||||
std::string file_label = file_label_;
|
||||
|
||||
// Get file size
|
||||
std::error_code ec;
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||
file_size = 0;
|
||||
}
|
||||
|
||||
// Add file to transfer list
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
|
||||
SubStreamWindowProperties::FileTransferInfo info;
|
||||
info.file_name = file_path.filename().string();
|
||||
info.file_path = file_path; // Store full path for precise matching
|
||||
info.file_size = file_size;
|
||||
info.status = SubStreamWindowProperties::FileTransferStatus::Queued;
|
||||
info.sent_bytes = 0;
|
||||
info.file_id = 0;
|
||||
info.rate_bps = 0;
|
||||
props->file_transfer_list_.push_back(info);
|
||||
}
|
||||
props->file_transfer_window_visible_ = true;
|
||||
|
||||
if (props->file_sending_.load()) {
|
||||
// Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
|
||||
SubStreamWindowProperties::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
props->file_send_queue_.push(queued_file);
|
||||
queue_size = props->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO("File added to queue: {} ({} files in queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
} else {
|
||||
StartFileTransfer(props, file_path, file_label);
|
||||
|
||||
if (props->file_sending_.load()) {
|
||||
} else {
|
||||
// Failed to start (race condition: another file started between
|
||||
// check and call) Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
|
||||
SubStreamWindowProperties::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
props->file_send_queue_.push(queued_file);
|
||||
queue_size = props->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO(
|
||||
"File added to queue after race condition: {} ({} files in "
|
||||
"queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessSelectedFile(path, props, file_label_);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -344,13 +356,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);
|
||||
|
||||
@@ -382,9 +392,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,
|
||||
@@ -445,12 +453,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);
|
||||
|
||||
@@ -9,25 +9,27 @@ 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_Borders,
|
||||
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));
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
|
||||
ImColor(1.0f, 1.0f, 1.0f), 100);
|
||||
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f,
|
||||
ImColor(signal_connected_ ? 0.0f : 1.0f,
|
||||
signal_connected_ ? 1.0f : 0.0f, 0.0f),
|
||||
100);
|
||||
draw_list->AddCircle(dot_pos, status_bar_height * 0.25f,
|
||||
ImColor(1.0f, 1.0f, 1.0f), 100);
|
||||
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
draw_list->AddText(
|
||||
@@ -40,8 +42,7 @@ int Render::StatusBar() {
|
||||
.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::EndChild();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "rounded_corner_button.h"
|
||||
|
||||
#define NEW_VERSION_ICON_RENDER_TIME_INTERVAL 2000
|
||||
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
|
||||
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -15,14 +17,28 @@ int Render::TitleBar(bool main_window) {
|
||||
float title_bar_button_width = title_bar_button_width_;
|
||||
float title_bar_button_height = title_bar_button_height_;
|
||||
if (main_window) {
|
||||
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_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
|
||||
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
|
||||
title_bar_height_ = title_bar_height;
|
||||
title_bar_button_width_ = title_bar_button_width;
|
||||
title_bar_button_height_ = title_bar_button_height;
|
||||
// When the main window is minimized, Dear ImGui may report DisplaySize as
|
||||
// (0, 0). Do not overwrite shared title-bar metrics with zeros, otherwise
|
||||
// stream/server windows (which reuse these metrics) will lose their title
|
||||
// bars and appear collapsed.
|
||||
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;
|
||||
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
|
||||
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
|
||||
|
||||
title_bar_height_ = title_bar_height;
|
||||
title_bar_button_width_ = title_bar_button_width;
|
||||
title_bar_button_height_ = title_bar_button_height;
|
||||
} else {
|
||||
// Keep using last known good values.
|
||||
title_bar_width = title_bar_width_;
|
||||
title_bar_height = title_bar_height_;
|
||||
title_bar_height_padding = title_bar_height_;
|
||||
title_bar_button_width = title_bar_button_width_;
|
||||
title_bar_button_height = title_bar_button_height_;
|
||||
}
|
||||
} else {
|
||||
title_bar_width = io.DisplaySize.x;
|
||||
title_bar_height = title_bar_button_height_;
|
||||
@@ -30,12 +46,12 @@ int Render::TitleBar(bool main_window) {
|
||||
title_bar_button_height = title_bar_button_height_;
|
||||
}
|
||||
|
||||
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",
|
||||
ImVec2(title_bar_width, title_bar_height_padding),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -92,13 +108,11 @@ int Render::TitleBar(bool main_window) {
|
||||
|
||||
std::string about_str = localization::about[localization_language_index_];
|
||||
if (update_available_) {
|
||||
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const double now_time = ImGui::GetTime();
|
||||
|
||||
// every 2 seconds
|
||||
if (now_time - new_version_icon_last_trigger_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
|
||||
kNewVersionIconBlinkIntervalSec) {
|
||||
show_new_version_icon_ = true;
|
||||
new_version_icon_render_start_time_ = now_time;
|
||||
new_version_icon_last_trigger_time_ = now_time;
|
||||
@@ -106,9 +120,9 @@ int Render::TitleBar(bool main_window) {
|
||||
|
||||
// render for 1 second
|
||||
if (show_new_version_icon_) {
|
||||
about_str = about_str + " " + ICON_FA_TRIANGLE_EXCLAMATION;
|
||||
about_str = about_str + " " + ICON_FA_CIRCLE_ARROW_UP;
|
||||
if (now_time - new_version_icon_render_start_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
|
||||
kNewVersionIconBlinkOnTimeSec) {
|
||||
show_new_version_icon_ = false;
|
||||
}
|
||||
} else {
|
||||
@@ -137,13 +151,11 @@ int Render::TitleBar(bool main_window) {
|
||||
}
|
||||
|
||||
if (update_available_ && show_new_version_icon_in_menu_) {
|
||||
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const double now_time = ImGui::GetTime();
|
||||
|
||||
// every 2 seconds
|
||||
if (now_time - new_version_icon_last_trigger_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
|
||||
kNewVersionIconBlinkIntervalSec) {
|
||||
show_new_version_icon_ = true;
|
||||
new_version_icon_render_start_time_ = now_time;
|
||||
new_version_icon_last_trigger_time_ = now_time;
|
||||
@@ -152,14 +164,13 @@ int Render::TitleBar(bool main_window) {
|
||||
// render for 1 second
|
||||
if (show_new_version_icon_) {
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(bar_pos_x + title_bar_button_width * 0.15f,
|
||||
bar_pos_y - title_bar_button_width * 0.325f));
|
||||
ImGui::Text(ICON_FA_TRIANGLE_EXCLAMATION);
|
||||
ImGui::SetCursorPos(ImVec2(bar_pos_x + title_bar_button_width * 0.21f,
|
||||
bar_pos_y - title_bar_button_width * 0.24f));
|
||||
ImGui::Text(ICON_FA_CIRCLE_ARROW_UP);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
if (now_time - new_version_icon_render_start_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
|
||||
kNewVersionIconBlinkOnTimeSec) {
|
||||
show_new_version_icon_ = false;
|
||||
}
|
||||
}
|
||||
@@ -187,6 +198,11 @@ int Render::TitleBar(bool main_window) {
|
||||
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
|
||||
if (ImGui::Button(window_minimize_button.c_str(),
|
||||
ImVec2(title_bar_button_width, title_bar_button_height))) {
|
||||
if (main_window) {
|
||||
last_main_minimize_request_tick_ = SDL_GetTicks();
|
||||
} else {
|
||||
last_stream_minimize_request_tick_ = SDL_GetTicks();
|
||||
}
|
||||
SDL_MinimizeWindow(main_window ? main_window_ : stream_window_);
|
||||
}
|
||||
draw_list->AddLine(
|
||||
@@ -270,8 +286,20 @@ int Render::TitleBar(bool main_window) {
|
||||
float xmark_pos_y = title_bar_button_height * 0.5f;
|
||||
float xmark_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();
|
||||
@@ -284,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,
|
||||
@@ -300,4 +329,4 @@ int Render::TitleBar(bool main_window) {
|
||||
ImGui::EndChild();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -89,7 +89,7 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
GetCursorPos(&pt);
|
||||
HMENU menu = CreatePopupMenu();
|
||||
AppendMenuW(menu, MF_STRING, 1001,
|
||||
localization::exit_program[language_index_]);
|
||||
localization::GetExitProgramLabel(language_index_));
|
||||
|
||||
SetForegroundWindow(hwnd_message_only_);
|
||||
int cmd =
|
||||
@@ -112,4 +112,4 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -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"
|
||||
@@ -8,11 +12,44 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool Render::OpenUrl(const std::string& url) {
|
||||
#if defined(_WIN32)
|
||||
int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
|
||||
if (wide_len <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring wide_url(static_cast<size_t>(wide_len), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, &wide_url[0], wide_len);
|
||||
if (!wide_url.empty() && wide_url.back() == L'\0') {
|
||||
wide_url.pop_back();
|
||||
}
|
||||
|
||||
std::wstring cmd = L"cmd.exe /c start \"\" \"" + wide_url + L"\"";
|
||||
STARTUPINFOW startup_info = {sizeof(startup_info)};
|
||||
PROCESS_INFORMATION process_info = {};
|
||||
if (!CreateProcessW(nullptr, &cmd[0], nullptr, nullptr, FALSE,
|
||||
CREATE_NO_WINDOW, nullptr, nullptr, &startup_info,
|
||||
&process_info)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseHandle(process_info.hThread);
|
||||
CloseHandle(process_info.hProcess);
|
||||
return true;
|
||||
#elif defined(__APPLE__)
|
||||
std::string cmd = "open " + url;
|
||||
return system(cmd.c_str()) == 0;
|
||||
#else
|
||||
std::string cmd = "xdg-open " + url;
|
||||
return system(cmd.c_str()) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Render::Hyperlink(const std::string& label, const std::string& url,
|
||||
const float window_width) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
|
||||
ImGui::SetCursorPosX(window_width * 0.1f);
|
||||
ImGui::Text("%s", label.c_str());
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
@@ -23,14 +60,7 @@ void Render::Hyperlink(const std::string& label, const std::string& url,
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
#if defined(_WIN32)
|
||||
std::string cmd = "start " + url;
|
||||
#elif defined(__APPLE__)
|
||||
std::string cmd = "open " + url;
|
||||
#else
|
||||
std::string cmd = "xdg-open " + url;
|
||||
#endif
|
||||
system(cmd.c_str()); // open browser
|
||||
OpenUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +70,7 @@ int Render::AboutWindow() {
|
||||
float about_window_width = title_bar_button_width_ * 7.5f;
|
||||
float about_window_height = latest_version_.empty()
|
||||
? title_bar_button_width_ * 4.0f
|
||||
: title_bar_button_width_ * 4.6f;
|
||||
: title_bar_button_width_ * 4.9f;
|
||||
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(
|
||||
@@ -52,8 +82,8 @@ 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_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::Begin(
|
||||
localization::about[localization_language_index_].c_str(), nullptr,
|
||||
@@ -74,16 +104,23 @@ int Render::AboutWindow() {
|
||||
ImGui::SetCursorPosX(about_window_width * 0.1f);
|
||||
ImGui::Text("%s", text.c_str());
|
||||
|
||||
if (update_available_) {
|
||||
std::string latest_version =
|
||||
if (0) {
|
||||
std::string new_version_available =
|
||||
localization::new_version_available[localization_language_index_] +
|
||||
": " + latest_version_;
|
||||
": ";
|
||||
ImGui::SetCursorPosX(about_window_width * 0.1f);
|
||||
ImGui::Text("%s", new_version_available.c_str());
|
||||
std::string access_website =
|
||||
localization::access_website[localization_language_index_];
|
||||
Hyperlink(latest_version, "https://crossdesk.cn", about_window_width);
|
||||
}
|
||||
ImGui::SetCursorPosX((about_window_width -
|
||||
ImGui::CalcTextSize(latest_version_.c_str()).x) /
|
||||
2.0f);
|
||||
Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width);
|
||||
|
||||
ImGui::Text("");
|
||||
ImGui::Spacing();
|
||||
} else {
|
||||
ImGui::Text("%s", "");
|
||||
}
|
||||
|
||||
std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved.";
|
||||
std::string license_text = "Licensed under GNU LGPL v3.";
|
||||
@@ -93,7 +130,7 @@ int Render::AboutWindow() {
|
||||
ImGui::Text("%s", license_text.c_str());
|
||||
|
||||
ImGui::SetCursorPosX(about_window_width * 0.445f);
|
||||
ImGui::SetCursorPosY(about_window_height * 0.75f);
|
||||
ImGui::SetCursorPosY(about_window_height * 0.8f);
|
||||
// OK
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str())) {
|
||||
show_about_window_ = false;
|
||||
|
||||
@@ -15,10 +15,10 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
|
||||
ImGui::Begin("ConnectionStatusWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
@@ -36,6 +36,18 @@ bool Render::ConnectionStatusWindow(
|
||||
text = localization::p2p_connecting[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
// cancel
|
||||
if (ImGui::Button(
|
||||
localization::cancel[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
re_enter_remote_id_ = true;
|
||||
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
|
||||
if (props->peer_) {
|
||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||
}
|
||||
ret_flag = true;
|
||||
}
|
||||
} else if (ConnectionStatus::Connected == props->connection_status_) {
|
||||
text = localization::p2p_connected[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
|
||||
@@ -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::PushStyleVar(ImGuiStyleVar_WindowRounding, 10.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 1.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
|
||||
ImGui::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_Borders, ImGuiWindowFlags_NoDecoration);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
props->control_window_pos_ = ImGui::GetWindowPos();
|
||||
SDL_GetMouseState(&props->mouse_pos_x_, &props->mouse_pos_y_);
|
||||
@@ -192,31 +234,27 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
props->mouse_diff_control_bar_pos_y_ =
|
||||
props->mouse_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
|
||||
|
||||
|
||||
@@ -30,12 +30,14 @@ int BitrateDisplay(int bitrate) {
|
||||
|
||||
int Render::FileTransferWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
|
||||
|
||||
// Only show window if there are files in transfer list or currently
|
||||
// transferring
|
||||
std::vector<SubStreamWindowProperties::FileTransferInfo> file_list;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
|
||||
file_list = props->file_transfer_list_;
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
file_list = state->file_transfer_list_;
|
||||
}
|
||||
|
||||
// Sort file list: Sending first, then Completed, then Queued, then Failed
|
||||
@@ -66,7 +68,7 @@ int Render::FileTransferWindow(
|
||||
// It will be reopened automatically when:
|
||||
// 1. A file transfer completes (in render_callback.cpp)
|
||||
// 2. A new file starts sending from queue (in render.cpp)
|
||||
if (!props->file_transfer_window_visible_) {
|
||||
if (!state->file_transfer_window_visible_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -87,10 +89,15 @@ int Render::FileTransferWindow(
|
||||
ImVec2(file_transfer_window_width, file_transfer_window_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
|
||||
// Set Chinese font for proper display
|
||||
if (stream_windows_system_chinese_font_) {
|
||||
ImGui::PushFont(stream_windows_system_chinese_font_);
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 0.5f);
|
||||
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));
|
||||
|
||||
@@ -110,7 +117,7 @@ int Render::FileTransferWindow(
|
||||
|
||||
// Close button handling
|
||||
if (!window_opened) {
|
||||
props->file_transfer_window_visible_ = false;
|
||||
state->file_transfer_window_visible_ = false;
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
@@ -121,9 +128,9 @@ int Render::FileTransferWindow(
|
||||
} else {
|
||||
// Use a scrollable child window for the file list
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::BeginChild("FileList",
|
||||
ImVec2(0, file_transfer_window_height * 0.75f),
|
||||
ImGuiChildFlags_Border);
|
||||
ImGui::BeginChild(
|
||||
"FileList", ImVec2(0, file_transfer_window_height * 0.75f),
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
|
||||
@@ -220,6 +227,11 @@ int Render::FileTransferWindow(
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::End();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
// Pop Chinese font if it was pushed
|
||||
if (stream_windows_system_chinese_font_) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
} else {
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
@@ -230,3 +242,4 @@ int Render::FileTransferWindow(
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
|
||||
@@ -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,8 +50,8 @@ 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_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
|
||||
ImGui::Begin(localization::settings[localization_language_index_].c_str(),
|
||||
nullptr,
|
||||
@@ -59,9 +60,9 @@ int Render::SettingWindow() {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
|
||||
{
|
||||
const char* language_items[] = {
|
||||
localization::language_zh[localization_language_index_].c_str(),
|
||||
localization::language_en[localization_language_index_].c_str()};
|
||||
const auto& supported_languages = localization::GetSupportedLanguages();
|
||||
language_button_value_ =
|
||||
localization::detail::ClampLanguageIndex(language_button_value_);
|
||||
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
@@ -76,13 +77,23 @@ int Render::SettingWindow() {
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
if (ImGui::BeginCombo("##language",
|
||||
language_items[language_button_value_])) {
|
||||
if (ImGui::BeginCombo(
|
||||
"##language",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_button_value_)]
|
||||
.display_name
|
||||
.c_str())) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
for (int i = 0; i < IM_ARRAYSIZE(language_items); i++) {
|
||||
for (int i = 0; i < static_cast<int>(supported_languages.size());
|
||||
++i) {
|
||||
bool selected = (i == language_button_value_);
|
||||
if (ImGui::Selectable(language_items[i], selected))
|
||||
if (ImGui::Selectable(
|
||||
supported_languages[i].display_name.c_str(), selected))
|
||||
language_button_value_ = i;
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
@@ -330,10 +341,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 +364,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();
|
||||
}
|
||||
@@ -374,16 +448,24 @@ int Render::SettingWindow() {
|
||||
show_self_hosted_server_config_window_ = false;
|
||||
|
||||
// Language
|
||||
language_button_value_ =
|
||||
localization::detail::ClampLanguageIndex(language_button_value_);
|
||||
if (language_button_value_ == 0) {
|
||||
config_center_->SetLanguage(ConfigCenter::LANGUAGE::CHINESE);
|
||||
localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
|
||||
} else if (language_button_value_ == 1) {
|
||||
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
|
||||
} else {
|
||||
config_center_->SetLanguage(ConfigCenter::LANGUAGE::ENGLISH);
|
||||
localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
|
||||
}
|
||||
config_center_->SetLanguage(localization_language_);
|
||||
language_button_value_last_ = language_button_value_;
|
||||
localization_language_ = (ConfigCenter::LANGUAGE)language_button_value_;
|
||||
localization_language_index_ = language_button_value_;
|
||||
LOG_INFO("Set localization language: {}",
|
||||
localization_language_index_ == 0 ? "zh" : "en");
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
localization_language_index_)]
|
||||
.code
|
||||
.c_str());
|
||||
|
||||
// Video quality
|
||||
if (video_quality_button_value_ == 0) {
|
||||
@@ -469,6 +551,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 +602,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);
|
||||
@@ -527,4 +620,4 @@ int Render::SettingWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -14,15 +14,16 @@ 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),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
LocalWindow();
|
||||
|
||||
@@ -56,4 +57,4 @@ int Render::MainWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -127,8 +127,8 @@ 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_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
|
||||
ImGui::Begin(
|
||||
|
||||
@@ -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,8 +51,8 @@ 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_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
|
||||
ImGui::Begin(localization::self_hosted_server_settings
|
||||
[localization_language_index_]
|
||||
@@ -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);
|
||||
|
||||
379
src/gui/windows/server_window.cpp
Normal file
379
src/gui/windows/server_window.cpp
Normal file
@@ -0,0 +1,379 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "rounded_corner_button.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
}
|
||||
|
||||
void BitrateDisplay(uint32_t bitrate) {
|
||||
const int num_of_digits = CountDigits(static_cast<int>(bitrate));
|
||||
if (num_of_digits <= 3) {
|
||||
ImGui::Text("%u bps", bitrate);
|
||||
} else if (num_of_digits > 3 && num_of_digits <= 6) {
|
||||
ImGui::Text("%u kbps", bitrate / 1000);
|
||||
} else {
|
||||
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
|
||||
}
|
||||
}
|
||||
|
||||
std::string FormatBytes(uint64_t bytes) {
|
||||
char buf[64];
|
||||
if (bytes < 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%llu B", (unsigned long long)bytes);
|
||||
} else if (bytes < 1024ULL * 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
|
||||
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f GB",
|
||||
bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
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, window_rounding_);
|
||||
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, 0.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::BeginChild(
|
||||
"ServerTitleBar",
|
||||
ImVec2(server_window_width_, server_window_title_bar_height_),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
|
||||
float server_title_bar_button_width = server_window_title_bar_height_;
|
||||
float server_title_bar_button_height = server_window_title_bar_height_;
|
||||
|
||||
// Collapse/expand toggle button (FontAwesome icon).
|
||||
{
|
||||
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const char* icon =
|
||||
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
|
||||
std::string toggle_label = std::string(icon) + "##server_toggle";
|
||||
|
||||
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;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
SDL_GetWindowSize(server_window_, &w, &h);
|
||||
SDL_GetWindowPosition(server_window_, &x, &y);
|
||||
|
||||
if (server_window_collapsed_) {
|
||||
const int normal_h = server_window_normal_height_;
|
||||
SDL_SetWindowSize(server_window_, w, normal_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = false;
|
||||
} else {
|
||||
const int collapsed_h = (int)server_window_title_bar_height_;
|
||||
// Collapse upward: keep top edge stable.
|
||||
SDL_SetWindowSize(server_window_, w, collapsed_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
RemoteClientInfoWindow();
|
||||
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::RemoteClientInfoWindow() {
|
||||
float remote_client_info_window_width = server_window_width_ * 0.8f;
|
||||
float remote_client_info_window_height =
|
||||
(server_window_height_ - server_window_title_bar_height_) * 0.9f;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
|
||||
ImGui::BeginChild(
|
||||
"RemoteClientInfoWindow",
|
||||
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> remote_entries;
|
||||
remote_entries.reserve(connection_status_.size());
|
||||
for (const auto& kv : connection_status_) {
|
||||
const auto host_it = connection_host_names_.find(kv.first);
|
||||
const std::string display_name =
|
||||
(host_it != connection_host_names_.end() && !host_it->second.empty())
|
||||
? host_it->second
|
||||
: kv.first;
|
||||
remote_entries.emplace_back(kv.first, display_name);
|
||||
}
|
||||
|
||||
auto find_display_name_by_remote_id =
|
||||
[&remote_entries](const std::string& remote_id) -> std::string {
|
||||
for (const auto& entry : remote_entries) {
|
||||
if (entry.first == remote_id) {
|
||||
return entry.second;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
if (!selected_server_remote_id_.empty() &&
|
||||
find_display_name_by_remote_id(selected_server_remote_id_).empty()) {
|
||||
selected_server_remote_id_.clear();
|
||||
selected_server_remote_hostname_.clear();
|
||||
}
|
||||
if (selected_server_remote_id_.empty() && !remote_entries.empty()) {
|
||||
selected_server_remote_id_ = remote_entries.front().first;
|
||||
}
|
||||
if (!selected_server_remote_id_.empty()) {
|
||||
selected_server_remote_hostname_ =
|
||||
find_display_name_by_remote_id(selected_server_remote_id_);
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s",
|
||||
localization::controller[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
|
||||
const char* selected_preview = "-";
|
||||
if (!selected_server_remote_hostname_.empty()) {
|
||||
selected_preview = selected_server_remote_hostname_.c_str();
|
||||
} else if (!remote_client_id_.empty()) {
|
||||
selected_preview = remote_client_id_.c_str();
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(remote_client_info_window_width *
|
||||
(localization_language_index_ == 0 ? 0.68f : 0.62f));
|
||||
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
if (ImGui::BeginCombo("##server_remote_id", selected_preview)) {
|
||||
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
|
||||
for (int i = 0; i < static_cast<int>(remote_entries.size()); i++) {
|
||||
const bool selected =
|
||||
(remote_entries[i].first == selected_server_remote_id_);
|
||||
if (ImGui::Selectable(remote_entries[i].second.c_str(), selected)) {
|
||||
selected_server_remote_id_ = remote_entries[i].first;
|
||||
selected_server_remote_hostname_ = remote_entries[i].second;
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
|
||||
if (!selected_server_remote_id_.empty()) {
|
||||
auto it = connection_status_.find(selected_server_remote_id_);
|
||||
const ConnectionStatus status = (it == connection_status_.end())
|
||||
? ConnectionStatus::Closed
|
||||
: it->second;
|
||||
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::connection_status[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_connected[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Connecting:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_connecting[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Disconnected:
|
||||
ImGui::Text("%s",
|
||||
localization::p2p_disconnected[localization_language_index_]
|
||||
.c_str());
|
||||
break;
|
||||
case ConnectionStatus::Failed:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_failed[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Closed:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_closed[localization_language_index_].c_str());
|
||||
break;
|
||||
default:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_failed[localization_language_index_].c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text(
|
||||
"%s", localization::file_transfer[localization_language_index_].c_str());
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(
|
||||
localization::select_file[localization_language_index_].c_str())) {
|
||||
std::string title = localization::select_file[localization_language_index_];
|
||||
std::string path = OpenFileDialog(title);
|
||||
LOG_INFO("Selected file path: {}", path.c_str());
|
||||
|
||||
ProcessSelectedFile(path, nullptr, file_label_, selected_server_remote_id_);
|
||||
}
|
||||
|
||||
if (file_transfer_.file_transfer_window_visible_) {
|
||||
ImGui::SameLine();
|
||||
const bool is_sending = file_transfer_.file_sending_.load();
|
||||
|
||||
if (is_sending) {
|
||||
// Simple animation: cycle icon every 0.5s while sending.
|
||||
static const char* kFileTransferIcons[] = {ICON_FA_ANGLE_UP,
|
||||
ICON_FA_ANGLES_UP};
|
||||
const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) %
|
||||
(static_cast<int>(sizeof(kFileTransferIcons) /
|
||||
sizeof(kFileTransferIcons[0])));
|
||||
ImGui::Text("%s", kFileTransferIcons[icon_index]);
|
||||
} else {
|
||||
// Completed.
|
||||
ImGui::Text("%s", ICON_FA_CHECK);
|
||||
}
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load();
|
||||
const uint64_t total_bytes = file_transfer_.file_total_bytes_.load();
|
||||
const uint32_t rate_bps = file_transfer_.file_send_rate_bps_.load();
|
||||
|
||||
float progress = 0.0f;
|
||||
if (total_bytes > 0) {
|
||||
progress =
|
||||
static_cast<float>(sent_bytes) / static_cast<float>(total_bytes);
|
||||
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
|
||||
}
|
||||
|
||||
std::string current_file_name;
|
||||
const uint32_t current_file_id = file_transfer_.current_file_id_.load();
|
||||
if (current_file_id != 0) {
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_.file_transfer_list_mutex_);
|
||||
for (const auto& info : file_transfer_.file_transfer_list_) {
|
||||
if (info.file_id == current_file_id) {
|
||||
current_file_name = info.file_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
if (server_windows_system_chinese_font_) {
|
||||
ImGui::PushFont(server_windows_system_chinese_font_);
|
||||
}
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (!current_file_name.empty()) {
|
||||
ImGui::Text("%s", current_file_name.c_str());
|
||||
}
|
||||
if (total_bytes > 0) {
|
||||
const std::string sent_str = FormatBytes(sent_bytes);
|
||||
const std::string total_str = FormatBytes(total_bytes);
|
||||
ImGui::Text("%s / %s", sent_str.c_str(), total_str.c_str());
|
||||
}
|
||||
|
||||
const float text_height = ImGui::GetTextLineHeight();
|
||||
char overlay[32];
|
||||
std::snprintf(overlay, sizeof(overlay), "%.1f%%", progress * 100.0f);
|
||||
ImGui::ProgressBar(progress, ImVec2(180.0f, text_height), overlay);
|
||||
BitrateDisplay(rate_bps);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
if (server_windows_system_chinese_font_) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
float close_connection_button_width = server_window_width_ * 0.1f;
|
||||
float close_connection_button_height = remote_client_info_window_height;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_);
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
|
||||
close_connection_button_height))) {
|
||||
if (peer_ && !selected_server_remote_id_.empty()) {
|
||||
LeaveConnection(peer_, selected_server_remote_id_.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -31,6 +31,34 @@ void Render::DrawConnectionStatusText(
|
||||
}
|
||||
}
|
||||
|
||||
void Render::DrawReceivingScreenText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
if (!props->connection_established_ ||
|
||||
props->connection_status_ != ConnectionStatus::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool has_valid_frame = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
has_valid_frame = props->stream_texture_ != nullptr &&
|
||||
props->video_width_ > 0 && props->video_height_ > 0 &&
|
||||
props->front_frame_ && !props->front_frame_->empty();
|
||||
}
|
||||
|
||||
if (has_valid_frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string& text =
|
||||
localization::receiving_screen[localization_language_index_];
|
||||
ImVec2 size = ImGui::GetWindowSize();
|
||||
ImVec2 text_size = ImGui::CalcTextSize(text.c_str());
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2((size.x - text_size.x) * 0.5f, (size.y - text_size.y) * 0.5f));
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str());
|
||||
}
|
||||
|
||||
void Render::CloseTab(decltype(client_properties_)::iterator& it) {
|
||||
// std::unique_lock lock(client_properties_mutex_);
|
||||
if (it != client_properties_.end()) {
|
||||
@@ -117,7 +145,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,10 +168,12 @@ int Render::StreamWindow() {
|
||||
UpdateRenderRect();
|
||||
|
||||
ControlWindow(props);
|
||||
|
||||
|
||||
// Show file transfer window if needed
|
||||
FileTransferWindow(props);
|
||||
|
||||
DrawReceivingScreenText(props);
|
||||
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
|
||||
if (!props->peer_) {
|
||||
@@ -151,12 +183,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 +249,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 +270,12 @@ int Render::StreamWindow() {
|
||||
UpdateRenderRect();
|
||||
|
||||
ControlWindow(props);
|
||||
|
||||
|
||||
// Show file transfer window if needed
|
||||
FileTransferWindow(props);
|
||||
|
||||
|
||||
DrawReceivingScreenText(props);
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (!props->peer_) {
|
||||
@@ -251,12 +287,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();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include "layout.h"
|
||||
@@ -77,8 +76,8 @@ 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_FrameRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::Begin(
|
||||
localization::notification[localization_language_index_].c_str(),
|
||||
nullptr,
|
||||
@@ -104,6 +103,7 @@ int Render::UpdateNotificationWindow() {
|
||||
localization::access_website[localization_language_index_] +
|
||||
"https://crossdesk.cn";
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::SetCursorPosX(update_notification_window_width * 0.1f);
|
||||
Hyperlink(download_text, "https://crossdesk.cn",
|
||||
update_notification_window_width);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
@@ -121,7 +121,7 @@ int Render::UpdateNotificationWindow() {
|
||||
ImGui::BeginChild(
|
||||
"ScrollableContent",
|
||||
ImVec2(update_notification_window_width * 0.9f, scrollable_height),
|
||||
ImGuiChildFlags_Border, ImGuiWindowFlags_None);
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
// set text wrap position to current available width (accounts for
|
||||
// scrollbar)
|
||||
@@ -184,14 +184,7 @@ int Render::UpdateNotificationWindow() {
|
||||
localization::update[localization_language_index_].c_str())) {
|
||||
// open download page
|
||||
std::string url = "https://crossdesk.cn";
|
||||
#if defined(_WIN32)
|
||||
std::string cmd = "start " + url;
|
||||
#elif defined(__APPLE__)
|
||||
std::string cmd = "open " + url;
|
||||
#else
|
||||
std::string cmd = "xdg-open " + url;
|
||||
#endif
|
||||
system(cmd.c_str());
|
||||
OpenUrl(url);
|
||||
show_update_notification_window_ = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,4 +62,4 @@ std::shared_ptr<spdlog::logger> get_logger() {
|
||||
|
||||
return g_logger;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -26,8 +26,6 @@ class PathManager {
|
||||
|
||||
std::filesystem::path GetLogPath();
|
||||
|
||||
std::filesystem::path GetCertPath();
|
||||
|
||||
bool CreateDirectories(const std::filesystem::path& p);
|
||||
|
||||
private:
|
||||
|
||||
@@ -138,6 +138,10 @@ int ScreenCapturerX11::SwitchTo(int monitor_index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerX11::ResetToInitialMonitor() {
|
||||
monitor_index_ = initial_monitor_index_;
|
||||
return 0;
|
||||
}
|
||||
std::vector<DisplayInfo> ScreenCapturerX11::GetDisplayInfoList() {
|
||||
return display_info_list_;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ class ScreenCapturerX11 : public ScreenCapturer {
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
|
||||
@@ -62,6 +63,7 @@ class ScreenCapturerX11 : public ScreenCapturer {
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<int> monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_;
|
||||
|
||||
@@ -62,6 +62,13 @@ int ScreenCapturerSck::SwitchTo(int monitor_index) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ScreenCapturerSck::ResetToInitialMonitor() {
|
||||
if (screen_capturer_sck_impl_) {
|
||||
return screen_capturer_sck_impl_->ResetToInitialMonitor();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() {
|
||||
if (screen_capturer_sck_impl_) {
|
||||
return screen_capturer_sck_impl_->GetDisplayInfoList();
|
||||
|
||||
@@ -33,6 +33,7 @@ class ScreenCapturerSck : public ScreenCapturer {
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
int Resume(int monitor_index) override { return 0; }
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override { return display_info_list_; }
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
private:
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
@@ -113,6 +114,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
// Currently selected display, or 0 if the full desktop is selected. This capturer does not
|
||||
// support full-desktop capture, and will fall back to the first display.
|
||||
CGDirectDisplayID current_display_ = 0;
|
||||
int initial_monitor_index_ = 0;
|
||||
};
|
||||
|
||||
std::string GetDisplayName(CGDirectDisplayID display_id) {
|
||||
@@ -261,6 +263,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
display_id_name_map_[display_id] = name;
|
||||
}
|
||||
|
||||
initial_monitor_index_ = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -295,6 +298,25 @@ int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
||||
int target = initial_monitor_index_;
|
||||
if (display_info_list_.empty()) return -1;
|
||||
CGDirectDisplayID target_display = display_id_map_[target];
|
||||
if (current_display_ == target_display) return 0;
|
||||
if (stream_) {
|
||||
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
stream_ = nil;
|
||||
current_display_ = target_display;
|
||||
StartOrReconfigureCapturer();
|
||||
}];
|
||||
} else {
|
||||
current_display_ = target_display;
|
||||
StartOrReconfigureCapturer();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Destroy() {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (stream_) {
|
||||
|
||||
@@ -31,6 +31,7 @@ class ScreenCapturer {
|
||||
|
||||
virtual std::vector<DisplayInfo> GetDisplayInfoList() = 0;
|
||||
virtual int SwitchTo(int monitor_index) = 0;
|
||||
virtual int ResetToInitialMonitor() = 0;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -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
|
||||
|
||||
356
src/screen_capturer/windows/screen_capturer_dxgi.cpp
Normal file
356
src/screen_capturer/windows/screen_capturer_dxgi.cpp
Normal file
@@ -0,0 +1,356 @@
|
||||
#include "screen_capturer_dxgi.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
std::string WideToUtf8(const std::wstring& wstr) {
|
||||
if (wstr.empty()) return {};
|
||||
int size_needed = WideCharToMultiByte(
|
||||
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
|
||||
std::string result(size_needed, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
|
||||
size_needed, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string CleanDisplayName(const std::wstring& wide_name) {
|
||||
std::string name = WideToUtf8(wide_name);
|
||||
name.erase(std::remove_if(name.begin(), name.end(),
|
||||
[](unsigned char c) { return !std::isalnum(c); }),
|
||||
name.end());
|
||||
return name;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ScreenCapturerDxgi::ScreenCapturerDxgi() {}
|
||||
ScreenCapturerDxgi::~ScreenCapturerDxgi() {
|
||||
Stop();
|
||||
Destroy();
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
callback_ = cb;
|
||||
if (!callback_) {
|
||||
LOG_ERROR("DXGI: callback is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!InitializeDxgi()) {
|
||||
LOG_ERROR("DXGI: initialize DXGI failed");
|
||||
return -2;
|
||||
}
|
||||
|
||||
EnumerateDisplays();
|
||||
if (display_info_list_.empty()) {
|
||||
LOG_ERROR("DXGI: no displays found");
|
||||
return -3;
|
||||
}
|
||||
|
||||
monitor_index_ = 0;
|
||||
initial_monitor_index_ = monitor_index_;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::Destroy() {
|
||||
Stop();
|
||||
ReleaseDuplication();
|
||||
outputs_.clear();
|
||||
d3d_context_.Reset();
|
||||
d3d_device_.Reset();
|
||||
dxgi_factory_.Reset();
|
||||
if (nv12_frame_) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = nullptr;
|
||||
nv12_width_ = 0;
|
||||
nv12_height_ = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::Start(bool show_cursor) {
|
||||
if (running_) return 0;
|
||||
show_cursor_ = show_cursor;
|
||||
|
||||
if (!CreateDuplicationForMonitor(monitor_index_)) {
|
||||
LOG_ERROR("DXGI: create duplication failed for monitor {}",
|
||||
monitor_index_.load());
|
||||
return -1;
|
||||
}
|
||||
|
||||
paused_ = false;
|
||||
running_ = true;
|
||||
thread_ = std::thread([this]() { CaptureLoop(); });
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::Stop() {
|
||||
if (!running_) return 0;
|
||||
running_ = false;
|
||||
if (thread_.joinable()) thread_.join();
|
||||
ReleaseDuplication();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::Pause(int monitor_index) {
|
||||
paused_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::Resume(int monitor_index) {
|
||||
paused_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
||||
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
|
||||
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
|
||||
return -1;
|
||||
}
|
||||
paused_ = true;
|
||||
monitor_index_ = monitor_index;
|
||||
ReleaseDuplication();
|
||||
if (!CreateDuplicationForMonitor(monitor_index_)) {
|
||||
LOG_ERROR("DXGI: create duplication failed for monitor {}",
|
||||
monitor_index_.load());
|
||||
return -2;
|
||||
}
|
||||
paused_ = false;
|
||||
LOG_INFO("DXGI: switched to monitor {}:{}", monitor_index_.load(),
|
||||
display_info_list_[monitor_index_].name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::ResetToInitialMonitor() {
|
||||
if (display_info_list_.empty()) return -1;
|
||||
int target = initial_monitor_index_;
|
||||
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
|
||||
if (monitor_index_ == target) return 0;
|
||||
if (running_) {
|
||||
paused_ = true;
|
||||
monitor_index_ = target;
|
||||
ReleaseDuplication();
|
||||
if (!CreateDuplicationForMonitor(monitor_index_)) {
|
||||
paused_ = false;
|
||||
return -2;
|
||||
}
|
||||
paused_ = false;
|
||||
LOG_INFO("DXGI: reset to initial monitor {}:{}", monitor_index_.load(),
|
||||
display_info_list_[monitor_index_].name);
|
||||
} else {
|
||||
monitor_index_ = target;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ScreenCapturerDxgi::InitializeDxgi() {
|
||||
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
|
||||
#ifdef _DEBUG
|
||||
flags |= D3D11_CREATE_DEVICE_DEBUG;
|
||||
#endif
|
||||
|
||||
D3D_FEATURE_LEVEL feature_levels[] = {
|
||||
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1,
|
||||
D3D_FEATURE_LEVEL_10_0};
|
||||
|
||||
D3D_FEATURE_LEVEL out_level{};
|
||||
HRESULT hr = D3D11CreateDevice(
|
||||
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, feature_levels,
|
||||
ARRAYSIZE(feature_levels), D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
|
||||
&out_level, d3d_context_.GetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, flags,
|
||||
feature_levels, ARRAYSIZE(feature_levels),
|
||||
D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
|
||||
&out_level, d3d_context_.GetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: D3D11CreateDevice failed, hr={}", (int)hr);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
hr = CreateDXGIFactory1(
|
||||
__uuidof(IDXGIFactory1),
|
||||
reinterpret_cast<void**>(dxgi_factory_.GetAddressOf()));
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)hr);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::EnumerateDisplays() {
|
||||
display_info_list_.clear();
|
||||
outputs_.clear();
|
||||
|
||||
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
|
||||
for (UINT a = 0;
|
||||
dxgi_factory_->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) !=
|
||||
DXGI_ERROR_NOT_FOUND;
|
||||
++a) {
|
||||
Microsoft::WRL::ComPtr<IDXGIOutput> output;
|
||||
for (UINT o = 0; adapter->EnumOutputs(o, output.ReleaseAndGetAddressOf()) !=
|
||||
DXGI_ERROR_NOT_FOUND;
|
||||
++o) {
|
||||
DXGI_OUTPUT_DESC desc{};
|
||||
if (FAILED(output->GetDesc(&desc))) {
|
||||
continue;
|
||||
}
|
||||
std::string name = CleanDisplayName(desc.DeviceName);
|
||||
MONITORINFOEX mi{};
|
||||
mi.cbSize = sizeof(MONITORINFOEX);
|
||||
if (GetMonitorInfo(desc.Monitor, &mi)) {
|
||||
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
|
||||
DisplayInfo info((void*)desc.Monitor, name, is_primary,
|
||||
mi.rcMonitor.left, mi.rcMonitor.top,
|
||||
mi.rcMonitor.right, mi.rcMonitor.bottom);
|
||||
// primary first
|
||||
if (is_primary)
|
||||
display_info_list_.insert(display_info_list_.begin(), info);
|
||||
else
|
||||
display_info_list_.push_back(info);
|
||||
outputs_.push_back(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
||||
if (monitor_index < 0 || monitor_index >= (int)outputs_.size()) return false;
|
||||
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
|
||||
HRESULT hr = outputs_[monitor_index]->QueryInterface(
|
||||
IID_PPV_ARGS(output1.GetAddressOf()));
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: Query IDXGIOutput1 failed, hr={}", (int)hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
duplication_.Reset();
|
||||
hr = output1->DuplicateOutput(d3d_device_.Get(), duplication_.GetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: DuplicateOutput failed, hr={}", (int)hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
staging_.Reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
staging_.Reset();
|
||||
if (duplication_) {
|
||||
duplication_->ReleaseFrame();
|
||||
}
|
||||
duplication_.Reset();
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::CaptureLoop() {
|
||||
const int timeout_ms = 33;
|
||||
while (running_) {
|
||||
if (paused_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!duplication_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
DXGI_OUTDUPL_FRAME_INFO frame_info{};
|
||||
Microsoft::WRL::ComPtr<IDXGIResource> desktop_resource;
|
||||
HRESULT hr = duplication_->AcquireNextFrame(
|
||||
timeout_ms, &frame_info, desktop_resource.GetAddressOf());
|
||||
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
|
||||
continue;
|
||||
}
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
||||
// attempt to recreate duplication
|
||||
ReleaseDuplication();
|
||||
CreateDuplicationForMonitor(monitor_index_);
|
||||
continue;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> acquired_tex;
|
||||
if (desktop_resource) {
|
||||
hr = desktop_resource->QueryInterface(
|
||||
IID_PPV_ARGS(acquired_tex.GetAddressOf()));
|
||||
if (FAILED(hr)) {
|
||||
duplication_->ReleaseFrame();
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
duplication_->ReleaseFrame();
|
||||
continue;
|
||||
}
|
||||
|
||||
D3D11_TEXTURE2D_DESC src_desc{};
|
||||
acquired_tex->GetDesc(&src_desc);
|
||||
|
||||
if (!staging_) {
|
||||
D3D11_TEXTURE2D_DESC staging_desc = src_desc;
|
||||
staging_desc.Usage = D3D11_USAGE_STAGING;
|
||||
staging_desc.BindFlags = 0;
|
||||
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
staging_desc.MiscFlags = 0;
|
||||
hr = d3d_device_->CreateTexture2D(&staging_desc, nullptr,
|
||||
staging_.GetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: CreateTexture2D staging failed, hr={}", (int)hr);
|
||||
duplication_->ReleaseFrame();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
d3d_context_->CopyResource(staging_.Get(), acquired_tex.Get());
|
||||
|
||||
D3D11_MAPPED_SUBRESOURCE mapped{};
|
||||
hr = d3d_context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
|
||||
if (FAILED(hr)) {
|
||||
duplication_->ReleaseFrame();
|
||||
continue;
|
||||
}
|
||||
|
||||
int logical_width = static_cast<int>(src_desc.Width);
|
||||
int even_width = logical_width & ~1;
|
||||
int even_height = static_cast<int>(src_desc.Height) & ~1;
|
||||
if (even_width <= 0 || even_height <= 0) {
|
||||
d3d_context_->Unmap(staging_.Get(), 0);
|
||||
duplication_->ReleaseFrame();
|
||||
continue;
|
||||
}
|
||||
|
||||
int nv12_size = even_width * even_height * 3 / 2;
|
||||
if (!nv12_frame_ || nv12_width_ != even_width ||
|
||||
nv12_height_ != even_height) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = new unsigned char[nv12_size];
|
||||
nv12_width_ = even_width;
|
||||
nv12_height_ = even_height;
|
||||
}
|
||||
|
||||
libyuv::ARGBToNV12(static_cast<const uint8_t*>(mapped.pData),
|
||||
static_cast<int>(mapped.RowPitch), nv12_frame_,
|
||||
even_width, nv12_frame_ + even_width * even_height,
|
||||
even_width, even_width, even_height);
|
||||
|
||||
if (callback_) {
|
||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
display_info_list_[monitor_index_].name.c_str());
|
||||
}
|
||||
|
||||
d3d_context_->Unmap(staging_.Get(), 0);
|
||||
duplication_->ReleaseFrame();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
81
src/screen_capturer/windows/screen_capturer_dxgi.h
Normal file
81
src/screen_capturer/windows/screen_capturer_dxgi.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-27
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURER_DXGI_H_
|
||||
#define _SCREEN_CAPTURER_DXGI_H_
|
||||
|
||||
#include <Windows.h>
|
||||
#include <d3d11.h>
|
||||
#include <dxgi1_2.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "screen_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
public:
|
||||
ScreenCapturerDxgi();
|
||||
~ScreenCapturerDxgi();
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
|
||||
int Pause(int monitor_index) override;
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override {
|
||||
return display_info_list_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool InitializeDxgi();
|
||||
void EnumerateDisplays();
|
||||
bool CreateDuplicationForMonitor(int monitor_index);
|
||||
void CaptureLoop();
|
||||
void ReleaseDuplication();
|
||||
|
||||
private:
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::vector<Microsoft::WRL::ComPtr<IDXGIOutput>> outputs_;
|
||||
|
||||
Microsoft::WRL::ComPtr<IDXGIFactory1> dxgi_factory_;
|
||||
Microsoft::WRL::ComPtr<ID3D11Device> d3d_device_;
|
||||
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
|
||||
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
|
||||
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<int> monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
std::thread thread_;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_ = nullptr;
|
||||
|
||||
unsigned char* nv12_frame_ = nullptr;
|
||||
int nv12_width_ = 0;
|
||||
int nv12_height_ = 0;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
217
src/screen_capturer/windows/screen_capturer_gdi.cpp
Normal file
217
src/screen_capturer/windows/screen_capturer_gdi.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
#include "screen_capturer_gdi.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
std::string WideToUtf8(const std::wstring& wstr) {
|
||||
if (wstr.empty()) return {};
|
||||
int size_needed = WideCharToMultiByte(
|
||||
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
|
||||
std::string result(size_needed, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
|
||||
size_needed, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string CleanDisplayName(const std::wstring& wide_name) {
|
||||
std::string name = WideToUtf8(wide_name);
|
||||
name.erase(std::remove_if(name.begin(), name.end(),
|
||||
[](unsigned char c) { return !std::isalnum(c); }),
|
||||
name.end());
|
||||
return name;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ScreenCapturerGdi::ScreenCapturerGdi() {}
|
||||
ScreenCapturerGdi::~ScreenCapturerGdi() {
|
||||
Stop();
|
||||
Destroy();
|
||||
}
|
||||
|
||||
BOOL CALLBACK ScreenCapturerGdi::EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
|
||||
LPARAM data) {
|
||||
auto displays = reinterpret_cast<std::vector<DisplayInfo>*>(data);
|
||||
MONITORINFOEX mi{};
|
||||
mi.cbSize = sizeof(MONITORINFOEX);
|
||||
if (GetMonitorInfo(hMonitor, &mi)) {
|
||||
std::string name = CleanDisplayName(mi.szDevice);
|
||||
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
|
||||
DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left,
|
||||
mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
|
||||
if (is_primary)
|
||||
displays->insert(displays->begin(), info);
|
||||
else
|
||||
displays->push_back(info);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void ScreenCapturerGdi::EnumerateDisplays() {
|
||||
display_info_list_.clear();
|
||||
EnumDisplayMonitors(nullptr, nullptr, EnumMonitorProc,
|
||||
(LPARAM)&display_info_list_);
|
||||
}
|
||||
|
||||
int ScreenCapturerGdi::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
callback_ = cb;
|
||||
if (!callback_) {
|
||||
LOG_ERROR("GDI: callback is null");
|
||||
return -1;
|
||||
}
|
||||
EnumerateDisplays();
|
||||
if (display_info_list_.empty()) {
|
||||
LOG_ERROR("GDI: no displays found");
|
||||
return -2;
|
||||
}
|
||||
monitor_index_ = 0;
|
||||
initial_monitor_index_ = monitor_index_;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerGdi::Destroy() {
|
||||
Stop();
|
||||
if (nv12_frame_) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = nullptr;
|
||||
nv12_width_ = 0;
|
||||
nv12_height_ = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerGdi::Start(bool show_cursor) {
|
||||
if (running_) return 0;
|
||||
show_cursor_ = show_cursor;
|
||||
paused_ = false;
|
||||
running_ = true;
|
||||
thread_ = std::thread([this]() { CaptureLoop(); });
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerGdi::Stop() {
|
||||
if (!running_) return 0;
|
||||
running_ = false;
|
||||
if (thread_.joinable()) thread_.join();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerGdi::Pause(int monitor_index) {
|
||||
paused_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerGdi::Resume(int monitor_index) {
|
||||
paused_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerGdi::SwitchTo(int monitor_index) {
|
||||
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
|
||||
LOG_ERROR("GDI: invalid monitor index {}", monitor_index);
|
||||
return -1;
|
||||
}
|
||||
monitor_index_ = monitor_index;
|
||||
LOG_INFO("GDI: switched to monitor {}:{}", monitor_index_.load(),
|
||||
display_info_list_[monitor_index_].name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerGdi::ResetToInitialMonitor() {
|
||||
if (display_info_list_.empty()) return -1;
|
||||
int target = initial_monitor_index_;
|
||||
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
|
||||
monitor_index_ = target;
|
||||
LOG_INFO("GDI: reset to initial monitor {}:{}", monitor_index_.load(),
|
||||
display_info_list_[monitor_index_].name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ScreenCapturerGdi::CaptureLoop() {
|
||||
int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
|
||||
HDC screen_dc = GetDC(nullptr);
|
||||
while (running_) {
|
||||
if (paused_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
if (!screen_dc) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& di = display_info_list_[monitor_index_];
|
||||
int left = di.left;
|
||||
int top = di.top;
|
||||
int width = di.width & ~1;
|
||||
int height = di.height & ~1;
|
||||
if (width <= 0 || height <= 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
|
||||
continue;
|
||||
}
|
||||
|
||||
BITMAPINFO bmi{};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = width;
|
||||
bmi.bmiHeader.biHeight = -height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* bits = nullptr;
|
||||
HDC mem_dc = CreateCompatibleDC(screen_dc);
|
||||
HBITMAP dib =
|
||||
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
HGDIOBJ old = SelectObject(mem_dc, dib);
|
||||
|
||||
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
|
||||
SRCCOPY | CAPTUREBLT);
|
||||
|
||||
if (show_cursor_) {
|
||||
CURSORINFO ci{};
|
||||
ci.cbSize = sizeof(CURSORINFO);
|
||||
if (GetCursorInfo(&ci) && ci.flags == CURSOR_SHOWING && ci.hCursor) {
|
||||
POINT pt = ci.ptScreenPos;
|
||||
int cx = pt.x - left;
|
||||
int cy = pt.y - top;
|
||||
if (cx >= -64 && cy >= -64 && cx < width + 64 && cy < height + 64) {
|
||||
DrawIconEx(mem_dc, cx, cy, ci.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int stride_argb = width * 4;
|
||||
int nv12_size = width * height * 3 / 2;
|
||||
if (!nv12_frame_ || nv12_width_ != width || nv12_height_ != height) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = new unsigned char[nv12_size];
|
||||
nv12_width_ = width;
|
||||
nv12_height_ = height;
|
||||
}
|
||||
|
||||
libyuv::ARGBToNV12(static_cast<const uint8_t*>(bits), stride_argb,
|
||||
nv12_frame_, width, nv12_frame_ + width * height, width,
|
||||
width, height);
|
||||
|
||||
if (callback_) {
|
||||
callback_(nv12_frame_, nv12_size, width, height, di.name.c_str());
|
||||
}
|
||||
|
||||
SelectObject(mem_dc, old);
|
||||
DeleteObject(dib);
|
||||
DeleteDC(mem_dc);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
|
||||
}
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
68
src/screen_capturer/windows/screen_capturer_gdi.h
Normal file
68
src/screen_capturer/windows/screen_capturer_gdi.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-27
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURER_GDI_H_
|
||||
#define _SCREEN_CAPTURER_GDI_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "screen_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ScreenCapturerGdi : public ScreenCapturer {
|
||||
public:
|
||||
ScreenCapturerGdi();
|
||||
~ScreenCapturerGdi();
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
|
||||
int Pause(int monitor_index) override;
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override {
|
||||
return display_info_list_;
|
||||
}
|
||||
|
||||
private:
|
||||
static BOOL CALLBACK EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
|
||||
LPARAM data);
|
||||
void EnumerateDisplays();
|
||||
void CaptureLoop();
|
||||
|
||||
private:
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<int> monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
std::thread thread_;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_ = nullptr;
|
||||
|
||||
unsigned char* nv12_frame_ = nullptr;
|
||||
int nv12_width_ = 0;
|
||||
int nv12_height_ = 0;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -149,6 +147,7 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
LOG_INFO("Default on monitor {}:{}", monitor_index_,
|
||||
display_info_list_[monitor_index_].name);
|
||||
|
||||
initial_monitor_index_ = monitor_index_;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -165,6 +164,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 +175,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;
|
||||
}
|
||||
|
||||
@@ -257,6 +268,26 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::ResetToInitialMonitor() {
|
||||
if (display_info_list_.empty()) return -1;
|
||||
if (initial_monitor_index_ < 0 ||
|
||||
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
|
||||
return -1;
|
||||
}
|
||||
if (monitor_index_ == initial_monitor_index_) {
|
||||
return 0;
|
||||
}
|
||||
if (running_) {
|
||||
Pause(monitor_index_);
|
||||
}
|
||||
monitor_index_ = initial_monitor_index_;
|
||||
LOG_INFO("Reset to initial monitor {}:{}", monitor_index_,
|
||||
display_info_list_[monitor_index_].name);
|
||||
if (running_) {
|
||||
Resume(monitor_index_);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
|
||||
int id) {
|
||||
if (!running_ || !on_data_) {
|
||||
|
||||
@@ -34,6 +34,7 @@ class ScreenCapturerWgc : public ScreenCapturer,
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() { return display_info_list_; }
|
||||
|
||||
int SwitchTo(int monitor_index);
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
void OnFrame(const WgcSession::wgc_session_frame& frame, int id);
|
||||
|
||||
@@ -45,6 +46,7 @@ class ScreenCapturerWgc : public ScreenCapturer,
|
||||
MONITORINFOEX monitor_info_;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
int monitor_index_ = 0;
|
||||
int initial_monitor_index_ = 0;
|
||||
|
||||
private:
|
||||
class WgcSessionInfo {
|
||||
@@ -57,8 +59,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;
|
||||
|
||||
|
||||
300
src/screen_capturer/windows/screen_capturer_win.cpp
Normal file
300
src/screen_capturer/windows/screen_capturer_win.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
#include "screen_capturer_win.h"
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "screen_capturer_dxgi.h"
|
||||
#include "screen_capturer_gdi.h"
|
||||
#include "wgc_plugin_api.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
class WgcPluginCapturer final : public ScreenCapturer {
|
||||
public:
|
||||
using CreateFn = ScreenCapturer* (*)();
|
||||
using DestroyFn = void (*)(ScreenCapturer*);
|
||||
|
||||
static std::unique_ptr<ScreenCapturer> Create() {
|
||||
std::filesystem::path plugin_path;
|
||||
wchar_t module_path[MAX_PATH] = {0};
|
||||
const DWORD len = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
|
||||
if (len == 0 || len >= MAX_PATH) {
|
||||
return nullptr;
|
||||
}
|
||||
plugin_path =
|
||||
std::filesystem::path(module_path).parent_path() / L"wgc_plugin.dll";
|
||||
|
||||
HMODULE module = LoadLibraryW(plugin_path.c_str());
|
||||
if (!module) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto create_fn = reinterpret_cast<CreateFn>(
|
||||
GetProcAddress(module, "CrossDeskCreateWgcCapturer"));
|
||||
auto destroy_fn = reinterpret_cast<DestroyFn>(
|
||||
GetProcAddress(module, "CrossDeskDestroyWgcCapturer"));
|
||||
if (!create_fn || !destroy_fn) {
|
||||
FreeLibrary(module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ScreenCapturer* impl = create_fn();
|
||||
if (!impl) {
|
||||
FreeLibrary(module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::unique_ptr<ScreenCapturer>(
|
||||
new WgcPluginCapturer(module, impl, destroy_fn));
|
||||
}
|
||||
|
||||
~WgcPluginCapturer() override {
|
||||
if (impl_) {
|
||||
destroy_fn_(impl_);
|
||||
impl_ = nullptr;
|
||||
}
|
||||
if (module_) {
|
||||
FreeLibrary(module_);
|
||||
module_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int Init(const int fps, cb_desktop_data cb) override {
|
||||
return impl_ ? impl_->Init(fps, std::move(cb)) : -1;
|
||||
}
|
||||
int Destroy() override { return impl_ ? impl_->Destroy() : 0; }
|
||||
int Start(bool show_cursor) override {
|
||||
return impl_ ? impl_->Start(show_cursor) : -1;
|
||||
}
|
||||
int Stop() override { return impl_ ? impl_->Stop() : 0; }
|
||||
int Pause(int monitor_index) override {
|
||||
return impl_ ? impl_->Pause(monitor_index) : -1;
|
||||
}
|
||||
int Resume(int monitor_index) override {
|
||||
return impl_ ? impl_->Resume(monitor_index) : -1;
|
||||
}
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override {
|
||||
return impl_ ? impl_->GetDisplayInfoList() : std::vector<DisplayInfo>{};
|
||||
}
|
||||
int SwitchTo(int monitor_index) override {
|
||||
return impl_ ? impl_->SwitchTo(monitor_index) : -1;
|
||||
}
|
||||
int ResetToInitialMonitor() override {
|
||||
return impl_ ? impl_->ResetToInitialMonitor() : -1;
|
||||
}
|
||||
|
||||
private:
|
||||
WgcPluginCapturer(HMODULE module, ScreenCapturer* impl, DestroyFn destroy_fn)
|
||||
: module_(module), impl_(impl), destroy_fn_(destroy_fn) {}
|
||||
|
||||
HMODULE module_ = nullptr;
|
||||
ScreenCapturer* impl_ = nullptr;
|
||||
DestroyFn destroy_fn_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ScreenCapturerWin::ScreenCapturerWin() {}
|
||||
ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
|
||||
|
||||
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
cb_orig_ = cb;
|
||||
cb_ = [this](unsigned char* data, int size, int w, int h,
|
||||
const char* display_name) {
|
||||
std::string mapped_name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
auto it = label_alias_.find(display_name);
|
||||
if (it != label_alias_.end())
|
||||
mapped_name = it->second;
|
||||
else
|
||||
mapped_name = display_name;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
|
||||
};
|
||||
|
||||
int ret = -1;
|
||||
|
||||
impl_ = WgcPluginCapturer::Create();
|
||||
impl_is_wgc_plugin_ = (impl_ != nullptr);
|
||||
ret = impl_ ? impl_->Init(fps_, cb_) : -1;
|
||||
if (ret == 0) {
|
||||
LOG_INFO("Windows capturer: using WGC plugin");
|
||||
BuildCanonicalFromImpl();
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_WARN("Windows capturer: WGC plugin init failed (ret={}), try DXGI", ret);
|
||||
impl_.reset();
|
||||
impl_is_wgc_plugin_ = false;
|
||||
|
||||
impl_ = std::make_unique<ScreenCapturerDxgi>();
|
||||
impl_is_wgc_plugin_ = false;
|
||||
ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
LOG_INFO("Windows capturer: using DXGI Desktop Duplication");
|
||||
BuildCanonicalFromImpl();
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_WARN("Windows capturer: DXGI init failed (ret={}), fallback to GDI", ret);
|
||||
impl_.reset();
|
||||
|
||||
impl_ = std::make_unique<ScreenCapturerGdi>();
|
||||
impl_is_wgc_plugin_ = false;
|
||||
ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
LOG_INFO("Windows capturer: using GDI BitBlt");
|
||||
BuildCanonicalFromImpl();
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("Windows capturer: all implementations failed, ret={}", ret);
|
||||
impl_.reset();
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Destroy() {
|
||||
if (impl_) {
|
||||
impl_->Destroy();
|
||||
impl_.reset();
|
||||
impl_is_wgc_plugin_ = false;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
label_alias_.clear();
|
||||
handle_to_canonical_.clear();
|
||||
canonical_labels_.clear();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
if (!impl_) return -1;
|
||||
int ret = impl_->Start(show_cursor);
|
||||
if (ret == 0) return 0;
|
||||
|
||||
LOG_WARN("Windows capturer: Start failed (ret={}), trying fallback", ret);
|
||||
|
||||
auto try_init_start = [&](std::unique_ptr<ScreenCapturer> cand) -> bool {
|
||||
int r = cand->Init(fps_, cb_);
|
||||
if (r != 0) return false;
|
||||
int s = cand->Start(show_cursor);
|
||||
if (s == 0) {
|
||||
impl_ = std::move(cand);
|
||||
impl_is_wgc_plugin_ = false;
|
||||
RebuildAliasesFromImpl();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (impl_is_wgc_plugin_) {
|
||||
if (try_init_start(std::make_unique<ScreenCapturerDxgi>())) {
|
||||
LOG_INFO("Windows capturer: fallback to DXGI");
|
||||
return 0;
|
||||
}
|
||||
if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
|
||||
LOG_INFO("Windows capturer: fallback to GDI");
|
||||
return 0;
|
||||
}
|
||||
} else if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get())) {
|
||||
if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
|
||||
LOG_INFO("Windows capturer: fallback to GDI");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_ERROR("Windows capturer: all fallbacks failed to start");
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Stop() {
|
||||
if (!impl_) return 0;
|
||||
return impl_->Stop();
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Pause(int monitor_index) {
|
||||
if (!impl_) return -1;
|
||||
return impl_->Pause(monitor_index);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Resume(int monitor_index) {
|
||||
if (!impl_) return -1;
|
||||
return impl_->Resume(monitor_index);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::SwitchTo(int monitor_index) {
|
||||
if (!impl_) return -1;
|
||||
return impl_->SwitchTo(monitor_index);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::ResetToInitialMonitor() {
|
||||
if (!impl_) return -1;
|
||||
return impl_->ResetToInitialMonitor();
|
||||
}
|
||||
|
||||
std::vector<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() {
|
||||
if (!impl_) return {};
|
||||
return impl_->GetDisplayInfoList();
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::BuildCanonicalFromImpl() {
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
handle_to_canonical_.clear();
|
||||
label_alias_.clear();
|
||||
canonical_displays_ = impl_->GetDisplayInfoList();
|
||||
canonical_labels_.clear();
|
||||
for (const auto& di : canonical_displays_) {
|
||||
handle_to_canonical_[di.handle] = di.name;
|
||||
canonical_labels_.insert(di.name);
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::RebuildAliasesFromImpl() {
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
label_alias_.clear();
|
||||
auto current = impl_->GetDisplayInfoList();
|
||||
auto similar = [&](const DisplayInfo& a, const DisplayInfo& b) {
|
||||
int dl = std::abs(a.left - b.left);
|
||||
int dt = std::abs(a.top - b.top);
|
||||
int dw = std::abs(a.width - b.width);
|
||||
int dh = std::abs(a.height - b.height);
|
||||
return dl <= 10 && dt <= 10 && dw <= 20 && dh <= 20;
|
||||
};
|
||||
for (const auto& di : current) {
|
||||
std::string canonical;
|
||||
auto it = handle_to_canonical_.find(di.handle);
|
||||
if (it != handle_to_canonical_.end()) {
|
||||
canonical = it->second;
|
||||
} else {
|
||||
for (const auto& c : canonical_displays_) {
|
||||
if (similar(di, c) || (di.is_primary && c.is_primary)) {
|
||||
canonical = c.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!canonical.empty() && canonical != di.name) {
|
||||
label_alias_[di.name] = canonical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
56
src/screen_capturer/windows/screen_capturer_win.h
Normal file
56
src/screen_capturer/windows/screen_capturer_win.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-27
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURER_WIN_H_
|
||||
#define _SCREEN_CAPTURER_WIN_H_
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "screen_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ScreenCapturerWin : public ScreenCapturer {
|
||||
public:
|
||||
ScreenCapturerWin();
|
||||
~ScreenCapturerWin();
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
|
||||
int Pause(int monitor_index) override;
|
||||
int Resume(int monitor_index) override;
|
||||
|
||||
int SwitchTo(int monitor_index) override;
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
bool impl_is_wgc_plugin_ = false;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data cb_;
|
||||
cb_desktop_data cb_orig_;
|
||||
|
||||
std::unordered_map<void*, std::string> handle_to_canonical_;
|
||||
std::unordered_map<std::string, std::string> label_alias_;
|
||||
std::mutex alias_mutex_;
|
||||
std::vector<DisplayInfo> canonical_displays_;
|
||||
std::unordered_set<std::string> canonical_labels_;
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
void RebuildAliasesFromImpl();
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
29
src/screen_capturer/windows/wgc_plugin_api.h
Normal file
29
src/screen_capturer/windows/wgc_plugin_api.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-03-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WGC_PLUGIN_API_H_
|
||||
#define _WGC_PLUGIN_API_H_
|
||||
|
||||
#include "screen_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
class ScreenCapturer;
|
||||
}
|
||||
|
||||
#if defined(_WIN32) && defined(CROSSDESK_WGC_PLUGIN_BUILD)
|
||||
#define CROSSDESK_WGC_PLUGIN_API __declspec(dllexport)
|
||||
#else
|
||||
#define CROSSDESK_WGC_PLUGIN_API
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
CROSSDESK_WGC_PLUGIN_API crossdesk::ScreenCapturer*
|
||||
CrossDeskCreateWgcCapturer();
|
||||
CROSSDESK_WGC_PLUGIN_API void CrossDeskDestroyWgcCapturer(
|
||||
crossdesk::ScreenCapturer* capturer);
|
||||
}
|
||||
|
||||
#endif
|
||||
13
src/screen_capturer/windows/wgc_plugin_entry.cpp
Normal file
13
src/screen_capturer/windows/wgc_plugin_entry.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "screen_capturer_wgc.h"
|
||||
#include "wgc_plugin_api.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
crossdesk::ScreenCapturer* CrossDeskCreateWgcCapturer() {
|
||||
return new crossdesk::ScreenCapturerWgc();
|
||||
}
|
||||
|
||||
void CrossDeskDestroyWgcCapturer(crossdesk::ScreenCapturer* capturer) {
|
||||
delete capturer;
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
#define CHECK_INIT \
|
||||
if (!is_initialized_) { \
|
||||
std::cout << "AE_NEED_INIT" << std::endl; \
|
||||
return 4; \
|
||||
#include "rd_log.h"
|
||||
|
||||
#define CHECK_INIT \
|
||||
if (!is_initialized_) { \
|
||||
LOG_ERROR("AE_NEED_INIT"); \
|
||||
return 4; \
|
||||
}
|
||||
|
||||
#define CHECK_CLOSED \
|
||||
@@ -64,6 +65,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 +91,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 +247,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 +270,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,14 +299,31 @@ 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() {
|
||||
if (is_initialized_) return 0;
|
||||
|
||||
if (!(d3d11_direct_device_ = CreateD3D11Device())) {
|
||||
std::cout << "AE_D3D_CREATE_DEVICE_FAILED" << std::endl;
|
||||
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -313,7 +339,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 +404,4 @@ LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
|
||||
// ::CloseWindow(hwnd_);
|
||||
// ::DestroyWindow(hwnd_);
|
||||
// }
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -452,11 +452,17 @@ static void MonitorThreadFunc() {
|
||||
LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
|
||||
|
||||
XEvent event;
|
||||
constexpr int kEventPollIntervalMs = 20;
|
||||
while (g_monitoring.load()) {
|
||||
XNextEvent(g_x11_display, &event);
|
||||
if (event.type == event_base + XFixesSelectionNotify) {
|
||||
HandleClipboardChange();
|
||||
// Avoid blocking on XNextEvent so StopMonitoring() can stop quickly.
|
||||
while (g_monitoring.load() && XPending(g_x11_display) > 0) {
|
||||
XNextEvent(g_x11_display, &event);
|
||||
if (event.type == event_base + XFixesSelectionNotify) {
|
||||
HandleClipboardChange();
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(kEventPollIntervalMs));
|
||||
}
|
||||
|
||||
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Submodule submodules/minirtc updated: fc5591eec7...e76c7809c0
32
xmake.lua
32
xmake.lua
@@ -28,6 +28,7 @@ if is_mode("debug") then
|
||||
add_defines("CROSSDESK_DEBUG")
|
||||
end
|
||||
|
||||
add_requireconfs("*.python", {version = "3.12", override = true, configs = {pgo = false}})
|
||||
add_requires("spdlog 1.14.1", {system = false})
|
||||
add_requires("imgui v1.92.1-docking", {configs = {sdl3 = true, sdl3_renderer = true}})
|
||||
add_requires("openssl3 3.3.2", {system = false})
|
||||
@@ -37,9 +38,9 @@ add_requires("tinyfiledialogs 3.15.1")
|
||||
|
||||
if is_os("windows") then
|
||||
add_requires("libyuv", "miniaudio 0.11.21")
|
||||
add_links("Shell32", "windowsapp", "dwmapi", "User32", "kernel32",
|
||||
add_links("Shell32", "dwmapi", "User32", "kernel32",
|
||||
"SDL3-static", "gdi32", "winmm", "setupapi", "version",
|
||||
"Imm32", "iphlpapi")
|
||||
"Imm32", "iphlpapi", "d3d11", "dxgi")
|
||||
add_cxflags("/WX")
|
||||
set_runtimes("MT")
|
||||
elseif is_os("linux") then
|
||||
@@ -70,6 +71,9 @@ target("common")
|
||||
set_kind("object")
|
||||
add_deps("rd_log")
|
||||
add_files("src/common/*.cpp")
|
||||
if is_os("macosx") then
|
||||
add_files("src/common/*.mm")
|
||||
end
|
||||
add_includedirs("src/common", {public = true})
|
||||
|
||||
target("path_manager")
|
||||
@@ -85,7 +89,9 @@ target("screen_capturer")
|
||||
add_includedirs("src/screen_capturer", {public = true})
|
||||
if is_os("windows") then
|
||||
add_packages("libyuv")
|
||||
add_files("src/screen_capturer/windows/*.cpp")
|
||||
add_files("src/screen_capturer/windows/screen_capturer_dxgi.cpp",
|
||||
"src/screen_capturer/windows/screen_capturer_gdi.cpp",
|
||||
"src/screen_capturer/windows/screen_capturer_win.cpp")
|
||||
add_includedirs("src/screen_capturer/windows", {public = true})
|
||||
elseif is_os("macosx") then
|
||||
add_files("src/screen_capturer/macosx/*.cpp",
|
||||
@@ -196,8 +202,26 @@ target("gui")
|
||||
add_files("src/gui/windows/*.mm")
|
||||
end
|
||||
|
||||
if is_os("windows") then
|
||||
target("wgc_plugin")
|
||||
set_kind("shared")
|
||||
add_packages("libyuv")
|
||||
add_deps("rd_log")
|
||||
add_defines("CROSSDESK_WGC_PLUGIN_BUILD=1")
|
||||
add_links("windowsapp")
|
||||
add_files("src/screen_capturer/windows/screen_capturer_wgc.cpp",
|
||||
"src/screen_capturer/windows/wgc_session_impl.cpp",
|
||||
"src/screen_capturer/windows/wgc_plugin_entry.cpp")
|
||||
add_includedirs("src/common", "src/screen_capturer",
|
||||
"src/screen_capturer/windows")
|
||||
end
|
||||
|
||||
target("crossdesk")
|
||||
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_deps("wgc_plugin")
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user