mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-06-30 11:01:50 +08:00
Compare commits
9 Commits
32345f93bf
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
| 5735f84008 | |||
| fe0cf42e5d | |||
| 04100584ce | |||
| 9d3a422916 | |||
| 65d8284fb8 | |||
| eea107db66 | |||
| 67812957db | |||
| 69d77e59cc | |||
| efcebfd82c |
+144
-21
@@ -7,6 +7,11 @@ on:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
patch:
|
||||
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
|
||||
required: false
|
||||
default: "0"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -37,21 +42,53 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
VERSION_NUM="${VERSION#v}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
BUILD_DATE_OVERRIDE=""
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set legal Debian version
|
||||
shell: bash
|
||||
run: |
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
BUILD_DATE="${BUILD_DATE_OVERRIDE}"
|
||||
if [[ -z "${BUILD_DATE}" ]]; then
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
fi
|
||||
|
||||
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
|
||||
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
|
||||
VERSION_BASE="0.0.0-${VERSION_BASE}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
|
||||
@@ -102,14 +139,43 @@ jobs:
|
||||
steps:
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
|
||||
echo "VERSION_NUM=${VERSION_NUM}"
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
@@ -163,10 +229,38 @@ jobs:
|
||||
$version = $ref -replace '^refs/(tags|heads)/', ''
|
||||
$version = $version -replace '^v', ''
|
||||
$version = $version -replace '/', '-'
|
||||
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
|
||||
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
|
||||
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV
|
||||
$PATCH_NUMBER = "${{ github.event.inputs.patch }}"
|
||||
|
||||
if ($PATCH_NUMBER -notmatch '^[0-9]+$') {
|
||||
$PATCH_NUMBER = "0"
|
||||
}
|
||||
|
||||
if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
$BUILD_DATE = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
$PATCH_NUMBER = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
}
|
||||
|
||||
if ($PATCH_NUMBER -ne "0") {
|
||||
$VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE"
|
||||
} else {
|
||||
$VERSION_NUM = "v$version-$BUILD_DATE"
|
||||
}
|
||||
|
||||
echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV
|
||||
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
|
||||
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
@@ -239,8 +333,7 @@ jobs:
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd "${{ github.workspace }}\scripts\windows"
|
||||
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
|
||||
& "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
|
||||
- name: Build Portable CrossDesk
|
||||
run: |
|
||||
@@ -310,19 +403,47 @@ jobs:
|
||||
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d)
|
||||
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}"
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
VERSION_WITH_V="v${VERSION_NUM}"
|
||||
VERSION_ONLY="${VERSION#v}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
@@ -380,8 +501,10 @@ jobs:
|
||||
run: |
|
||||
cat > version.json << EOF
|
||||
{
|
||||
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
|
||||
"latest_version": "${{ steps.version.outputs.VERSION_NUM }}",
|
||||
"version": "${{ steps.version.outputs.VERSION_NUM }}",
|
||||
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
|
||||
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
|
||||
"releaseName": "",
|
||||
"releaseNotes": "",
|
||||
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
|
||||
|
||||
@@ -20,21 +20,39 @@ jobs:
|
||||
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
TAG_NAME="${{ github.event.release.tag_name }}"
|
||||
VERSION_ONLY="${TAG_NAME#v}"
|
||||
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
|
||||
TAG_VERSION="${TAG_NAME#v}"
|
||||
VERSION_FULL="${TAG_VERSION}"
|
||||
VERSION_BASE="${TAG_VERSION}"
|
||||
PATCH_NUMBER=0
|
||||
|
||||
# Extract date from tag if available (format: v1.2.3-20251113-abc)
|
||||
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then
|
||||
DATE_STR="${BASH_REMATCH[1]}"
|
||||
# Extract date and patch from tags such as v1.2.3-1-20251113.
|
||||
if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
DATE_STR="${BASH_REMATCH[4]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
DATE_STR="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
DATE_STR="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
else
|
||||
# Use release published date
|
||||
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
|
||||
fi
|
||||
|
||||
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
@@ -122,8 +140,10 @@ jobs:
|
||||
# Generate version.json using cat and heredoc
|
||||
cat > version.json << EOF
|
||||
{
|
||||
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
|
||||
"latest_version": "${{ steps.version.outputs.VERSION_FULL }}",
|
||||
"version": "${{ steps.version.outputs.VERSION_FULL }}",
|
||||
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
|
||||
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
|
||||
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
|
||||
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
|
||||
"tagName": "${{ steps.version.outputs.TAG_NAME }}",
|
||||
|
||||
@@ -4,13 +4,31 @@ set -e
|
||||
PKG_NAME="crossdesk"
|
||||
APP_NAME="CrossDesk"
|
||||
|
||||
APP_VERSION="$1"
|
||||
ARCHITECTURE="amd64"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
|
||||
|
||||
@@ -4,13 +4,31 @@ set -e
|
||||
PKG_NAME="crossdesk"
|
||||
APP_NAME="CrossDesk"
|
||||
|
||||
APP_VERSION="$1"
|
||||
ARCHITECTURE="arm64"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
|
||||
|
||||
@@ -4,13 +4,31 @@ set -e
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
|
||||
APP_VERSION="$1"
|
||||
PLATFORM="macos"
|
||||
ARCH="arm64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="10.12"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
APP_BUNDLE="${APP_NAME_UPPER}.app"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
|
||||
@@ -4,13 +4,31 @@ set -e
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
|
||||
APP_VERSION="$1"
|
||||
PLATFORM="macos"
|
||||
ARCH="x64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="10.12"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
APP_BUNDLE="${APP_NAME_UPPER}.app"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<description>CrossDesk Portable Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -79,6 +79,9 @@ int ConfigCenter::Load() {
|
||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
enable_minimize_to_tray_ = ini_.GetBoolValue(
|
||||
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
|
||||
portable_service_prompt_suppressed_ =
|
||||
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
const char* file_transfer_save_path_value =
|
||||
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
|
||||
@@ -118,6 +121,8 @@ int ConfigCenter::Save() {
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
||||
enable_minimize_to_tray_);
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
@@ -325,6 +330,18 @@ int ConfigCenter::SetDaemon(bool enable_daemon) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
|
||||
portable_service_prompt_suppressed_ = suppressed;
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// getters
|
||||
|
||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
||||
@@ -377,6 +394,10 @@ bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||
|
||||
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
|
||||
return portable_service_prompt_suppressed_;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
|
||||
file_transfer_save_path_ = path;
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
|
||||
@@ -39,6 +39,7 @@ class ConfigCenter {
|
||||
int SetMinimizeToTray(bool enable_minimize_to_tray);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||
int SetFileTransferSavePath(const std::string& path);
|
||||
|
||||
// read config
|
||||
@@ -60,6 +61,7 @@ class ConfigCenter {
|
||||
bool IsMinimizeToTray() const;
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
bool IsPortableServicePromptSuppressed() const;
|
||||
std::string GetFileTransferSavePath() const;
|
||||
|
||||
int Load();
|
||||
@@ -87,6 +89,7 @@ class ConfigCenter {
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::string file_transfer_save_path_ = "";
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -72,18 +72,45 @@ struct TranslationRow {
|
||||
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
||||
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
|
||||
X(windows_service_setup_message, \
|
||||
u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \
|
||||
"The portable version needs the local Windows service for full control on the lock screen, sign-in UI, and secure desktop. The service is not installed. Click Install and approve the system prompt.", \
|
||||
u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \
|
||||
X(install_windows_service, u8"安装", "Install", \
|
||||
u8"Установить службу") \
|
||||
u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
|
||||
u8"Service。\n未安装该服务不影响 CrossDesk " \
|
||||
u8"正常使用,仅无法在锁屏状态下控制本机。", \
|
||||
"To support remote control of this device while it is locked, CrossDesk " \
|
||||
"Service must be installed with administrator permission.\nWithout this " \
|
||||
"service, CrossDesk still works normally; only lock-screen control of " \
|
||||
"this computer is unavailable.", \
|
||||
u8"Чтобы поддерживать удаленное управление этим устройством на экране " \
|
||||
u8"блокировки, необходимо установить CrossDesk Service с правами " \
|
||||
u8"администратора.\nБез этой службы CrossDesk продолжит работать " \
|
||||
u8"нормально; будет недоступно только управление этим компьютером на " \
|
||||
u8"экране блокировки.") \
|
||||
X(install_windows_service, u8"安装", "Install", u8"Установить") \
|
||||
X(windows_service_settings_label, u8"锁屏控制服务:", \
|
||||
"Lock Screen Service:", u8"Служба блокировки экрана:") \
|
||||
X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
|
||||
X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
|
||||
u8"Больше не напоминать") \
|
||||
X(windows_service_prompt_suppressed_message, \
|
||||
u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \
|
||||
"You will not be reminded again. To enable remote control while locked " \
|
||||
"later, click Install in Settings.", \
|
||||
u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \
|
||||
u8"экране блокировки, нажмите «Установить» в настройках.") \
|
||||
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
|
||||
u8"Установка службы...") \
|
||||
X(windows_service_install_success, u8"服务已安装并启动", \
|
||||
"Service installed and started", u8"Служба установлена и запущена") \
|
||||
X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \
|
||||
"Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \
|
||||
u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \
|
||||
X(windows_service_install_failed, \
|
||||
u8"服务安装失败。请确认 " \
|
||||
u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \
|
||||
u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \
|
||||
"Service installation failed. Make sure CrossDesk.exe, " \
|
||||
"crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \
|
||||
"portable folder, then approve the administrator prompt.", \
|
||||
u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \
|
||||
u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \
|
||||
u8"одной папке портативной версии, затем подтвердите запрос прав " \
|
||||
u8"администратора.") \
|
||||
X(remote_unlock_requires_secure_desktop, \
|
||||
u8"当前仍需要安全桌面专用采集/输入", \
|
||||
"Secure desktop capture/input is still required", \
|
||||
|
||||
+40
-20
@@ -609,6 +609,11 @@ int Render::LoadSettingsFromCacheFile() {
|
||||
enable_autostart_ = config_center_->IsEnableAutostart();
|
||||
enable_daemon_ = config_center_->IsEnableDaemon();
|
||||
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
portable_service_prompt_suppressed_ =
|
||||
config_center_->IsPortableServicePromptSuppressed();
|
||||
portable_service_do_not_remind_ = portable_service_prompt_suppressed_;
|
||||
#endif
|
||||
|
||||
// File transfer save path
|
||||
{
|
||||
@@ -1714,26 +1719,6 @@ int Render::DrawServerWindow() {
|
||||
}
|
||||
|
||||
int Render::Run() {
|
||||
latest_version_info_ = CheckUpdate();
|
||||
if (!latest_version_info_.empty() &&
|
||||
latest_version_info_.contains("version") &&
|
||||
latest_version_info_["version"].is_string()) {
|
||||
latest_version_ = 'v' + latest_version_info_["version"].get<std::string>();
|
||||
if (latest_version_info_.contains("releaseNotes") &&
|
||||
latest_version_info_["releaseNotes"].is_string()) {
|
||||
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
|
||||
} else {
|
||||
release_notes_ = "";
|
||||
}
|
||||
update_available_ = IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
||||
if (update_available_) {
|
||||
show_update_notification_window_ = true;
|
||||
}
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
update_available_ = false;
|
||||
}
|
||||
|
||||
path_manager_ = std::make_unique<PathManager>("CrossDesk");
|
||||
if (path_manager_) {
|
||||
exec_log_path_ = path_manager_->GetLogPath().string();
|
||||
@@ -1762,6 +1747,41 @@ int Render::Run() {
|
||||
InitializeLogger();
|
||||
LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION);
|
||||
|
||||
latest_version_info_ = CheckUpdate();
|
||||
if (!latest_version_info_.empty()) {
|
||||
std::string version;
|
||||
if (latest_version_info_.contains("latest_version") &&
|
||||
latest_version_info_["latest_version"].is_string()) {
|
||||
version = latest_version_info_["latest_version"].get<std::string>();
|
||||
} else if (latest_version_info_.contains("version") &&
|
||||
latest_version_info_["version"].is_string()) {
|
||||
version = latest_version_info_["version"].get<std::string>();
|
||||
}
|
||||
|
||||
if (!version.empty()) {
|
||||
latest_version_ = 'v' + version;
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
}
|
||||
if (latest_version_info_.contains("releaseNotes") &&
|
||||
latest_version_info_["releaseNotes"].is_string()) {
|
||||
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
|
||||
} else {
|
||||
release_notes_ = "";
|
||||
}
|
||||
update_available_ =
|
||||
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
||||
LOG_INFO("Update check: current={}, latest={}, available={}",
|
||||
CROSSDESK_VERSION, latest_version_, update_available_);
|
||||
if (update_available_) {
|
||||
show_update_notification_window_ = true;
|
||||
}
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
update_available_ = false;
|
||||
LOG_WARN("Update check skipped: version.json is empty or missing latest_version");
|
||||
}
|
||||
|
||||
InitializeSettings();
|
||||
InitializeSDL();
|
||||
InitializeModules();
|
||||
|
||||
@@ -567,6 +567,9 @@ class Render {
|
||||
#if CROSSDESK_PORTABLE
|
||||
bool portable_service_prompt_checked_ = false;
|
||||
bool show_portable_service_install_window_ = false;
|
||||
bool show_portable_service_prompt_suppressed_window_ = false;
|
||||
bool portable_service_do_not_remind_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::atomic<PortableServiceInstallState> portable_service_install_state_{
|
||||
PortableServiceInstallState::idle};
|
||||
std::thread portable_service_install_thread_;
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
#include "render.h"
|
||||
#include "tinyfiledialogs.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::SettingWindow() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
float portable_y_padding = 0.0f;
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
portable_y_padding = 0.05f;
|
||||
#endif
|
||||
|
||||
if (show_settings_window_) {
|
||||
if (settings_window_pos_reset_) {
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
@@ -18,12 +27,14 @@ int Render::SettingWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f));
|
||||
ImVec2(io.DisplaySize.x * 0.315f,
|
||||
io.DisplaySize.y * (0.9f + portable_y_padding)));
|
||||
#else
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f));
|
||||
ImVec2(io.DisplaySize.x * 0.315f,
|
||||
io.DisplaySize.y * (0.85f + portable_y_padding)));
|
||||
#endif
|
||||
} else {
|
||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
||||
@@ -32,12 +43,14 @@ int Render::SettingWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f));
|
||||
ImVec2(io.DisplaySize.x * 0.42f,
|
||||
io.DisplaySize.y * (0.9f + portable_y_padding)));
|
||||
#else
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f));
|
||||
ImVec2(io.DisplaySize.x * 0.42f,
|
||||
io.DisplaySize.y * (0.85f + portable_y_padding)));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -73,23 +86,21 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
if (ImGui::BeginCombo(
|
||||
"##language",
|
||||
if (ImGui::BeginCombo("##language",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_button_value_)]
|
||||
.display_name
|
||||
.c_str())) {
|
||||
.display_name.c_str())) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
for (int i = 0; i < static_cast<int>(supported_languages.size());
|
||||
++i) {
|
||||
bool selected = (i == language_button_value_);
|
||||
if (ImGui::Selectable(
|
||||
supported_languages[i].display_name.c_str(), selected))
|
||||
if (ImGui::Selectable(supported_languages[i].display_name.c_str(),
|
||||
selected))
|
||||
language_button_value_ = i;
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
@@ -125,7 +136,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -158,7 +169,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -194,7 +205,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -228,7 +239,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_hardware_video_codec",
|
||||
@@ -249,7 +260,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_turn", &enable_turn_);
|
||||
@@ -268,7 +279,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_srtp", &enable_srtp_);
|
||||
@@ -289,7 +300,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
|
||||
@@ -308,7 +319,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
|
||||
@@ -327,7 +338,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
|
||||
@@ -359,7 +370,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_minimize_to_tray_",
|
||||
@@ -384,7 +395,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
}
|
||||
|
||||
std::string display_path =
|
||||
@@ -429,6 +440,80 @@ int Render::SettingWindow() {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s", localization::windows_service_settings_label
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.0f);
|
||||
} else if (ConfigCenter::LANGUAGE::ENGLISH == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.42f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
|
||||
}
|
||||
|
||||
const PortableServiceInstallState state =
|
||||
portable_service_install_state_.load(std::memory_order_acquire);
|
||||
const bool service_installed =
|
||||
IsCrossDeskServiceInstalled() ||
|
||||
state == PortableServiceInstallState::succeeded;
|
||||
if (service_installed) {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.9f);
|
||||
} else if (ConfigCenter::LANGUAGE::ENGLISH ==
|
||||
localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.32f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
|
||||
}
|
||||
ImGui::Text("%s", localization::windows_service_installed
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
} else {
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(localization::install_windows_service
|
||||
[localization_language_index_]
|
||||
.c_str())) {
|
||||
StartPortableWindowsServiceInstall();
|
||||
}
|
||||
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s", localization::installing_windows_service
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
} else if (state == PortableServiceInstallState::failed) {
|
||||
ImGui::SameLine();
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::failed[localization_language_index_].c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::PushTextWrapPos(title_bar_button_width_ * 10.0f);
|
||||
ImGui::TextWrapped("%s",
|
||||
localization::windows_service_install_failed
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f);
|
||||
} else {
|
||||
@@ -436,7 +521,7 @@ int Render::SettingWindow() {
|
||||
}
|
||||
|
||||
settings_items_offset +=
|
||||
settings_items_padding + title_bar_button_width_ * 0.3f;
|
||||
settings_items_padding + title_bar_button_width_ * 0.15f;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
@@ -464,8 +549,7 @@ int Render::SettingWindow() {
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
localization_language_index_)]
|
||||
.code
|
||||
.c_str());
|
||||
.code.c_str());
|
||||
|
||||
// Video quality
|
||||
if (video_quality_button_value_ == 0) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "localization.h"
|
||||
@@ -16,8 +17,7 @@ namespace {
|
||||
std::filesystem::path GetCurrentExecutablePath() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
DWORD length =
|
||||
GetModuleFileNameW(nullptr, buffer.data(),
|
||||
DWORD length = GetModuleFileNameW(nullptr, buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0) {
|
||||
return {};
|
||||
@@ -46,7 +46,8 @@ bool InstallServiceWithElevation() {
|
||||
if (!std::filesystem::exists(service_path) ||
|
||||
!std::filesystem::exists(helper_path)) {
|
||||
LOG_ERROR(
|
||||
"Portable service install failed: service binaries missing, service={}, "
|
||||
"Portable service install failed: service binaries missing, "
|
||||
"service={}, "
|
||||
"helper={}",
|
||||
service_path.string(), helper_path.string());
|
||||
return false;
|
||||
@@ -106,12 +107,17 @@ void Render::CheckPortableWindowsService() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (portable_service_prompt_suppressed_) {
|
||||
return;
|
||||
}
|
||||
|
||||
portable_service_install_state_.store(PortableServiceInstallState::idle,
|
||||
std::memory_order_relaxed);
|
||||
show_portable_service_install_window_ = true;
|
||||
}
|
||||
|
||||
void Render::StartPortableWindowsServiceInstall() {
|
||||
portable_service_do_not_remind_ = false;
|
||||
PortableServiceInstallState expected = PortableServiceInstallState::idle;
|
||||
if (!portable_service_install_state_.compare_exchange_strong(
|
||||
expected, PortableServiceInstallState::installing,
|
||||
@@ -140,18 +146,82 @@ void Render::JoinPortableWindowsServiceInstallThread() {
|
||||
}
|
||||
|
||||
int Render::PortableServiceInstallWindow() {
|
||||
if (!show_portable_service_install_window_) {
|
||||
if (!show_portable_service_install_window_ &&
|
||||
!show_portable_service_prompt_suppressed_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
const float window_width = title_bar_button_width_ * 12.0f;
|
||||
const float window_height = title_bar_button_width_ * 4.0f;
|
||||
const float window_width =
|
||||
(std::min)(viewport->WorkSize.x * 0.6f, title_bar_button_width_ * 18.0f);
|
||||
const float window_height =
|
||||
(std::min)(viewport->WorkSize.y * 0.5f, title_bar_button_width_ * 8.0f);
|
||||
|
||||
if (show_portable_service_prompt_suppressed_window_) {
|
||||
const float notice_width = window_width;
|
||||
const float notice_height = (std::min)(viewport->WorkSize.y * 0.35f,
|
||||
title_bar_button_width_ * 4.6f);
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) /
|
||||
2.0f,
|
||||
(viewport->WorkSize.y - viewport->WorkPos.y - window_height) /
|
||||
2.0f),
|
||||
ImVec2(
|
||||
viewport->WorkPos.x + (viewport->WorkSize.x - notice_width) / 2.0f,
|
||||
viewport->WorkPos.y +
|
||||
(viewport->WorkSize.y - notice_height) / 2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(notice_width, notice_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
|
||||
ImGui::Begin(
|
||||
localization::notification[localization_language_index_].c_str(),
|
||||
nullptr,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoTitleBar);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::SetWindowFontScale(0.55f);
|
||||
ImGui::SetCursorPosX(notice_width * 0.08f);
|
||||
ImGui::Text(
|
||||
"%s", localization::notification[localization_language_index_].c_str());
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::SetCursorPosX(notice_width * 0.06f);
|
||||
ImGui::SetCursorPosY(notice_height * 0.28f);
|
||||
ImGui::PushTextWrapPos(notice_width * 0.88f);
|
||||
ImGui::TextWrapped("%s",
|
||||
localization::windows_service_prompt_suppressed_message
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
const std::string ok_label = localization::ok[localization_language_index_];
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float ok_width =
|
||||
ImGui::CalcTextSize(ok_label.c_str()).x + style.FramePadding.x * 2.0f;
|
||||
ImGui::SetCursorPosX((notice_width - ok_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(notice_height * 0.75f);
|
||||
if (ImGui::Button(ok_label.c_str())) {
|
||||
show_portable_service_prompt_suppressed_window_ = false;
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if (!show_portable_service_install_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(
|
||||
viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f,
|
||||
viewport->WorkPos.y + (viewport->WorkSize.y - window_height) / 2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||
ImGuiCond_Always);
|
||||
@@ -187,13 +257,11 @@ int Render::PortableServiceInstallWindow() {
|
||||
localization::installing_windows_service[localization_language_index_]
|
||||
.c_str();
|
||||
if (state == PortableServiceInstallState::succeeded) {
|
||||
status_text =
|
||||
localization::windows_service_install_success
|
||||
status_text = localization::windows_service_install_success
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
} else if (state == PortableServiceInstallState::failed) {
|
||||
status_text =
|
||||
localization::windows_service_install_failed
|
||||
status_text = localization::windows_service_install_failed
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
}
|
||||
@@ -203,7 +271,7 @@ int Render::PortableServiceInstallWindow() {
|
||||
ImGui::SetCursorPosX(window_width * 0.04f);
|
||||
ImGui::SetCursorPosY(window_height * 0.22f);
|
||||
ImGui::BeginChild("PortableServiceInstallContent",
|
||||
ImVec2(window_width * 0.92f, window_height * 0.5f),
|
||||
ImVec2(window_width * 0.92f, window_height * 0.45f),
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const float wrap_pos = ImGui::GetContentRegionAvail().x;
|
||||
@@ -220,7 +288,15 @@ int Render::PortableServiceInstallWindow() {
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const float button_y = window_height * 0.76f;
|
||||
ImGui::SetCursorPosX(window_width * 0.08f);
|
||||
ImGui::SetCursorPosY(window_height * 0.71f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
|
||||
ImGui::Checkbox(
|
||||
localization::do_not_remind_again[localization_language_index_].c_str(),
|
||||
&portable_service_do_not_remind_);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
const float button_y = window_height * 0.84f;
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const auto default_button_width = [&style](const std::string& label) {
|
||||
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
|
||||
@@ -259,6 +335,11 @@ int Render::PortableServiceInstallWindow() {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(cancel_label.c_str())) {
|
||||
if (portable_service_do_not_remind_) {
|
||||
portable_service_prompt_suppressed_ = true;
|
||||
config_center_->SetPortableServicePromptSuppressed(true);
|
||||
show_portable_service_prompt_suppressed_window_ = true;
|
||||
}
|
||||
show_portable_service_install_window_ = false;
|
||||
}
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
|
||||
@@ -8,7 +8,15 @@
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -16,12 +24,296 @@
|
||||
namespace crossdesk {
|
||||
|
||||
static std::string latest_release_date_ = "";
|
||||
static bool latest_patch_available_ = false;
|
||||
static int latest_patch_ = 0;
|
||||
|
||||
std::vector<int> SplitVersion(const std::string& ver);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxInlinePatchDigits = 4;
|
||||
|
||||
struct ParsedVersion {
|
||||
std::vector<int> numbers;
|
||||
std::string date;
|
||||
bool has_patch = false;
|
||||
int patch = 0;
|
||||
};
|
||||
|
||||
bool IsDigit(char c) {
|
||||
return std::isdigit(static_cast<unsigned char>(c)) != 0;
|
||||
}
|
||||
|
||||
bool IsAlphaNumeric(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0;
|
||||
}
|
||||
|
||||
bool IsAllDigits(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char c : value) {
|
||||
if (!IsDigit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseNonNegativeInt(const std::string& value, int* result) {
|
||||
if (!IsAllDigits(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const long long parsed = std::stoll(value);
|
||||
if (parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
*result = static_cast<int>(parsed);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryParseInlinePatch(const std::string& value, int* result) {
|
||||
if (value.size() > kMaxInlinePatchDigits) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseNonNegativeInt(value, result);
|
||||
}
|
||||
|
||||
size_t FindNumericStart(const std::string& version) {
|
||||
size_t start = 0;
|
||||
while (start < version.size() && !IsDigit(version[start])) {
|
||||
start++;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
size_t FindNumericEnd(const std::string& version, size_t start) {
|
||||
size_t end = start;
|
||||
while (end < version.size() &&
|
||||
(IsDigit(version[end]) || version[end] == '.')) {
|
||||
end++;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
bool HasDigitBoundary(const std::string& value, size_t pos, size_t len) {
|
||||
const bool before_ok = pos == 0 || !IsDigit(value[pos - 1]);
|
||||
const size_t end = pos + len;
|
||||
const bool after_ok = end >= value.size() || !IsDigit(value[end]);
|
||||
return before_ok && after_ok;
|
||||
}
|
||||
|
||||
bool IsCompactDateAt(const std::string& value, size_t pos) {
|
||||
if (pos + 8 > value.size() || !HasDigitBoundary(value, pos, 8)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 8; ++i) {
|
||||
if (!IsDigit(value[pos + i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CompactDateToIso(const std::string& compact_date) {
|
||||
return compact_date.substr(0, 4) + "-" + compact_date.substr(4, 2) + "-" +
|
||||
compact_date.substr(6, 2);
|
||||
}
|
||||
|
||||
bool ExtractDateFromText(const std::string& value,
|
||||
std::string* date,
|
||||
size_t* date_end) {
|
||||
for (size_t i = 0; i < value.size(); ++i) {
|
||||
if (IsCompactDateAt(value, i)) {
|
||||
*date = CompactDateToIso(value.substr(i, 8));
|
||||
*date_end = i + 8;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ParsedVersion ParseVersion(const std::string& version) {
|
||||
const size_t numeric_start = FindNumericStart(version);
|
||||
const size_t numeric_end = FindNumericEnd(version, numeric_start);
|
||||
|
||||
ParsedVersion parsed;
|
||||
parsed.numbers = SplitVersion(version.substr(numeric_start,
|
||||
numeric_end - numeric_start));
|
||||
|
||||
const std::string suffix = version.substr(numeric_end);
|
||||
size_t pos = 0;
|
||||
while (pos < suffix.size()) {
|
||||
while (pos < suffix.size() && !IsAlphaNumeric(suffix[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
const size_t token_start = pos;
|
||||
while (pos < suffix.size() && IsAlphaNumeric(suffix[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (token_start == pos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string token = suffix.substr(token_start, pos - token_start);
|
||||
if (parsed.date.empty() && IsCompactDateAt(token, 0)) {
|
||||
parsed.date = CompactDateToIso(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
int patch = 0;
|
||||
if (!parsed.has_patch && TryParseInlinePatch(token, &patch)) {
|
||||
parsed.has_patch = true;
|
||||
parsed.patch = patch;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
int CompareNumericVersion(const std::vector<int>& current,
|
||||
const std::vector<int>& latest) {
|
||||
std::vector<int> current_parts = current;
|
||||
std::vector<int> latest_parts = latest;
|
||||
const size_t len = std::max(current_parts.size(), latest_parts.size());
|
||||
current_parts.resize(len, 0);
|
||||
latest_parts.resize(len, 0);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (latest_parts[i] > current_parts[i]) {
|
||||
return 1;
|
||||
}
|
||||
if (latest_parts[i] < current_parts[i]) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ResetLatestMetadata() {
|
||||
latest_release_date_ = "";
|
||||
latest_patch_available_ = false;
|
||||
latest_patch_ = 0;
|
||||
}
|
||||
|
||||
bool ReadPatchField(const nlohmann::json& json, int* patch) {
|
||||
if (!json.contains("patch")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& patch_value = json["patch"];
|
||||
if (patch_value.is_number_integer()) {
|
||||
const long long parsed = patch_value.get<long long>();
|
||||
if (parsed < 0 || parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
*patch = static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (patch_value.is_string()) {
|
||||
return TryParseNonNegativeInt(patch_value.get<std::string>(), patch);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void LogHttpError(const httplib::Result& result) {
|
||||
LOG_WARN("Failed to fetch version.json: error={}, message={}",
|
||||
static_cast<int>(result.error()), httplib::to_string(result.error()));
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
LOG_WARN("version.json SSL error={}, OpenSSL error={}", result.ssl_error(),
|
||||
result.ssl_openssl_error());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
|
||||
bool PathExists(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
return std::filesystem::exists(path, ec);
|
||||
}
|
||||
|
||||
std::string GetEnvPathIfExists(const char* key) {
|
||||
const char* value = std::getenv(key);
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string path = value;
|
||||
return PathExists(path) ? path : "";
|
||||
}
|
||||
|
||||
std::string FindFirstExistingPath(
|
||||
const std::vector<std::string>& candidates) {
|
||||
for (const auto& candidate : candidates) {
|
||||
if (PathExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void ConfigureLinuxCaCerts(httplib::Client* cli) {
|
||||
const std::string ca_file = [&]() {
|
||||
const std::string env_path = GetEnvPathIfExists("SSL_CERT_FILE");
|
||||
if (!env_path.empty()) {
|
||||
return env_path;
|
||||
}
|
||||
|
||||
return FindFirstExistingPath({
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/ssl/cert.pem",
|
||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
||||
});
|
||||
}();
|
||||
|
||||
const std::string ca_dir = [&]() {
|
||||
const std::string env_path = GetEnvPathIfExists("SSL_CERT_DIR");
|
||||
if (!env_path.empty()) {
|
||||
return env_path;
|
||||
}
|
||||
|
||||
return FindFirstExistingPath({
|
||||
"/etc/ssl/certs",
|
||||
"/etc/pki/tls/certs",
|
||||
"/etc/openssl/certs",
|
||||
});
|
||||
}();
|
||||
|
||||
if (ca_file.empty() && ca_dir.empty()) {
|
||||
LOG_WARN("No Linux CA bundle found for version.json request; relying on OpenSSL defaults");
|
||||
return;
|
||||
}
|
||||
|
||||
cli->set_ca_cert_path(ca_file, ca_dir);
|
||||
LOG_INFO("Configured version.json TLS CA bundle: file={}, dir={}",
|
||||
ca_file.empty() ? "<none>" : ca_file,
|
||||
ca_dir.empty() ? "<none>" : ca_dir);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string ExtractNumericPart(const std::string& ver) {
|
||||
size_t start = 0;
|
||||
while (start < ver.size() && !std::isdigit(ver[start])) start++;
|
||||
size_t end = start;
|
||||
while (end < ver.size() && (std::isdigit(ver[end]) || ver[end] == '.')) end++;
|
||||
const size_t start = FindNumericStart(ver);
|
||||
const size_t end = FindNumericEnd(ver, start);
|
||||
return ver.substr(start, end - start);
|
||||
}
|
||||
|
||||
@@ -42,25 +334,13 @@ std::vector<int> SplitVersion(const std::string& ver) {
|
||||
// extract date from version string (format: v1.2.3-20251113-abc
|
||||
// or 1.2.3-20251113-abc)
|
||||
std::string ExtractDateFromVersion(const std::string& version) {
|
||||
size_t dash1 = version.find('-');
|
||||
if (dash1 != std::string::npos) {
|
||||
size_t dash2 = version.find('-', dash1 + 1);
|
||||
if (dash2 != std::string::npos) {
|
||||
std::string date_part = version.substr(dash1 + 1, dash2 - dash1 - 1);
|
||||
|
||||
bool is_date = true;
|
||||
for (char c : date_part) {
|
||||
if (!std::isdigit(c)) {
|
||||
is_date = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_date) {
|
||||
// convert YYYYMMDD to YYYY-MM-DD
|
||||
return date_part.substr(0, 4) + "-" + date_part.substr(4, 2) + "-" +
|
||||
date_part.substr(6, 2);
|
||||
}
|
||||
}
|
||||
const size_t numeric_start = FindNumericStart(version);
|
||||
const size_t numeric_end = FindNumericEnd(version, numeric_start);
|
||||
const std::string suffix = version.substr(numeric_end);
|
||||
std::string date;
|
||||
size_t date_end = 0;
|
||||
if (ExtractDateFromText(suffix, &date, &date_end)) {
|
||||
return date;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -73,55 +353,41 @@ bool IsNewerDate(const std::string& date1, const std::string& date2) {
|
||||
}
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest) {
|
||||
auto v1 = SplitVersion(ExtractNumericPart(current));
|
||||
auto v2 = SplitVersion(ExtractNumericPart(latest));
|
||||
|
||||
size_t len = std::max(v1.size(), v2.size());
|
||||
v1.resize(len, 0);
|
||||
v2.resize(len, 0);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (v2[i] > v1[i]) return true;
|
||||
if (v2[i] < v1[i]) return false;
|
||||
}
|
||||
|
||||
// if versions are equal, compare by release date
|
||||
if (!latest_release_date_.empty()) {
|
||||
// try to extract date from current version string
|
||||
std::string current_date = ExtractDateFromVersion(current);
|
||||
if (!current_date.empty()) {
|
||||
return IsNewerDate(current_date, latest_release_date_);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return IsNewerVersionWithMetadata(
|
||||
current, latest, latest_release_date_,
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
}
|
||||
|
||||
bool IsNewerVersionWithDate(const std::string& current_version,
|
||||
const std::string& current_date,
|
||||
const std::string& latest_version,
|
||||
const std::string& latest_date) {
|
||||
// compare versions
|
||||
auto v1 = SplitVersion(ExtractNumericPart(current_version));
|
||||
auto v2 = SplitVersion(ExtractNumericPart(latest_version));
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
const std::string& latest,
|
||||
const std::string& latest_date,
|
||||
int latest_patch) {
|
||||
(void)latest_date;
|
||||
|
||||
size_t len = std::max(v1.size(), v2.size());
|
||||
v1.resize(len, 0);
|
||||
v2.resize(len, 0);
|
||||
const ParsedVersion current_version = ParseVersion(current);
|
||||
const ParsedVersion latest_version = ParseVersion(latest);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (v2[i] > v1[i]) return true;
|
||||
if (v2[i] < v1[i]) return false;
|
||||
const int numeric_compare =
|
||||
CompareNumericVersion(current_version.numbers, latest_version.numbers);
|
||||
if (numeric_compare > 0) {
|
||||
return true;
|
||||
}
|
||||
if (numeric_compare < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if versions are equal, compare by release date
|
||||
if (!current_date.empty() && !latest_date.empty()) {
|
||||
return IsNewerDate(current_date, latest_date);
|
||||
const bool metadata_has_patch = latest_patch >= 0;
|
||||
const bool latest_has_patch = metadata_has_patch || latest_version.has_patch;
|
||||
if (latest_has_patch || current_version.has_patch) {
|
||||
const int resolved_latest_patch =
|
||||
metadata_has_patch ? latest_patch
|
||||
: (latest_version.has_patch ? latest_version.patch
|
||||
: 0);
|
||||
const int resolved_current_patch =
|
||||
current_version.has_patch ? current_version.patch : 0;
|
||||
return resolved_latest_patch > resolved_current_patch;
|
||||
}
|
||||
|
||||
// if dates are not available, versions are equal
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -130,8 +396,14 @@ nlohmann::json CheckUpdate() {
|
||||
|
||||
cli.set_connection_timeout(5);
|
||||
cli.set_read_timeout(5);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
if (auto res = cli.Get("/version.json")) {
|
||||
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
|
||||
ConfigureLinuxCaCerts(&cli);
|
||||
#endif
|
||||
|
||||
auto res = cli.Get("/version.json");
|
||||
if (res) {
|
||||
if (res->status == 200) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(res->body);
|
||||
@@ -140,17 +412,26 @@ nlohmann::json CheckUpdate() {
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
}
|
||||
latest_patch_ = 0;
|
||||
latest_patch_available_ = ReadPatchField(j, &latest_patch_);
|
||||
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
|
||||
j.value("latest_version", j.value("version", "")),
|
||||
j.value("releaseDate", ""),
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
return j;
|
||||
} catch (std::exception&) {
|
||||
latest_release_date_ = "";
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARN("Failed to parse version.json: {}", e.what());
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
LogHttpError(res);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ nlohmann::json CheckUpdate();
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest);
|
||||
|
||||
// Pass latest_patch < 0 when patch metadata is unavailable.
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
const std::string& latest,
|
||||
const std::string& latest_date,
|
||||
int latest_patch);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -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;
|
||||
}
|
||||
@@ -82,16 +82,27 @@ int main() {
|
||||
}
|
||||
|
||||
const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc");
|
||||
const std::string portable_rc =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_portable.rc");
|
||||
const std::string manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
const std::string debug_manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
const std::string portable_manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_portable.manifest");
|
||||
const std::string targets = ReadFile(repo_root / "xmake/targets.lua");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST");
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc,
|
||||
"crossdesk_portable.manifest");
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc, "RT_MANIFEST");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets,
|
||||
"scripts/windows/crossdesk_portable.rc");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets, "CROSSDESK_PORTABLE");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"level=\"requireAdministrator\"");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
@@ -108,10 +119,22 @@ int main() {
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"level=\"asInvoker\"");
|
||||
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"level=\"requireAdministrator\"");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2005/WindowsSettings");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
#ifdef _WIN32
|
||||
ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_portable.manifest");
|
||||
#endif
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
+24
-2
@@ -3,6 +3,11 @@ function setup_targets()
|
||||
|
||||
includes("submodules", "thirdparty")
|
||||
|
||||
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
|
||||
if is_config("CROSSDESK_PORTABLE", true) then
|
||||
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
|
||||
end
|
||||
|
||||
target("rd_log")
|
||||
set_kind("object")
|
||||
add_packages("spdlog")
|
||||
@@ -65,6 +70,19 @@ function setup_targets()
|
||||
set_default(false)
|
||||
add_files("tests/display_popup_hover_state_test.cpp")
|
||||
|
||||
target("version_checker_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_packages("cpp-httplib")
|
||||
add_deps("rd_log")
|
||||
add_includedirs("src/version_checker")
|
||||
add_files("tests/version_checker_test.cpp",
|
||||
"src/version_checker/version_checker.cpp")
|
||||
if is_os("macosx") then
|
||||
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
|
||||
add_frameworks("Security", "CoreFoundation")
|
||||
end
|
||||
|
||||
target("screen_capturer")
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "common")
|
||||
@@ -164,6 +182,10 @@ function setup_targets()
|
||||
add_deps("rd_log")
|
||||
add_files("src/version_checker/*.cpp")
|
||||
add_includedirs("src/version_checker", {public = true})
|
||||
if is_os("macosx") then
|
||||
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
|
||||
add_frameworks("Security", "CoreFoundation")
|
||||
end
|
||||
|
||||
target("tools")
|
||||
set_kind("object")
|
||||
@@ -223,7 +245,7 @@ function setup_targets()
|
||||
add_deps("rd_log", "path_manager")
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||
add_files("src/service/windows/session_helper_main.cpp")
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
add_files(crossdesk_windows_resource)
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
end
|
||||
|
||||
@@ -237,6 +259,6 @@ function setup_targets()
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
add_files(crossdesk_windows_resource)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user