Compare commits

..

9 Commits

21 changed files with 1266 additions and 346 deletions
+145 -22
View File
@@ -7,6 +7,11 @@ on:
tags: tags:
- "*" - "*"
workflow_dispatch: workflow_dispatch:
inputs:
patch:
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
required: false
default: "0"
permissions: permissions:
contents: write contents: write
@@ -37,21 +42,53 @@ jobs:
steps: steps:
- name: Extract version number - name: Extract version number
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}" VERSION_BASE="${VERSION_REF#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV PATCH_NUMBER="${{ github.event.inputs.patch }}"
BUILD_DATE_OVERRIDE=""
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV
- name: Set legal Debian version - name: Set legal Debian version
shell: bash shell: bash
run: | run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) BUILD_DATE="${BUILD_DATE_OVERRIDE}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) if [[ -z "${BUILD_DATE}" ]]; then
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
fi
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" VERSION_BASE="0.0.0-${VERSION_BASE}"
fi
if [[ "${PATCH_NUMBER}" != "0" ]]; then
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else else
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
fi fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
@@ -102,14 +139,43 @@ jobs:
steps: steps:
- name: Extract version number - name: Extract version number
id: version id: version
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) VERSION_BASE="${VERSION_REF#v}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}" PATCH_NUMBER="${{ github.event.inputs.patch }}"
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
if [[ "${PATCH_NUMBER}" != "0" ]]; then
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
fi
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}" echo "VERSION_NUM=${VERSION_NUM}"
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
@@ -163,10 +229,38 @@ jobs:
$version = $ref -replace '^refs/(tags|heads)/', '' $version = $ref -replace '^refs/(tags|heads)/', ''
$version = $version -replace '^v', '' $version = $version -replace '^v', ''
$version = $version -replace '/', '-' $version = $version -replace '/', '-'
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd") $BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV $PATCH_NUMBER = "${{ github.event.inputs.patch }}"
if ($PATCH_NUMBER -notmatch '^[0-9]+$') {
$PATCH_NUMBER = "0"
}
if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') {
$version = $Matches[1]
$PATCH_NUMBER = $Matches[3]
$BUILD_DATE = $Matches[4]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
$version = $Matches[1]
$BUILD_DATE = $Matches[3]
$PATCH_NUMBER = $Matches[4]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') {
$version = $Matches[1]
$BUILD_DATE = $Matches[3]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") {
$version = $Matches[1]
$PATCH_NUMBER = $Matches[3]
}
if ($PATCH_NUMBER -ne "0") {
$VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE"
} else {
$VERSION_NUM = "v$version-$BUILD_DATE"
}
echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
@@ -239,8 +333,7 @@ jobs:
- name: Package - name: Package
shell: pwsh shell: pwsh
run: | run: |
cd "${{ github.workspace }}\scripts\windows" & "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
- name: Build Portable CrossDesk - name: Build Portable CrossDesk
run: | run: |
@@ -310,19 +403,47 @@ jobs:
- name: Extract version number - name: Extract version number
id: version id: version
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) VERSION_BASE="${VERSION_REF#v}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d) PATCH_NUMBER="${{ github.event.inputs.patch }}"
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}"
if [[ "${PATCH_NUMBER}" != "0" ]]; then
VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else
VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}"
fi
VERSION_WITH_V="v${VERSION_NUM}" VERSION_WITH_V="v${VERSION_NUM}"
VERSION_ONLY="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Rename artifacts - name: Rename artifacts
run: | run: |
@@ -380,8 +501,10 @@ jobs:
run: | run: |
cat > version.json << EOF cat > version.json << EOF
{ {
"version": "${{ steps.version.outputs.VERSION_ONLY }}", "latest_version": "${{ steps.version.outputs.VERSION_NUM }}",
"version": "${{ steps.version.outputs.VERSION_NUM }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}", "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
"releaseName": "", "releaseName": "",
"releaseNotes": "", "releaseNotes": "",
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}", "tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
+27 -7
View File
@@ -20,21 +20,39 @@ jobs:
- name: Extract version from tag - name: Extract version from tag
id: version id: version
shell: bash
run: | run: |
TAG_NAME="${{ github.event.release.tag_name }}" TAG_NAME="${{ github.event.release.tag_name }}"
VERSION_ONLY="${TAG_NAME#v}" TAG_VERSION="${TAG_NAME#v}"
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT VERSION_FULL="${TAG_VERSION}"
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT VERSION_BASE="${TAG_VERSION}"
PATCH_NUMBER=0
# Extract date from tag if available (format: v1.2.3-20251113-abc) # Extract date and patch from tags such as v1.2.3-1-20251113.
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
DATE_STR="${BASH_REMATCH[1]}" VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
DATE_STR="${BASH_REMATCH[4]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
DATE_STR="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
DATE_STR="${BASH_REMATCH[3]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}" BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
else else
# Use release published date # Use release published date
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1) BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
fi fi
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Install jq - name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq run: sudo apt-get update && sudo apt-get install -y jq
@@ -122,8 +140,10 @@ jobs:
# Generate version.json using cat and heredoc # Generate version.json using cat and heredoc
cat > version.json << EOF cat > version.json << EOF
{ {
"version": "${{ steps.version.outputs.VERSION_ONLY }}", "latest_version": "${{ steps.version.outputs.VERSION_FULL }}",
"version": "${{ steps.version.outputs.VERSION_FULL }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}", "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }}, "releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }}, "releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
"tagName": "${{ steps.version.outputs.TAG_NAME }}", "tagName": "${{ steps.version.outputs.TAG_NAME }}",
+19 -1
View File
@@ -4,13 +4,31 @@ set -e
PKG_NAME="crossdesk" PKG_NAME="crossdesk"
APP_NAME="CrossDesk" APP_NAME="CrossDesk"
APP_VERSION="$1"
ARCHITECTURE="amd64" ARCHITECTURE="amd64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>" MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client." DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64" ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr" PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit) # Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}" DEB_VERSION="${APP_VERSION#v}"
+19 -1
View File
@@ -4,13 +4,31 @@ set -e
PKG_NAME="crossdesk" PKG_NAME="crossdesk"
APP_NAME="CrossDesk" APP_NAME="CrossDesk"
APP_VERSION="$1"
ARCHITECTURE="arm64" ARCHITECTURE="arm64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>" MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client." DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64" ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr" PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit) # Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}" DEB_VERSION="${APP_VERSION#v}"
+19 -1
View File
@@ -4,13 +4,31 @@ set -e
APP_NAME="crossdesk" APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk" APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk" EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
APP_VERSION="$1"
PLATFORM="macos" PLATFORM="macos"
ARCH="arm64" ARCH="arm64"
IDENTIFIER="cn.crossdesk.app" IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" MACOS_MIN_VERSION="10.12"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
APP_BUNDLE="${APP_NAME_UPPER}.app" APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents" CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
+19 -1
View File
@@ -4,13 +4,31 @@ set -e
APP_NAME="crossdesk" APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk" APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk" EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
APP_VERSION="$1"
PLATFORM="macos" PLATFORM="macos"
ARCH="x64" ARCH="x64"
IDENTIFIER="cn.crossdesk.app" IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" MACOS_MIN_VERSION="10.12"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
APP_BUNDLE="${APP_NAME_UPPER}.app" APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents" CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
name="CrossDesk"
type="win32" />
<description>CrossDesk Portable Application</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
+8
View File
@@ -0,0 +1,8 @@
// Portable build resource. The app itself runs as the current user; service
// installation raises a separate UAC prompt only when the user chooses it.
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
#define RT_MANIFEST 24
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
+40
View File
@@ -0,0 +1,40 @@
param(
[Parameter(Mandatory = $true)]
[string]$Version
)
$ErrorActionPreference = "Stop"
function Normalize-AppVersion {
param(
[Parameter(Mandatory = $true)]
[string]$InputVersion
)
$prefix = ""
$body = $InputVersion
if ($body.StartsWith("v")) {
$prefix = "v"
$body = $body.Substring(1)
}
if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])"
}
return $InputVersion
}
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Push-Location $scriptDir
try {
& makensis "/DVERSION=$normalizedVersion" "nsis_script.nsi"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
} finally {
Pop-Location
}
+21
View File
@@ -79,6 +79,9 @@ int ConfigCenter::Load() {
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_); enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue( enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_); section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
portable_service_prompt_suppressed_ =
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
const char* file_transfer_save_path_value = const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr); ini_.GetValue(section_, "file_transfer_save_path", nullptr);
@@ -118,6 +121,8 @@ int ConfigCenter::Save() {
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_); ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray", ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_); enable_minimize_to_tray_);
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
ini_.SetValue(section_, "file_transfer_save_path", ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str()); file_transfer_save_path_.c_str());
@@ -325,6 +330,18 @@ int ConfigCenter::SetDaemon(bool enable_daemon) {
return 0; return 0;
} }
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
portable_service_prompt_suppressed_ = suppressed;
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
// getters // getters
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; } ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
@@ -377,6 +394,10 @@ bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; } bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
return portable_service_prompt_suppressed_;
}
int ConfigCenter::SetFileTransferSavePath(const std::string& path) { int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
file_transfer_save_path_ = path; file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path", ini_.SetValue(section_, "file_transfer_save_path",
+3
View File
@@ -39,6 +39,7 @@ class ConfigCenter {
int SetMinimizeToTray(bool enable_minimize_to_tray); int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart); int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon); int SetDaemon(bool enable_daemon);
int SetPortableServicePromptSuppressed(bool suppressed);
int SetFileTransferSavePath(const std::string& path); int SetFileTransferSavePath(const std::string& path);
// read config // read config
@@ -60,6 +61,7 @@ class ConfigCenter {
bool IsMinimizeToTray() const; bool IsMinimizeToTray() const;
bool IsEnableAutostart() const; bool IsEnableAutostart() const;
bool IsEnableDaemon() const; bool IsEnableDaemon() const;
bool IsPortableServicePromptSuppressed() const;
std::string GetFileTransferSavePath() const; std::string GetFileTransferSavePath() const;
int Load(); int Load();
@@ -87,6 +89,7 @@ class ConfigCenter {
bool enable_minimize_to_tray_ = false; bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false; bool enable_autostart_ = false;
bool enable_daemon_ = false; bool enable_daemon_ = false;
bool portable_service_prompt_suppressed_ = false;
std::string file_transfer_save_path_ = ""; std::string file_transfer_save_path_ = "";
}; };
} // namespace crossdesk } // namespace crossdesk
+197 -170
View File
@@ -19,176 +19,203 @@ struct TranslationRow {
}; };
// Single source of truth for all UI strings. // Single source of truth for all UI strings.
#define CROSSDESK_LOCALIZATION_ALL(X) \ #define CROSSDESK_LOCALIZATION_ALL(X) \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \ X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \ X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \ X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \ u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \ X(password, u8"密码", "Password", u8"Пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \ X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \ X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \ u8"Удаленный рабочий стол") \
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \ X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
X(connect, u8"连接", "Connect", u8"Подключиться") \ X(connect, u8"连接", "Connect", u8"Подключиться") \
X(recent_connections, u8"近期连接", "Recent Connections", \ X(recent_connections, u8"近期连接", "Recent Connections", \
u8"Недавние подключения") \ u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \ X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \ X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \ X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
u8"Развернуть панель управления") \ u8"Развернуть панель управления") \
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \ X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
u8"Свернуть панель управления") \ u8"Свернуть панель управления") \
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \ X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \ X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
u8"Показать статистику трафика") \ u8"Показать статистику трафика") \
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \ X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
u8"Скрыть статистику трафика") \ u8"Скрыть статистику трафика") \
X(video, u8"视频", "Video", u8"Видео") \ X(video, u8"视频", "Video", u8"Видео") \
X(audio, u8"音频", "Audio", u8"Аудио") \ X(audio, u8"音频", "Audio", u8"Аудио") \
X(data, u8"数据", "Data", u8"Данные") \ X(data, u8"数据", "Data", u8"Данные") \
X(total, u8"总计", "Total", u8"Итого") \ X(total, u8"总计", "Total", u8"Итого") \
X(in, u8"输入", "In", u8"Вход") \ X(in, u8"输入", "In", u8"Вход") \
X(out, u8"输出", "Out", u8"Выход") \ X(out, u8"输出", "Out", u8"Выход") \
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \ X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \ X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
u8"Выйти из полноэкранного режима") \ u8"Выйти из полноэкранного режима") \
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \ X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \ X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \ X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
X(mute, u8" 静音", " Mute", u8"Без звука") \ X(mute, u8" 静音", " Mute", u8"Без звука") \
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \ X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \ X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \ X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
X(remote_password_box_visible, u8"远端密码框已出现", \ X(remote_password_box_visible, u8"远端密码框已出现", \
"Remote password box visible", u8"Окно ввода пароля видно") \ "Remote password box visible", u8"Окно ввода пароля видно") \
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \ X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
"Remote lock screen visible, send SAS", \ "Remote lock screen visible, send SAS", \
u8"Видна блокировка, отправьте SAS") \ u8"Видна блокировка, отправьте SAS") \
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \ X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \ "Remote secure desktop active", u8"Активен защищенный рабочий стол") \
X(remote_service_unavailable, u8"远端Windows服务不可用", \ X(remote_service_unavailable, u8"远端Windows服务不可用", \
"Remote Windows service unavailable", \ "Remote Windows service unavailable", \
u8"Служба Windows на удаленной стороне недоступна") \ u8"Служба Windows на удаленной стороне недоступна") \
X(windows_service_setup_title, u8"安装 CrossDesk Service", \ X(windows_service_setup_title, u8"安装 CrossDesk Service", \
"Install CrossDesk Service", u8"Установить CrossDesk Service") \ "Install CrossDesk Service", u8"Установить CrossDesk Service") \
X(windows_service_setup_message, \ X(windows_service_setup_message, \
u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \ u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
"The portable version needs the local Windows service for full control on the lock screen, sign-in UI, and secure desktop. The service is not installed. Click Install and approve the system prompt.", \ u8"Service。\n未安装该服务不影响 CrossDesk " \
u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \ u8"正常使用,仅无法在锁屏状态下控制本机。", \
X(install_windows_service, u8"安装", "Install", \ "To support remote control of this device while it is locked, CrossDesk " \
u8"Установить службу") \ "Service must be installed with administrator permission.\nWithout this " \
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \ "service, CrossDesk still works normally; only lock-screen control of " \
u8"Установка службы...") \ "this computer is unavailable.", \
X(windows_service_install_success, u8"服务已安装并启动", \ u8"Чтобы поддерживать удаленное управление этим устройством на экране " \
"Service installed and started", u8"Служба установлена и запущена") \ u8"блокировки, необходимо установить CrossDesk Service с правами " \
X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \ u8"администратора.\nБез этой службы CrossDesk продолжит работать " \
"Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \ u8"нормально; будет недоступно только управление этим компьютером на " \
u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \ u8"экране блокировки.") \
X(remote_unlock_requires_secure_desktop, \ X(install_windows_service, u8"安装", "Install", u8"Установить") \
u8"当前仍需要安全桌面专用采集/输入", \ X(windows_service_settings_label, u8"锁屏控制服务:", \
"Secure desktop capture/input is still required", \ "Lock Screen Service:", u8"Служба блокировки экрана:") \
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \ X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
u8"стола") \ X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
X(settings, u8"设置", "Settings", u8"Настройки") \ u8"Больше не напоминать") \
X(language, u8"语言:", "Language:", u8"Язык:") \ X(windows_service_prompt_suppressed_message, \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \ u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \
X(video_frame_rate, u8"画面采集帧率:", \ "You will not be reminded again. To enable remote control while locked " \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \ "later, click Install in Settings.", \
X(video_quality_high, u8"高", "High", u8"Высокое") \ u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \ u8"экране блокировки, нажмите «Установить» в настройках.") \
X(video_quality_low, u8"低", "Low", u8"Низкое") \ X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
X(video_encode_format, u8"视频编码格式:", \ u8"Установка службы...") \
"Video Encode Format:", u8"Формат кодека видео:") \ X(windows_service_install_success, u8"服务已安装并启动", \
X(av1, u8"AV1", "AV1", "AV1") \ "Service installed and started", u8"Служба установлена и запущена") \
X(h264, u8"H.264", "H.264", "H.264") \ X(windows_service_install_failed, \
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \ u8"服务安装失败。请确认 " \
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \ u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \
X(enable_turn, u8"启用中继服务:", \ u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \ "Service installation failed. Make sure CrossDesk.exe, " \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \ "crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \ "portable folder, then approve the administrator prompt.", \
u8"Конфигурация self-hosted") \ u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \ u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \
u8"Настройки self-hosted") \ u8"одной папке портативной версии, затем подтвердите запрос прав " \
X(self_hosted_server_address, u8"服务器地址:", \ u8"администратора.") \
"Server Address:", u8"Адрес сервера:") \ X(remote_unlock_requires_secure_desktop, \
X(self_hosted_server_port, u8"信令服务端口:", \ u8"当前仍需要安全桌面专用采集/输入", \
"Signal Service Port:", u8"Порт сигнального сервиса:") \ "Secure desktop capture/input is still required", \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \ u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
"Relay Service Port:", u8"Порт реле-сервиса:") \ u8"стола") \
X(ok, u8"确认", "OK", u8"ОК") \ X(settings, u8"设置", "Settings", u8"Настройки") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \ X(language, u8"语言:", "Language:", u8"Язык:") \
X(new_password, u8"请输入六位密码:", \ X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
"Please input a six-char password:", u8"Введите шестизначный пароль:") \ X(video_frame_rate, u8"画面采集帧率:", \
X(input_password, u8"请输入密码:", \ "Video Capture Frame Rate:", u8"Частота захвата видео:") \
"Please input password:", u8"Введите пароль:") \ X(video_quality_high, u8"高", "High", u8"Высокое") \
X(validate_password, u8"验证密码中...", "Validate password ...", \ X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
u8"Проверка пароля...") \ X(video_quality_low, u8"低", "Low", u8"Низкое") \
X(reinput_password, u8"请重新输入密码", "Please input password again", \ X(video_encode_format, u8"视频编码格式:", \
u8"Повторно введите пароль") \ "Video Encode Format:", u8"Формат кодека видео:") \
X(remember_password, u8"记住密码", "Remember password", \ X(av1, u8"AV1", "AV1", "AV1") \
u8"Запомнить пароль") \ X(h264, u8"H.264", "H.264", "H.264") \
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \ X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \ "Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
u8"Нет подключения к серверу") \ X(enable_turn, u8"启用中继服务:", \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \ "Enable TURN Service:", u8"Включить TURN-сервис:") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \ X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
u8"P2P отключено") \ X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \ u8"Конфигурация self-hosted") \
u8"Подключение P2P...") \ X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \ u8"Настройки self-hosted") \
u8"Получение изображения...") \ X(self_hosted_server_address, u8"服务器地址:", \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \ "Server Address:", u8"Адрес сервера:") \
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \ X(self_hosted_server_port, u8"信令服务端口:", \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \ "Signal Service Port:", u8"Порт сигнального сервиса:") \
X(about, u8"关于", "About", u8"О программе") \ X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
X(notification, u8"通知", "Notification", u8"Уведомление") \ "Relay Service Port:", u8"Порт реле-сервиса:") \
X(new_version_available, u8"新版本可用", "New Version Available", \ X(ok, u8"确认", "OK", u8"ОК") \
u8"Доступна новая версия") \ X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(version, u8"版本", "Version", u8"Версия") \ X(new_password, u8"请输入六位密码:", \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \ "Please input a six-char password:", u8"Введите шестизначный пароль:") \
X(access_website, u8"访问官网: ", \ X(input_password, u8"请输入密码:", \
"Access Website: ", u8"Официальный сайт: ") \ "Please input password:", u8"Введите пароль:") \
X(update, u8"更新", "Update", u8"Обновить") \ X(validate_password, u8"验证密码中...", "Validate password ...", \
X(confirm_delete_connection, u8"确认删除此连接", \ u8"Проверка пароля...") \
"Confirm to delete this connection", u8"Удалить это подключение?") \ X(reinput_password, u8"请重新输入密码", "Please input password again", \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \ u8"Повторно введите пароль") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \ X(remember_password, u8"记住密码", "Remember password", \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \ u8"Запомнить пароль") \
u8"Вступит в силу после перезапуска") \ X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
X(select_file, u8"选择文件发送", "Select File to Send", \ X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Выбрать файл для отправки") \ u8"Нет подключения к серверу") \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \ X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
u8"Прогресс передачи файлов") \ X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
X(queued, u8"队列中", "Queued", u8"В очереди") \ u8"P2P отключено") \
X(sending, u8"正在传输", "Sending", u8"Передача") \ X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
X(completed, u8"已完成", "Completed", u8"Завершено") \ u8"Подключение P2P...") \
X(failed, u8"失败", "Failed", u8"Ошибка") \ X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \ u8"Получение изображения...") \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \ X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
X(connection_status, u8"连接状态:", \ X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
"Connection Status:", u8"Состояние соединения:") \ X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(file_transfer_save_path, u8"文件接收保存路径:", \ X(about, u8"关于", "About", u8"О программе") \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \ X(notification, u8"通知", "Notification", u8"Уведомление") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \ X(new_version_available, u8"新版本可用", "New Version Available", \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \ u8"Доступна новая версия") \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \ X(version, u8"版本", "Version", u8"Версия") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \ X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \ X(access_website, u8"访问官网: ", \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \ "Access Website: ", u8"Официальный сайт: ") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \ X(update, u8"更新", "Update", u8"Обновить") \
X(online, u8"在线", "Online", u8"Онлайн") \ X(confirm_delete_connection, u8"确认删除此连接", \
X(offline, u8"离线", "Offline", u8"Офлайн") \ "Confirm to delete this connection", u8"Удалить это подключение?") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \ X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(request_permissions, u8"权限请求", "Request Permissions", \ X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
u8"Запрос разрешений") \ X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
X(screen_recording_permission, u8"屏幕录制权限", \ u8"Вступит в силу после перезапуска") \
"Screen Recording Permission", u8"Разрешение на запись экрана") \ X(select_file, u8"选择文件发送", "Select File to Send", \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \ u8"Выбрать файл для отправки") \
u8"Разрешение специальных возможностей") \ X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
X(permission_required_message, u8"该应用需要授权以下权限:", \ u8"Прогресс передачи файлов") \
"The application requires the following permissions:", \ X(queued, u8"队列中", "Queued", u8"В очереди") \
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"Выход") X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = { inline constexpr TranslationRow kTranslationRows[] = {
+40 -20
View File
@@ -609,6 +609,11 @@ int Render::LoadSettingsFromCacheFile() {
enable_autostart_ = config_center_->IsEnableAutostart(); enable_autostart_ = config_center_->IsEnableAutostart();
enable_daemon_ = config_center_->IsEnableDaemon(); enable_daemon_ = config_center_->IsEnableDaemon();
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray(); enable_minimize_to_tray_ = config_center_->IsMinimizeToTray();
#if _WIN32 && CROSSDESK_PORTABLE
portable_service_prompt_suppressed_ =
config_center_->IsPortableServicePromptSuppressed();
portable_service_do_not_remind_ = portable_service_prompt_suppressed_;
#endif
// File transfer save path // File transfer save path
{ {
@@ -1714,26 +1719,6 @@ int Render::DrawServerWindow() {
} }
int Render::Run() { int Render::Run() {
latest_version_info_ = CheckUpdate();
if (!latest_version_info_.empty() &&
latest_version_info_.contains("version") &&
latest_version_info_["version"].is_string()) {
latest_version_ = 'v' + latest_version_info_["version"].get<std::string>();
if (latest_version_info_.contains("releaseNotes") &&
latest_version_info_["releaseNotes"].is_string()) {
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
} else {
release_notes_ = "";
}
update_available_ = IsNewerVersion(CROSSDESK_VERSION, latest_version_);
if (update_available_) {
show_update_notification_window_ = true;
}
} else {
latest_version_ = "";
update_available_ = false;
}
path_manager_ = std::make_unique<PathManager>("CrossDesk"); path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (path_manager_) { if (path_manager_) {
exec_log_path_ = path_manager_->GetLogPath().string(); exec_log_path_ = path_manager_->GetLogPath().string();
@@ -1762,6 +1747,41 @@ int Render::Run() {
InitializeLogger(); InitializeLogger();
LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION); LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION);
latest_version_info_ = CheckUpdate();
if (!latest_version_info_.empty()) {
std::string version;
if (latest_version_info_.contains("latest_version") &&
latest_version_info_["latest_version"].is_string()) {
version = latest_version_info_["latest_version"].get<std::string>();
} else if (latest_version_info_.contains("version") &&
latest_version_info_["version"].is_string()) {
version = latest_version_info_["version"].get<std::string>();
}
if (!version.empty()) {
latest_version_ = 'v' + version;
} else {
latest_version_ = "";
}
if (latest_version_info_.contains("releaseNotes") &&
latest_version_info_["releaseNotes"].is_string()) {
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
} else {
release_notes_ = "";
}
update_available_ =
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
LOG_INFO("Update check: current={}, latest={}, available={}",
CROSSDESK_VERSION, latest_version_, update_available_);
if (update_available_) {
show_update_notification_window_ = true;
}
} else {
latest_version_ = "";
update_available_ = false;
LOG_WARN("Update check skipped: version.json is empty or missing latest_version");
}
InitializeSettings(); InitializeSettings();
InitializeSDL(); InitializeSDL();
InitializeModules(); InitializeModules();
+3
View File
@@ -567,6 +567,9 @@ class Render {
#if CROSSDESK_PORTABLE #if CROSSDESK_PORTABLE
bool portable_service_prompt_checked_ = false; bool portable_service_prompt_checked_ = false;
bool show_portable_service_install_window_ = false; bool show_portable_service_install_window_ = false;
bool show_portable_service_prompt_suppressed_window_ = false;
bool portable_service_do_not_remind_ = false;
bool portable_service_prompt_suppressed_ = false;
std::atomic<PortableServiceInstallState> portable_service_install_state_{ std::atomic<PortableServiceInstallState> portable_service_install_state_{
PortableServiceInstallState::idle}; PortableServiceInstallState::idle};
std::thread portable_service_install_thread_; std::thread portable_service_install_thread_;
+113 -29
View File
@@ -4,10 +4,19 @@
#include "render.h" #include "render.h"
#include "tinyfiledialogs.h" #include "tinyfiledialogs.h"
#if _WIN32 && CROSSDESK_PORTABLE
#include "service_host.h"
#endif
namespace crossdesk { namespace crossdesk {
int Render::SettingWindow() { int Render::SettingWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
float portable_y_padding = 0.0f;
#if _WIN32 && CROSSDESK_PORTABLE
portable_y_padding = 0.05f;
#endif
if (show_settings_window_) { if (show_settings_window_) {
if (settings_window_pos_reset_) { if (settings_window_pos_reset_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport* viewport = ImGui::GetMainViewport();
@@ -18,12 +27,14 @@ int Render::SettingWindow() {
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f)); ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f)); ImVec2(io.DisplaySize.x * 0.315f,
io.DisplaySize.y * (0.9f + portable_y_padding)));
#else #else
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f)); ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f)); ImVec2(io.DisplaySize.x * 0.315f,
io.DisplaySize.y * (0.85f + portable_y_padding)));
#endif #endif
} else { } else {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
@@ -32,12 +43,14 @@ int Render::SettingWindow() {
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f)); ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f)); ImVec2(io.DisplaySize.x * 0.42f,
io.DisplaySize.y * (0.9f + portable_y_padding)));
#else #else
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f)); ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f)); ImVec2(io.DisplaySize.x * 0.42f,
io.DisplaySize.y * (0.85f + portable_y_padding)));
#endif #endif
} }
@@ -73,23 +86,21 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo( if (ImGui::BeginCombo("##language",
"##language", localization::GetSupportedLanguages()
localization::GetSupportedLanguages() [localization::detail::ClampLanguageIndex(
[localization::detail::ClampLanguageIndex( language_button_value_)]
language_button_value_)] .display_name.c_str())) {
.display_name
.c_str())) {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < static_cast<int>(supported_languages.size()); for (int i = 0; i < static_cast<int>(supported_languages.size());
++i) { ++i) {
bool selected = (i == language_button_value_); bool selected = (i == language_button_value_);
if (ImGui::Selectable( if (ImGui::Selectable(supported_languages[i].display_name.c_str(),
supported_languages[i].display_name.c_str(), selected)) selected))
language_button_value_ = i; language_button_value_ = i;
if (selected) { if (selected) {
ImGui::SetItemDefaultFocus(); ImGui::SetItemDefaultFocus();
@@ -125,7 +136,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -158,7 +169,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -194,7 +205,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -228,7 +239,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_hardware_video_codec", ImGui::Checkbox("##enable_hardware_video_codec",
@@ -249,7 +260,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_turn", &enable_turn_); ImGui::Checkbox("##enable_turn", &enable_turn_);
@@ -268,7 +279,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_srtp", &enable_srtp_); ImGui::Checkbox("##enable_srtp", &enable_srtp_);
@@ -289,7 +300,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_); ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
@@ -308,7 +319,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_autostart_", &enable_autostart_); ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
@@ -327,7 +338,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_daemon_", &enable_daemon_); ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
@@ -359,7 +370,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_minimize_to_tray_", ImGui::Checkbox("##enable_minimize_to_tray_",
@@ -384,7 +395,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f); ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
} }
std::string display_path = std::string display_path =
@@ -429,6 +440,80 @@ int Render::SettingWindow() {
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
#if _WIN32 && CROSSDESK_PORTABLE
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::windows_service_settings_label
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.0f);
} else if (ConfigCenter::LANGUAGE::ENGLISH == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.42f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
}
const PortableServiceInstallState state =
portable_service_install_state_.load(std::memory_order_acquire);
const bool service_installed =
IsCrossDeskServiceInstalled() ||
state == PortableServiceInstallState::succeeded;
if (service_installed) {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.9f);
} else if (ConfigCenter::LANGUAGE::ENGLISH ==
localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.32f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
}
ImGui::Text("%s", localization::windows_service_installed
[localization_language_index_]
.c_str());
} else {
if (state == PortableServiceInstallState::installing) {
ImGui::BeginDisabled();
}
if (ImGui::Button(localization::install_windows_service
[localization_language_index_]
.c_str())) {
StartPortableWindowsServiceInstall();
}
if (state == PortableServiceInstallState::installing) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::Text("%s", localization::installing_windows_service
[localization_language_index_]
.c_str());
} else if (state == PortableServiceInstallState::failed) {
ImGui::SameLine();
ImGui::Text(
"%s",
localization::failed[localization_language_index_].c_str());
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::PushTextWrapPos(title_bar_button_width_ * 10.0f);
ImGui::TextWrapped("%s",
localization::windows_service_install_failed
[localization_language_index_]
.c_str());
ImGui::PopTextWrapPos();
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
}
}
#endif
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f); ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f);
} else { } else {
@@ -436,7 +521,7 @@ int Render::SettingWindow() {
} }
settings_items_offset += settings_items_offset +=
settings_items_padding + title_bar_button_width_ * 0.3f; settings_items_padding + title_bar_button_width_ * 0.15f;
ImGui::SetCursorPosY(settings_items_offset); ImGui::SetCursorPosY(settings_items_offset);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -463,9 +548,8 @@ int Render::SettingWindow() {
LOG_INFO("Set localization language: {}", LOG_INFO("Set localization language: {}",
localization::GetSupportedLanguages() localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex( [localization::detail::ClampLanguageIndex(
localization_language_index_)] localization_language_index_)]
.code .code.c_str());
.c_str());
// Video quality // Video quality
if (video_quality_button_value_ == 0) { if (video_quality_button_value_ == 0) {
@@ -4,6 +4,7 @@
#include <shellapi.h> #include <shellapi.h>
#include <algorithm>
#include <vector> #include <vector>
#include "localization.h" #include "localization.h"
@@ -16,9 +17,8 @@ namespace {
std::filesystem::path GetCurrentExecutablePath() { std::filesystem::path GetCurrentExecutablePath() {
std::vector<wchar_t> buffer(MAX_PATH); std::vector<wchar_t> buffer(MAX_PATH);
while (true) { while (true) {
DWORD length = DWORD length = GetModuleFileNameW(nullptr, buffer.data(),
GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
static_cast<DWORD>(buffer.size()));
if (length == 0) { if (length == 0) {
return {}; return {};
} }
@@ -46,7 +46,8 @@ bool InstallServiceWithElevation() {
if (!std::filesystem::exists(service_path) || if (!std::filesystem::exists(service_path) ||
!std::filesystem::exists(helper_path)) { !std::filesystem::exists(helper_path)) {
LOG_ERROR( LOG_ERROR(
"Portable service install failed: service binaries missing, service={}, " "Portable service install failed: service binaries missing, "
"service={}, "
"helper={}", "helper={}",
service_path.string(), helper_path.string()); service_path.string(), helper_path.string());
return false; return false;
@@ -106,12 +107,17 @@ void Render::CheckPortableWindowsService() {
return; return;
} }
if (portable_service_prompt_suppressed_) {
return;
}
portable_service_install_state_.store(PortableServiceInstallState::idle, portable_service_install_state_.store(PortableServiceInstallState::idle,
std::memory_order_relaxed); std::memory_order_relaxed);
show_portable_service_install_window_ = true; show_portable_service_install_window_ = true;
} }
void Render::StartPortableWindowsServiceInstall() { void Render::StartPortableWindowsServiceInstall() {
portable_service_do_not_remind_ = false;
PortableServiceInstallState expected = PortableServiceInstallState::idle; PortableServiceInstallState expected = PortableServiceInstallState::idle;
if (!portable_service_install_state_.compare_exchange_strong( if (!portable_service_install_state_.compare_exchange_strong(
expected, PortableServiceInstallState::installing, expected, PortableServiceInstallState::installing,
@@ -140,18 +146,82 @@ void Render::JoinPortableWindowsServiceInstallThread() {
} }
int Render::PortableServiceInstallWindow() { int Render::PortableServiceInstallWindow() {
if (!show_portable_service_install_window_) { if (!show_portable_service_install_window_ &&
!show_portable_service_prompt_suppressed_window_) {
return 0; return 0;
} }
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport* viewport = ImGui::GetMainViewport();
const float window_width = title_bar_button_width_ * 12.0f; const float window_width =
const float window_height = title_bar_button_width_ * 4.0f; (std::min)(viewport->WorkSize.x * 0.6f, title_bar_button_width_ * 18.0f);
const float window_height =
(std::min)(viewport->WorkSize.y * 0.5f, title_bar_button_width_ * 8.0f);
if (show_portable_service_prompt_suppressed_window_) {
const float notice_width = window_width;
const float notice_height = (std::min)(viewport->WorkSize.y * 0.35f,
title_bar_button_width_ * 4.6f);
ImGui::SetNextWindowPos(
ImVec2(
viewport->WorkPos.x + (viewport->WorkSize.x - notice_width) / 2.0f,
viewport->WorkPos.y +
(viewport->WorkSize.y - notice_height) / 2.0f),
ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(notice_width, notice_height),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(
localization::notification[localization_language_index_].c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoTitleBar);
ImGui::Spacing();
ImGui::SetWindowFontScale(0.55f);
ImGui::SetCursorPosX(notice_width * 0.08f);
ImGui::Text(
"%s", localization::notification[localization_language_index_].c_str());
ImGui::SetWindowFontScale(0.5f);
ImGui::SetCursorPosX(notice_width * 0.06f);
ImGui::SetCursorPosY(notice_height * 0.28f);
ImGui::PushTextWrapPos(notice_width * 0.88f);
ImGui::TextWrapped("%s",
localization::windows_service_prompt_suppressed_message
[localization_language_index_]
.c_str());
ImGui::PopTextWrapPos();
const std::string ok_label = localization::ok[localization_language_index_];
const ImGuiStyle& style = ImGui::GetStyle();
const float ok_width =
ImGui::CalcTextSize(ok_label.c_str()).x + style.FramePadding.x * 2.0f;
ImGui::SetCursorPosX((notice_width - ok_width) * 0.5f);
ImGui::SetCursorPosY(notice_height * 0.75f);
if (ImGui::Button(ok_label.c_str())) {
show_portable_service_prompt_suppressed_window_ = false;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
}
if (!show_portable_service_install_window_) {
return 0;
}
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) / ImVec2(
2.0f, viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f,
(viewport->WorkSize.y - viewport->WorkPos.y - window_height) / viewport->WorkPos.y + (viewport->WorkSize.y - window_height) / 2.0f),
2.0f),
ImGuiCond_Appearing); ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
ImGuiCond_Always); ImGuiCond_Always);
@@ -187,15 +257,13 @@ int Render::PortableServiceInstallWindow() {
localization::installing_windows_service[localization_language_index_] localization::installing_windows_service[localization_language_index_]
.c_str(); .c_str();
if (state == PortableServiceInstallState::succeeded) { if (state == PortableServiceInstallState::succeeded) {
status_text = status_text = localization::windows_service_install_success
localization::windows_service_install_success [localization_language_index_]
[localization_language_index_] .c_str();
.c_str();
} else if (state == PortableServiceInstallState::failed) { } else if (state == PortableServiceInstallState::failed) {
status_text = status_text = localization::windows_service_install_failed
localization::windows_service_install_failed [localization_language_index_]
[localization_language_index_] .c_str();
.c_str();
} }
} }
@@ -203,7 +271,7 @@ int Render::PortableServiceInstallWindow() {
ImGui::SetCursorPosX(window_width * 0.04f); ImGui::SetCursorPosX(window_width * 0.04f);
ImGui::SetCursorPosY(window_height * 0.22f); ImGui::SetCursorPosY(window_height * 0.22f);
ImGui::BeginChild("PortableServiceInstallContent", ImGui::BeginChild("PortableServiceInstallContent",
ImVec2(window_width * 0.92f, window_height * 0.5f), ImVec2(window_width * 0.92f, window_height * 0.45f),
ImGuiChildFlags_Borders, ImGuiWindowFlags_None); ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
const float wrap_pos = ImGui::GetContentRegionAvail().x; const float wrap_pos = ImGui::GetContentRegionAvail().x;
@@ -220,7 +288,15 @@ int Render::PortableServiceInstallWindow() {
ImGui::EndChild(); ImGui::EndChild();
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
const float button_y = window_height * 0.76f; ImGui::SetCursorPosX(window_width * 0.08f);
ImGui::SetCursorPosY(window_height * 0.71f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
ImGui::Checkbox(
localization::do_not_remind_again[localization_language_index_].c_str(),
&portable_service_do_not_remind_);
ImGui::PopStyleVar();
const float button_y = window_height * 0.84f;
const ImGuiStyle& style = ImGui::GetStyle(); const ImGuiStyle& style = ImGui::GetStyle();
const auto default_button_width = [&style](const std::string& label) { const auto default_button_width = [&style](const std::string& label) {
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f; return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
@@ -259,6 +335,11 @@ int Render::PortableServiceInstallWindow() {
ImGui::BeginDisabled(); ImGui::BeginDisabled();
} }
if (ImGui::Button(cancel_label.c_str())) { if (ImGui::Button(cancel_label.c_str())) {
if (portable_service_do_not_remind_) {
portable_service_prompt_suppressed_ = true;
config_center_->SetPortableServicePromptSuppressed(true);
show_portable_service_prompt_suppressed_window_ = true;
}
show_portable_service_install_window_ = false; show_portable_service_install_window_ = false;
} }
if (state == PortableServiceInstallState::installing) { if (state == PortableServiceInstallState::installing) {
+351 -70
View File
@@ -8,7 +8,15 @@
#include <httplib.h> #include <httplib.h>
#include "rd_log.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <iostream> #include <iostream>
#include <limits>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -16,12 +24,296 @@
namespace crossdesk { namespace crossdesk {
static std::string latest_release_date_ = ""; static std::string latest_release_date_ = "";
static bool latest_patch_available_ = false;
static int latest_patch_ = 0;
std::vector<int> SplitVersion(const std::string& ver);
namespace {
constexpr size_t kMaxInlinePatchDigits = 4;
struct ParsedVersion {
std::vector<int> numbers;
std::string date;
bool has_patch = false;
int patch = 0;
};
bool IsDigit(char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
}
bool IsAlphaNumeric(char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0;
}
bool IsAllDigits(const std::string& value) {
if (value.empty()) {
return false;
}
for (char c : value) {
if (!IsDigit(c)) {
return false;
}
}
return true;
}
bool TryParseNonNegativeInt(const std::string& value, int* result) {
if (!IsAllDigits(value)) {
return false;
}
try {
const long long parsed = std::stoll(value);
if (parsed > std::numeric_limits<int>::max()) {
return false;
}
*result = static_cast<int>(parsed);
return true;
} catch (...) {
return false;
}
}
bool TryParseInlinePatch(const std::string& value, int* result) {
if (value.size() > kMaxInlinePatchDigits) {
return false;
}
return TryParseNonNegativeInt(value, result);
}
size_t FindNumericStart(const std::string& version) {
size_t start = 0;
while (start < version.size() && !IsDigit(version[start])) {
start++;
}
return start;
}
size_t FindNumericEnd(const std::string& version, size_t start) {
size_t end = start;
while (end < version.size() &&
(IsDigit(version[end]) || version[end] == '.')) {
end++;
}
return end;
}
bool HasDigitBoundary(const std::string& value, size_t pos, size_t len) {
const bool before_ok = pos == 0 || !IsDigit(value[pos - 1]);
const size_t end = pos + len;
const bool after_ok = end >= value.size() || !IsDigit(value[end]);
return before_ok && after_ok;
}
bool IsCompactDateAt(const std::string& value, size_t pos) {
if (pos + 8 > value.size() || !HasDigitBoundary(value, pos, 8)) {
return false;
}
for (size_t i = 0; i < 8; ++i) {
if (!IsDigit(value[pos + i])) {
return false;
}
}
return true;
}
std::string CompactDateToIso(const std::string& compact_date) {
return compact_date.substr(0, 4) + "-" + compact_date.substr(4, 2) + "-" +
compact_date.substr(6, 2);
}
bool ExtractDateFromText(const std::string& value,
std::string* date,
size_t* date_end) {
for (size_t i = 0; i < value.size(); ++i) {
if (IsCompactDateAt(value, i)) {
*date = CompactDateToIso(value.substr(i, 8));
*date_end = i + 8;
return true;
}
}
return false;
}
ParsedVersion ParseVersion(const std::string& version) {
const size_t numeric_start = FindNumericStart(version);
const size_t numeric_end = FindNumericEnd(version, numeric_start);
ParsedVersion parsed;
parsed.numbers = SplitVersion(version.substr(numeric_start,
numeric_end - numeric_start));
const std::string suffix = version.substr(numeric_end);
size_t pos = 0;
while (pos < suffix.size()) {
while (pos < suffix.size() && !IsAlphaNumeric(suffix[pos])) {
pos++;
}
const size_t token_start = pos;
while (pos < suffix.size() && IsAlphaNumeric(suffix[pos])) {
pos++;
}
if (token_start == pos) {
continue;
}
const std::string token = suffix.substr(token_start, pos - token_start);
if (parsed.date.empty() && IsCompactDateAt(token, 0)) {
parsed.date = CompactDateToIso(token);
continue;
}
int patch = 0;
if (!parsed.has_patch && TryParseInlinePatch(token, &patch)) {
parsed.has_patch = true;
parsed.patch = patch;
}
}
return parsed;
}
int CompareNumericVersion(const std::vector<int>& current,
const std::vector<int>& latest) {
std::vector<int> current_parts = current;
std::vector<int> latest_parts = latest;
const size_t len = std::max(current_parts.size(), latest_parts.size());
current_parts.resize(len, 0);
latest_parts.resize(len, 0);
for (size_t i = 0; i < len; ++i) {
if (latest_parts[i] > current_parts[i]) {
return 1;
}
if (latest_parts[i] < current_parts[i]) {
return -1;
}
}
return 0;
}
void ResetLatestMetadata() {
latest_release_date_ = "";
latest_patch_available_ = false;
latest_patch_ = 0;
}
bool ReadPatchField(const nlohmann::json& json, int* patch) {
if (!json.contains("patch")) {
return false;
}
const auto& patch_value = json["patch"];
if (patch_value.is_number_integer()) {
const long long parsed = patch_value.get<long long>();
if (parsed < 0 || parsed > std::numeric_limits<int>::max()) {
return false;
}
*patch = static_cast<int>(parsed);
return true;
}
if (patch_value.is_string()) {
return TryParseNonNegativeInt(patch_value.get<std::string>(), patch);
}
return false;
}
void LogHttpError(const httplib::Result& result) {
LOG_WARN("Failed to fetch version.json: error={}, message={}",
static_cast<int>(result.error()), httplib::to_string(result.error()));
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
LOG_WARN("version.json SSL error={}, OpenSSL error={}", result.ssl_error(),
result.ssl_openssl_error());
#endif
}
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
bool PathExists(const std::string& path) {
if (path.empty()) {
return false;
}
std::error_code ec;
return std::filesystem::exists(path, ec);
}
std::string GetEnvPathIfExists(const char* key) {
const char* value = std::getenv(key);
if (!value) {
return "";
}
const std::string path = value;
return PathExists(path) ? path : "";
}
std::string FindFirstExistingPath(
const std::vector<std::string>& candidates) {
for (const auto& candidate : candidates) {
if (PathExists(candidate)) {
return candidate;
}
}
return "";
}
void ConfigureLinuxCaCerts(httplib::Client* cli) {
const std::string ca_file = [&]() {
const std::string env_path = GetEnvPathIfExists("SSL_CERT_FILE");
if (!env_path.empty()) {
return env_path;
}
return FindFirstExistingPath({
"/etc/ssl/certs/ca-certificates.crt",
"/etc/pki/tls/certs/ca-bundle.crt",
"/etc/ssl/cert.pem",
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
});
}();
const std::string ca_dir = [&]() {
const std::string env_path = GetEnvPathIfExists("SSL_CERT_DIR");
if (!env_path.empty()) {
return env_path;
}
return FindFirstExistingPath({
"/etc/ssl/certs",
"/etc/pki/tls/certs",
"/etc/openssl/certs",
});
}();
if (ca_file.empty() && ca_dir.empty()) {
LOG_WARN("No Linux CA bundle found for version.json request; relying on OpenSSL defaults");
return;
}
cli->set_ca_cert_path(ca_file, ca_dir);
LOG_INFO("Configured version.json TLS CA bundle: file={}, dir={}",
ca_file.empty() ? "<none>" : ca_file,
ca_dir.empty() ? "<none>" : ca_dir);
}
#endif
} // namespace
std::string ExtractNumericPart(const std::string& ver) { std::string ExtractNumericPart(const std::string& ver) {
size_t start = 0; const size_t start = FindNumericStart(ver);
while (start < ver.size() && !std::isdigit(ver[start])) start++; const size_t end = FindNumericEnd(ver, start);
size_t end = start;
while (end < ver.size() && (std::isdigit(ver[end]) || ver[end] == '.')) end++;
return ver.substr(start, end - start); return ver.substr(start, end - start);
} }
@@ -42,25 +334,13 @@ std::vector<int> SplitVersion(const std::string& ver) {
// extract date from version string (format: v1.2.3-20251113-abc // extract date from version string (format: v1.2.3-20251113-abc
// or 1.2.3-20251113-abc) // or 1.2.3-20251113-abc)
std::string ExtractDateFromVersion(const std::string& version) { std::string ExtractDateFromVersion(const std::string& version) {
size_t dash1 = version.find('-'); const size_t numeric_start = FindNumericStart(version);
if (dash1 != std::string::npos) { const size_t numeric_end = FindNumericEnd(version, numeric_start);
size_t dash2 = version.find('-', dash1 + 1); const std::string suffix = version.substr(numeric_end);
if (dash2 != std::string::npos) { std::string date;
std::string date_part = version.substr(dash1 + 1, dash2 - dash1 - 1); size_t date_end = 0;
if (ExtractDateFromText(suffix, &date, &date_end)) {
bool is_date = true; return date;
for (char c : date_part) {
if (!std::isdigit(c)) {
is_date = false;
break;
}
}
if (is_date) {
// convert YYYYMMDD to YYYY-MM-DD
return date_part.substr(0, 4) + "-" + date_part.substr(4, 2) + "-" +
date_part.substr(6, 2);
}
}
} }
return ""; return "";
} }
@@ -73,55 +353,41 @@ bool IsNewerDate(const std::string& date1, const std::string& date2) {
} }
bool IsNewerVersion(const std::string& current, const std::string& latest) { bool IsNewerVersion(const std::string& current, const std::string& latest) {
auto v1 = SplitVersion(ExtractNumericPart(current)); return IsNewerVersionWithMetadata(
auto v2 = SplitVersion(ExtractNumericPart(latest)); current, latest, latest_release_date_,
latest_patch_available_ ? latest_patch_ : -1);
size_t len = std::max(v1.size(), v2.size());
v1.resize(len, 0);
v2.resize(len, 0);
for (size_t i = 0; i < len; ++i) {
if (v2[i] > v1[i]) return true;
if (v2[i] < v1[i]) return false;
}
// if versions are equal, compare by release date
if (!latest_release_date_.empty()) {
// try to extract date from current version string
std::string current_date = ExtractDateFromVersion(current);
if (!current_date.empty()) {
return IsNewerDate(current_date, latest_release_date_);
} else {
return true;
}
}
return false;
} }
bool IsNewerVersionWithDate(const std::string& current_version, bool IsNewerVersionWithMetadata(const std::string& current,
const std::string& current_date, const std::string& latest,
const std::string& latest_version, const std::string& latest_date,
const std::string& latest_date) { int latest_patch) {
// compare versions (void)latest_date;
auto v1 = SplitVersion(ExtractNumericPart(current_version));
auto v2 = SplitVersion(ExtractNumericPart(latest_version));
size_t len = std::max(v1.size(), v2.size()); const ParsedVersion current_version = ParseVersion(current);
v1.resize(len, 0); const ParsedVersion latest_version = ParseVersion(latest);
v2.resize(len, 0);
for (size_t i = 0; i < len; ++i) { const int numeric_compare =
if (v2[i] > v1[i]) return true; CompareNumericVersion(current_version.numbers, latest_version.numbers);
if (v2[i] < v1[i]) return false; if (numeric_compare > 0) {
return true;
}
if (numeric_compare < 0) {
return false;
} }
// if versions are equal, compare by release date const bool metadata_has_patch = latest_patch >= 0;
if (!current_date.empty() && !latest_date.empty()) { const bool latest_has_patch = metadata_has_patch || latest_version.has_patch;
return IsNewerDate(current_date, latest_date); if (latest_has_patch || current_version.has_patch) {
const int resolved_latest_patch =
metadata_has_patch ? latest_patch
: (latest_version.has_patch ? latest_version.patch
: 0);
const int resolved_current_patch =
current_version.has_patch ? current_version.patch : 0;
return resolved_latest_patch > resolved_current_patch;
} }
// if dates are not available, versions are equal
return false; return false;
} }
@@ -130,8 +396,14 @@ nlohmann::json CheckUpdate() {
cli.set_connection_timeout(5); cli.set_connection_timeout(5);
cli.set_read_timeout(5); cli.set_read_timeout(5);
cli.set_follow_location(true);
if (auto res = cli.Get("/version.json")) { #if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
ConfigureLinuxCaCerts(&cli);
#endif
auto res = cli.Get("/version.json");
if (res) {
if (res->status == 200) { if (res->status == 200) {
try { try {
auto j = nlohmann::json::parse(res->body); auto j = nlohmann::json::parse(res->body);
@@ -140,19 +412,28 @@ nlohmann::json CheckUpdate() {
} else { } else {
latest_release_date_ = ""; latest_release_date_ = "";
} }
latest_patch_ = 0;
latest_patch_available_ = ReadPatchField(j, &latest_patch_);
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
j.value("latest_version", j.value("version", "")),
j.value("releaseDate", ""),
latest_patch_available_ ? latest_patch_ : -1);
return j; return j;
} catch (std::exception&) { } catch (const std::exception& e) {
latest_release_date_ = ""; LOG_WARN("Failed to parse version.json: {}", e.what());
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} else { } else {
latest_release_date_ = ""; LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status);
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} else { } else {
latest_release_date_ = ""; LogHttpError(res);
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} }
} // namespace crossdesk } // namespace crossdesk
+7 -1
View File
@@ -16,6 +16,12 @@ nlohmann::json CheckUpdate();
bool IsNewerVersion(const std::string& current, const std::string& latest); bool IsNewerVersion(const std::string& current, const std::string& latest);
// Pass latest_patch < 0 when patch metadata is unavailable.
bool IsNewerVersionWithMetadata(const std::string& current,
const std::string& latest,
const std::string& latest_date,
int latest_patch);
} // namespace crossdesk } // namespace crossdesk
#endif #endif
+54
View File
@@ -0,0 +1,54 @@
#include "version_checker.h"
#include <iostream>
#include <string>
namespace {
bool ExpectEqual(const std::string& name, bool actual, bool expected) {
if (actual == expected) {
return true;
}
std::cerr << name << " mismatch\n"
<< " expected: " << expected << "\n"
<< " actual: " << actual << "\n";
return false;
}
} // namespace
int main() {
bool ok = true;
ok &= ExpectEqual("new patch-before-date is newer",
crossdesk::IsNewerVersionWithMetadata(
"v1.3.5-20260529", "v1.3.5-1-20260529", "", -1),
true);
ok &= ExpectEqual("larger patch wins regardless of date",
crossdesk::IsNewerVersionWithMetadata(
"v1.3.5-2-20260530", "v1.3.5-3-20260529", "", -1),
true);
ok &= ExpectEqual("smaller patch loses regardless of date",
crossdesk::IsNewerVersionWithMetadata(
"v1.3.5-3-20260529", "v1.3.5-2-20260530", "", -1),
false);
ok &= ExpectEqual("old date-before-patch remains supported",
crossdesk::IsNewerVersionWithMetadata(
"v1.3.5-20260529-1", "v1.3.5-20260529-2", "", -1),
true);
ok &= ExpectEqual("metadata patch overrides date",
crossdesk::IsNewerVersionWithMetadata(
"v1.3.5-9-20260530", "v1.3.5", "2026-05-31", 10),
true);
ok &= ExpectEqual("date alone does not update same version",
crossdesk::IsNewerVersionWithMetadata(
"v1.3.5-20260529", "v1.3.5-20260530", "", -1),
false);
ok &= ExpectEqual("numeric version still wins",
crossdesk::IsNewerVersionWithMetadata(
"v1.3.5-9-20260529", "v1.3.6-1-20260529", "", -1),
true);
return ok ? 0 : 1;
}
+23
View File
@@ -82,16 +82,27 @@ int main() {
} }
const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc"); const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc");
const std::string portable_rc =
ReadFile(repo_root / "scripts/windows/crossdesk_portable.rc");
const std::string manifest = const std::string manifest =
ReadFile(repo_root / "scripts/windows/crossdesk.manifest"); ReadFile(repo_root / "scripts/windows/crossdesk.manifest");
const std::string debug_manifest = const std::string debug_manifest =
ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest"); ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest");
const std::string portable_manifest =
ReadFile(repo_root / "scripts/windows/crossdesk_portable.manifest");
const std::string targets = ReadFile(repo_root / "xmake/targets.lua");
bool ok = true; bool ok = true;
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest"); ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest");
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest"); ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest");
ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG"); ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG");
ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST"); ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST");
ok &= ExpectContains("crossdesk_portable.rc", portable_rc,
"crossdesk_portable.manifest");
ok &= ExpectContains("crossdesk_portable.rc", portable_rc, "RT_MANIFEST");
ok &= ExpectContains("xmake/targets.lua", targets,
"scripts/windows/crossdesk_portable.rc");
ok &= ExpectContains("xmake/targets.lua", targets, "CROSSDESK_PORTABLE");
ok &= ExpectContains("crossdesk.manifest", manifest, ok &= ExpectContains("crossdesk.manifest", manifest,
"level=\"requireAdministrator\""); "level=\"requireAdministrator\"");
ok &= ExpectContains("crossdesk.manifest", manifest, ok &= ExpectContains("crossdesk.manifest", manifest,
@@ -108,10 +119,22 @@ int main() {
"http://schemas.microsoft.com/SMI/2016/WindowsSettings"); "http://schemas.microsoft.com/SMI/2016/WindowsSettings");
ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest, ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest,
"processorArchitecture=\"*\""); "processorArchitecture=\"*\"");
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
"level=\"asInvoker\"");
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
"level=\"requireAdministrator\"");
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
"http://schemas.microsoft.com/SMI/2005/WindowsSettings");
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
"processorArchitecture=\"*\"");
#ifdef _WIN32 #ifdef _WIN32
ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest"); ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
ok &= ExpectActivationContext( ok &= ExpectActivationContext(
repo_root / "scripts/windows/crossdesk_debug.manifest"); repo_root / "scripts/windows/crossdesk_debug.manifest");
ok &= ExpectActivationContext(
repo_root / "scripts/windows/crossdesk_portable.manifest");
#endif #endif
return ok ? 0 : 1; return ok ? 0 : 1;
} }
+24 -2
View File
@@ -3,6 +3,11 @@ function setup_targets()
includes("submodules", "thirdparty") includes("submodules", "thirdparty")
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
if is_config("CROSSDESK_PORTABLE", true) then
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
end
target("rd_log") target("rd_log")
set_kind("object") set_kind("object")
add_packages("spdlog") add_packages("spdlog")
@@ -65,6 +70,19 @@ function setup_targets()
set_default(false) set_default(false)
add_files("tests/display_popup_hover_state_test.cpp") add_files("tests/display_popup_hover_state_test.cpp")
target("version_checker_test")
set_kind("binary")
set_default(false)
add_packages("cpp-httplib")
add_deps("rd_log")
add_includedirs("src/version_checker")
add_files("tests/version_checker_test.cpp",
"src/version_checker/version_checker.cpp")
if is_os("macosx") then
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
add_frameworks("Security", "CoreFoundation")
end
target("screen_capturer") target("screen_capturer")
set_kind("object") set_kind("object")
add_deps("rd_log", "common") add_deps("rd_log", "common")
@@ -164,6 +182,10 @@ function setup_targets()
add_deps("rd_log") add_deps("rd_log")
add_files("src/version_checker/*.cpp") add_files("src/version_checker/*.cpp")
add_includedirs("src/version_checker", {public = true}) add_includedirs("src/version_checker", {public = true})
if is_os("macosx") then
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
add_frameworks("Security", "CoreFoundation")
end
target("tools") target("tools")
set_kind("object") set_kind("object")
@@ -223,7 +245,7 @@ function setup_targets()
add_deps("rd_log", "path_manager") add_deps("rd_log", "path_manager")
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32") add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
add_files("src/service/windows/session_helper_main.cpp") add_files("src/service/windows/session_helper_main.cpp")
add_files("scripts/windows/crossdesk.rc") add_files(crossdesk_windows_resource)
add_includedirs("src/service/windows", {public = true}) add_includedirs("src/service/windows", {public = true})
end end
@@ -237,6 +259,6 @@ function setup_targets()
add_includedirs("src/service/windows", {public = true}) add_includedirs("src/service/windows", {public = true})
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv") add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper") add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
add_files("scripts/windows/crossdesk.rc") add_files(crossdesk_windows_resource)
end end
end end