mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-22 04:23:06 +08:00
Compare commits
10
Commits
auto-update
...
deskhub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ac6e153c8 | ||
|
|
dd5c572852 | ||
|
|
33d4199f7c | ||
|
|
a6f4df405b | ||
|
|
97960f3a6b | ||
|
|
53c657aa55 | ||
|
|
941de898fb | ||
|
|
6abcafc205 | ||
|
|
171e559b0d | ||
|
|
41f1e29583 |
@@ -50,7 +50,7 @@ def sign_update(payload, private_key, expected_public_key):
|
|||||||
return {"payload": payload, "signature": signature.hex()}
|
return {"payload": payload, "signature": signature.hex()}
|
||||||
|
|
||||||
|
|
||||||
def version_info(version, fallback_date=None):
|
def version_fields(version):
|
||||||
version = version.removeprefix("v")
|
version = version.removeprefix("v")
|
||||||
base = r"[0-9]+(?:\.[0-9]+){1,3}"
|
base = r"[0-9]+(?:\.[0-9]+){1,3}"
|
||||||
for pattern in (
|
for pattern in (
|
||||||
@@ -60,18 +60,41 @@ def version_info(version, fallback_date=None):
|
|||||||
):
|
):
|
||||||
match = re.fullmatch(pattern, version)
|
match = re.fullmatch(pattern, version)
|
||||||
if match:
|
if match:
|
||||||
fields = match.groupdict()
|
return match.groupdict()
|
||||||
date = fields.get("date")
|
|
||||||
release_date = (datetime.datetime.strptime(date, "%Y%m%d").date().isoformat()
|
|
||||||
if date else fallback_date)
|
|
||||||
if not release_date:
|
|
||||||
raise ValueError(f"Release date is missing for {version}")
|
|
||||||
datetime.date.fromisoformat(release_date)
|
|
||||||
return {"latest_version": version, "version": version,
|
|
||||||
"releaseDate": release_date, "patch": int(fields.get("patch") or 0)}
|
|
||||||
raise ValueError(f"Invalid release version: {version}")
|
raise ValueError(f"Invalid release version: {version}")
|
||||||
|
|
||||||
|
|
||||||
|
def version_info(version, fallback_date=None):
|
||||||
|
fields = version_fields(version)
|
||||||
|
version = version.removeprefix("v")
|
||||||
|
date = fields.get("date")
|
||||||
|
release_date = (datetime.datetime.strptime(date, "%Y%m%d").date().isoformat()
|
||||||
|
if date else fallback_date)
|
||||||
|
if not release_date:
|
||||||
|
raise ValueError(f"Release date is missing for {version}")
|
||||||
|
datetime.date.fromisoformat(release_date)
|
||||||
|
return {"latest_version": version, "version": version,
|
||||||
|
"releaseDate": release_date, "patch": int(fields.get("patch") or 0)}
|
||||||
|
|
||||||
|
|
||||||
|
def release_order(metadata):
|
||||||
|
"""Match client ordering: zero-padded numeric components, then patch, never date."""
|
||||||
|
version = metadata.get("latest_version") or metadata.get("version")
|
||||||
|
if not version:
|
||||||
|
return None
|
||||||
|
fields = version_fields(version)
|
||||||
|
numbers = tuple(int(part) for part in fields["base"].split("."))
|
||||||
|
inline_patch = fields.get("patch") or "0"
|
||||||
|
patch = int(inline_patch) if len(inline_patch) <= 4 else 0
|
||||||
|
# Legacy manifests may store the effective patch outside the version string.
|
||||||
|
explicit_patch = metadata.get("patch")
|
||||||
|
if isinstance(explicit_patch, str) and re.fullmatch(r"[0-9]+", explicit_patch):
|
||||||
|
explicit_patch = int(explicit_patch)
|
||||||
|
if type(explicit_patch) is int and 0 <= explicit_patch <= 2147483647:
|
||||||
|
patch = explicit_patch
|
||||||
|
return numbers + (0,) * (4 - len(numbers)) + (patch,)
|
||||||
|
|
||||||
|
|
||||||
def artifact_downloads(directory):
|
def artifact_downloads(directory):
|
||||||
downloads = {}
|
downloads = {}
|
||||||
private_key = os.environ.get("CROSSDESK_UPDATE_PRIVATE_KEY", "")
|
private_key = os.environ.get("CROSSDESK_UPDATE_PRIVATE_KEY", "")
|
||||||
@@ -129,10 +152,34 @@ def merge_release(current, tag, name, notes, downloads, published_date=None):
|
|||||||
if not tag.startswith("v"):
|
if not tag.startswith("v"):
|
||||||
raise ValueError(f"Expected a version tag, got {tag!r}")
|
raise ValueError(f"Expected a version tag, got {tag!r}")
|
||||||
metadata = version_info(tag, published_date)
|
metadata = version_info(tag, published_date)
|
||||||
|
# Clients require the full artifact version to match the advertised
|
||||||
|
# desktop version, including its date and patch spelling.
|
||||||
|
for platform, download in downloads.items():
|
||||||
|
if (platform.startswith(("windows-", "macos-", "linux-"))
|
||||||
|
and download["version"] != metadata["version"]):
|
||||||
|
raise ValueError(
|
||||||
|
f"Desktop release {tag} expects version {metadata['version']}, "
|
||||||
|
f"but {platform} artifact {download['filename']} "
|
||||||
|
f"has version {download['version']}")
|
||||||
|
current_order = release_order(current)
|
||||||
|
if current_order is not None and release_order(metadata) < current_order:
|
||||||
|
current_version = current.get("latest_version") or current.get("version")
|
||||||
|
print(f"Skipping {tag}: older than current desktop release {current_version}")
|
||||||
|
return dict(current)
|
||||||
|
|
||||||
|
accepted_downloads = {}
|
||||||
|
for platform, download in downloads.items():
|
||||||
|
previous_order = release_order(current["downloads"].get(platform, {}))
|
||||||
|
if previous_order is not None and release_order(download) < previous_order:
|
||||||
|
print(f"Skipping {tag} download for {platform}: a newer version is already published")
|
||||||
|
continue
|
||||||
|
accepted_downloads[platform] = download
|
||||||
|
if mobile and not accepted_downloads:
|
||||||
|
return dict(current)
|
||||||
|
|
||||||
result = dict(current)
|
result = dict(current)
|
||||||
result.pop("platforms", None)
|
result.pop("platforms", None)
|
||||||
result["downloads"] = {**current["downloads"], **downloads}
|
result["downloads"] = {**current["downloads"], **accepted_downloads}
|
||||||
if not mobile:
|
if not mobile:
|
||||||
result.update({**metadata, "releaseName": name, "releaseNotes": notes, "tagName": tag})
|
result.update({**metadata, "releaseName": name, "releaseNotes": notes, "tagName": tag})
|
||||||
return result
|
return result
|
||||||
@@ -159,11 +206,16 @@ def main():
|
|||||||
parser.error("Supply --release-json or --tag, --name and --notes")
|
parser.error("Supply --release-json or --tag, --name and --notes")
|
||||||
tag, name, notes = args.tag, args.name, args.notes.read_text()
|
tag, name, notes = args.tag, args.name, args.notes.read_text()
|
||||||
published_date = None
|
published_date = None
|
||||||
current = json.loads(args.current.read_text())
|
current_text = args.current.read_text()
|
||||||
|
current = json.loads(current_text)
|
||||||
downloads = artifact_downloads(args.artifacts)
|
downloads = artifact_downloads(args.artifacts)
|
||||||
result = merge_release(current, tag, name, notes, downloads, published_date)
|
result = merge_release(current, tag, name, notes, downloads, published_date)
|
||||||
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
if result == current:
|
||||||
print(f"Updated {tag}: {', '.join(downloads)}")
|
args.output.write_text(current_text)
|
||||||
|
print(f"No changes to version.json for {tag}")
|
||||||
|
else:
|
||||||
|
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
print(f"Updated version.json for {tag}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+73
-219
@@ -36,9 +36,68 @@ env:
|
|||||||
CROSSDESK_DESKTOP_VERSION: ${{ inputs.desktop_version }}
|
CROSSDESK_DESKTOP_VERSION: ${{ inputs.desktop_version }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
prepare-version:
|
||||||
|
name: Resolve desktop version
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
# Resolve the date once for every desktop build and the release job.
|
||||||
|
steps:
|
||||||
|
- name: Resolve desktop version
|
||||||
|
id: version
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PATCH_NUMBER: ${{ inputs.patch || '0' }}
|
||||||
|
run: |
|
||||||
|
VERSION_REF="${GITHUB_REF##*/}"
|
||||||
|
if [[ "${GITHUB_REF}" == refs/heads/* ]]; then
|
||||||
|
VERSION_REF="${CROSSDESK_DESKTOP_VERSION:-0.0.0}"
|
||||||
|
if [[ ! "${VERSION_REF}" =~ ^v?[0-9]+(\.[0-9]+){1,3}$ ]]; then
|
||||||
|
echo "::error::desktop_version must be a numeric version such as 1.7.2"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [[ -n "${CROSSDESK_DESKTOP_VERSION}" ]]; then
|
||||||
|
echo "::error::desktop_version is only supported for branch builds; tags determine release versions"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VERSION_BASE="${VERSION_REF#v}"
|
||||||
|
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||||
|
|
||||||
|
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=${VERSION_NUM}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Desktop version: ${VERSION_NUM}"
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
name: Build Linux (${{ matrix.arch }})
|
name: Build Linux (${{ matrix.arch }})
|
||||||
|
needs: prepare-version
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
|
env:
|
||||||
|
LEGAL_VERSION: ${{ needs.prepare-version.outputs.version }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
@@ -58,69 +117,6 @@ jobs:
|
|||||||
options: --user root
|
options: --user root
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Extract version number
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
VERSION_REF="${GITHUB_REF##*/}"
|
|
||||||
if [[ "${GITHUB_REF}" == refs/heads/* ]]; then
|
|
||||||
VERSION_REF="${CROSSDESK_DESKTOP_VERSION:-0.0.0}"
|
|
||||||
if [[ ! "${VERSION_REF}" =~ ^v?[0-9]+(\.[0-9]+){1,3}$ ]]; then
|
|
||||||
echo "::error::desktop_version must be a numeric version such as 1.7.2"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elif [[ -n "${CROSSDESK_DESKTOP_VERSION}" ]]; then
|
|
||||||
echo "::error::desktop_version is only supported for branch builds; tags determine release versions"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
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: |
|
|
||||||
BUILD_DATE="${BUILD_DATE_OVERRIDE}"
|
|
||||||
if [[ -z "${BUILD_DATE}" ]]; then
|
|
||||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
|
||||||
fi
|
|
||||||
|
|
||||||
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_BASE}-${BUILD_DATE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
|
|
||||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
@@ -218,8 +214,10 @@ jobs:
|
|||||||
# macOS
|
# macOS
|
||||||
build-macos:
|
build-macos:
|
||||||
name: Build on macOS
|
name: Build on macOS
|
||||||
|
needs: prepare-version
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
env:
|
env:
|
||||||
|
VERSION_NUM: ${{ needs.prepare-version.outputs.version }}
|
||||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||||
EXPECTED_MACOS_SDK: "26.5"
|
EXPECTED_MACOS_SDK: "26.5"
|
||||||
strategy:
|
strategy:
|
||||||
@@ -239,56 +237,6 @@ jobs:
|
|||||||
package_script: ./apps/desktop/scripts/macos/pkg_arm64.sh
|
package_script: ./apps/desktop/scripts/macos/pkg_arm64.sh
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Extract version number
|
|
||||||
id: version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
VERSION_REF="${GITHUB_REF##*/}"
|
|
||||||
if [[ "${GITHUB_REF}" == refs/heads/* ]]; then
|
|
||||||
VERSION_REF="${CROSSDESK_DESKTOP_VERSION:-0.0.0}"
|
|
||||||
if [[ ! "${VERSION_REF}" =~ ^v?[0-9]+(\.[0-9]+){1,3}$ ]]; then
|
|
||||||
echo "::error::desktop_version must be a numeric version such as 1.7.2"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elif [[ -n "${CROSSDESK_DESKTOP_VERSION}" ]]; then
|
|
||||||
echo "::error::desktop_version is only supported for branch builds; tags determine release versions"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
VERSION_BASE="${VERSION_REF#v}"
|
|
||||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
|
||||||
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: Select Xcode 26.6
|
- name: Select Xcode 26.6
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -380,62 +328,12 @@ jobs:
|
|||||||
# Windows
|
# Windows
|
||||||
build-windows-x64:
|
build-windows-x64:
|
||||||
name: Build on Windows x64
|
name: Build on Windows x64
|
||||||
|
needs: prepare-version
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
env:
|
env:
|
||||||
|
VERSION_NUM: ${{ needs.prepare-version.outputs.version }}
|
||||||
XMAKE_GLOBALDIR: D:\xmake_global
|
XMAKE_GLOBALDIR: D:\xmake_global
|
||||||
steps:
|
steps:
|
||||||
- name: Extract version number
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$ref = $env:GITHUB_REF
|
|
||||||
$version = $ref -replace '^refs/(tags|heads)/', ''
|
|
||||||
if ($ref.StartsWith("refs/heads/")) {
|
|
||||||
$version = if ([string]::IsNullOrEmpty($env:CROSSDESK_DESKTOP_VERSION)) {
|
|
||||||
"0.0.0"
|
|
||||||
} else {
|
|
||||||
$env:CROSSDESK_DESKTOP_VERSION
|
|
||||||
}
|
|
||||||
if ($version -notmatch '^v?[0-9]+(\.[0-9]+){1,3}\z') {
|
|
||||||
throw "desktop_version must be a numeric version such as 1.7.2"
|
|
||||||
}
|
|
||||||
} elseif (![string]::IsNullOrEmpty($env:CROSSDESK_DESKTOP_VERSION)) {
|
|
||||||
throw "desktop_version is only supported for branch builds; tags determine release versions"
|
|
||||||
}
|
|
||||||
$version = $version -replace '^v', ''
|
|
||||||
$version = $version -replace '/', '-'
|
|
||||||
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
|
|
||||||
$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
|
- name: Cache xmake dependencies
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
@@ -636,7 +534,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Publish Release
|
name: Publish Release
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: [build-linux, build-macos, build-ios, build-android, sign-android, build-windows-x64]
|
needs: [prepare-version, build-linux, build-macos, build-ios, build-android, sign-android, build-windows-x64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
concurrency:
|
concurrency:
|
||||||
group: publish-release-metadata
|
group: publish-release-metadata
|
||||||
@@ -651,50 +549,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
- name: Extract version number
|
|
||||||
id: version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
VERSION_REF="${GITHUB_REF##*/}"
|
|
||||||
VERSION_BASE="${VERSION_REF#v}"
|
|
||||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
|
||||||
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}"
|
|
||||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
|
|
||||||
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $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: Prepare release notes
|
- name: Prepare release notes
|
||||||
run: |
|
run: |
|
||||||
cat > "$RUNNER_TEMP/release-notes.md" << EOF
|
cat > "$RUNNER_TEMP/release-notes.md" << EOF
|
||||||
@@ -724,18 +578,18 @@ jobs:
|
|||||||
- name: Rename artifacts
|
- name: Rename artifacts
|
||||||
run: |
|
run: |
|
||||||
mkdir -p release
|
mkdir -p release
|
||||||
cp artifacts/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
cp artifacts/crossdesk-macos-x64-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-macos-x64-${{ needs.prepare-version.outputs.version }}.pkg
|
||||||
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
cp artifacts/crossdesk-macos-arm64-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-macos-arm64-${{ needs.prepare-version.outputs.version }}.pkg
|
||||||
for ARCH in amd64 arm64; do
|
for ARCH in amd64 arm64; do
|
||||||
for PACKAGE in crossdesk crossdesk-virtual-desktop-xfce; do
|
for PACKAGE in crossdesk crossdesk-virtual-desktop-xfce; do
|
||||||
FILE="${PACKAGE}-linux-${ARCH}-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
|
FILE="${PACKAGE}-linux-${ARCH}-${{ needs.prepare-version.outputs.version }}.deb"
|
||||||
cp "artifacts/crossdesk-linux-${ARCH}-${{ steps.version.outputs.VERSION_WITH_V }}/${FILE}" "release/${FILE}"
|
cp "artifacts/crossdesk-linux-${ARCH}-${{ needs.prepare-version.outputs.version }}/${FILE}" "release/${FILE}"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
cp artifacts/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.zip
|
cp artifacts/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.zip
|
||||||
cp artifacts/crossdesk-android-arm64-${{ needs.build-android.outputs.version }}/*.apk release/crossdesk-android-arm64-${{ needs.build-android.outputs.version }}.apk
|
cp artifacts/crossdesk-android-arm64-${{ needs.build-android.outputs.version }}/*.apk release/crossdesk-android-arm64-${{ needs.build-android.outputs.version }}.apk
|
||||||
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
|
cp artifacts/crossdesk-win-x64-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-win-x64-${{ needs.prepare-version.outputs.version }}.exe
|
||||||
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
cp artifacts/crossdesk-win-x64-portable-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-win-x64-portable-${{ needs.prepare-version.outputs.version }}.zip
|
||||||
|
|
||||||
- name: List release files
|
- name: List release files
|
||||||
run: ls -lh release/
|
run: ls -lh release/
|
||||||
@@ -743,10 +597,10 @@ jobs:
|
|||||||
- name: Upload to Versioned GitHub Release
|
- name: Upload to Versioned GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.version.outputs.VERSION_WITH_V }}
|
tag_name: ${{ needs.prepare-version.outputs.version }}
|
||||||
# The pushed tag already determines the release source commit.
|
# The pushed tag already determines the release source commit.
|
||||||
target_commitish: ${{ github.event.repository.default_branch }}
|
target_commitish: ${{ github.event.repository.default_branch }}
|
||||||
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
|
name: Release ${{ needs.prepare-version.outputs.version }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: release/*
|
files: release/*
|
||||||
@@ -789,7 +643,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate version.json for all platforms
|
- name: Generate version.json for all platforms
|
||||||
env:
|
env:
|
||||||
RELEASE_TAG: ${{ steps.version.outputs.VERSION_WITH_V }}
|
RELEASE_TAG: ${{ needs.prepare-version.outputs.version }}
|
||||||
CROSSDESK_UPDATE_PRIVATE_KEY: ${{ secrets.CROSSDESK_UPDATE_PRIVATE_KEY }}
|
CROSSDESK_UPDATE_PRIVATE_KEY: ${{ secrets.CROSSDESK_UPDATE_PRIVATE_KEY }}
|
||||||
run: |
|
run: |
|
||||||
python3 .github/scripts/update_version_manifest.py \
|
python3 .github/scripts/update_version_manifest.py \
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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.SessionHelper" type="win32" />
|
||||||
|
<description>CrossDesk Session Helper</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>
|
||||||
@@ -4,13 +4,10 @@ IDI_ICON1 ICON "crossdesk.ico"
|
|||||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||||
#define RT_MANIFEST 24
|
#define RT_MANIFEST 24
|
||||||
|
|
||||||
#ifdef CROSSDESK_PORTABLE
|
// The service supplies either the user's token (desktop observer) or a SYSTEM
|
||||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
// token (secure capture/input). Requiring elevation here prevents the observer
|
||||||
#elif defined(CROSSDESK_DEBUG)
|
// from starting with CreateProcessAsUserW in installed release builds (740).
|
||||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_session_helper.manifest"
|
||||||
#else
|
|
||||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Session Helper"
|
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Session Helper"
|
||||||
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_session_helper.exe"
|
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_session_helper.exe"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "platform/windows/gui/slint_backend.h"
|
#include "platform/windows/gui/slint_backend.h"
|
||||||
#include "service_host.h"
|
#include "service_host.h"
|
||||||
#include "service_lifetime.h"
|
#include "service_lifetime.h"
|
||||||
|
#include "shutdown_order.h"
|
||||||
#include "unattended_config.h"
|
#include "unattended_config.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -406,6 +407,16 @@ static int RunApplication(
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Apply in every process, including the daemon and --child. Windows does
|
||||||
|
// not inherit shutdown priority from a parent process.
|
||||||
|
const DWORD shutdown_error = crossdesk::ConfigureWindowsShutdownOrder(
|
||||||
|
crossdesk::WindowsShutdownRole::host);
|
||||||
|
if (shutdown_error != ERROR_SUCCESS) {
|
||||||
|
std::cerr << "Could not configure Windows host shutdown order, error="
|
||||||
|
<< shutdown_error << '\n';
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
// Run before GUI, configuration, logging or display initialization.
|
// Run before GUI, configuration, logging or display initialization.
|
||||||
if (argc == 2 &&
|
if (argc == 2 &&
|
||||||
|
|||||||
@@ -426,9 +426,8 @@ ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ConfigCenter::IsHardwareVideoCodecAvailable() {
|
bool ConfigCenter::IsHardwareVideoCodecAvailable() {
|
||||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
#if defined(_WIN32) || defined(__APPLE__) || \
|
||||||
!defined(__arm__) && USE_CUDA) || \
|
(defined(__linux__) && !defined(__aarch64__) && !defined(__arm__) && USE_CUDA)
|
||||||
defined(__APPLE__))
|
|
||||||
return true;
|
return true;
|
||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
#include "crossdesk_ui.h"
|
#include "crossdesk_ui.h"
|
||||||
#include "fa_solid_900.h"
|
#include "fa_solid_900.h"
|
||||||
|
#include "features/about/about_catalog.h"
|
||||||
#include "features/file_transfer/transfer_notification_layout.h"
|
#include "features/file_transfer/transfer_notification_layout.h"
|
||||||
#include "features/gallery/gallery_fullscreen_transition.h"
|
#include "features/gallery/gallery_fullscreen_transition.h"
|
||||||
#include "features/gallery/gallery_window_state.h"
|
#include "features/gallery/gallery_window_state.h"
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "platform/windows/gui/slint_backend.h"
|
#include "platform/windows/gui/slint_backend.h"
|
||||||
|
#include "platform/windows/gui/shutdown_session_guard.h"
|
||||||
#include "platform/windows/gui/tray/win_tray.h"
|
#include "platform/windows/gui/tray/win_tray.h"
|
||||||
#include "platform/windows/gui/window_icons.h"
|
#include "platform/windows/gui/window_icons.h"
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
@@ -922,6 +924,10 @@ void DragWindow(WindowHandle& component, int phase, float mouse_x,
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
struct GuiApplication::SlintUi {
|
struct GuiApplication::SlintUi {
|
||||||
|
#if _WIN32
|
||||||
|
std::unique_ptr<ShutdownSessionGuard> shutdown_guard;
|
||||||
|
std::wstring_view shutdown_reason;
|
||||||
|
#endif
|
||||||
gallery::Workspace gallery;
|
gallery::Workspace gallery;
|
||||||
std::filesystem::path gallery_path;
|
std::filesystem::path gallery_path;
|
||||||
std::vector<std::string> gallery_ids;
|
std::vector<std::string> gallery_ids;
|
||||||
@@ -1283,6 +1289,10 @@ void GuiApplication::InitializeUi() {
|
|||||||
slint::set_xdg_app_id("crossdesk");
|
slint::set_xdg_app_id("crossdesk");
|
||||||
#endif
|
#endif
|
||||||
ui_ = std::make_unique<SlintUi>();
|
ui_ = std::make_unique<SlintUi>();
|
||||||
|
#if _WIN32
|
||||||
|
ui_->shutdown_guard =
|
||||||
|
std::make_unique<ShutdownSessionGuard>(windows_rdp_handoff_connected_);
|
||||||
|
#endif
|
||||||
ui_->sponsorship_prompt_pending =
|
ui_->sponsorship_prompt_pending =
|
||||||
config_center_->GetSponsorshipPromptDismissedVersion() != CROSSDESK_VERSION;
|
config_center_->GetSponsorshipPromptDismissedVersion() != CROSSDESK_VERSION;
|
||||||
ui_->video_presenter =
|
ui_->video_presenter =
|
||||||
@@ -1785,6 +1795,11 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
|
|||||||
window->set_update_install_busy(updater_->busy());
|
window->set_update_install_busy(updater_->busy());
|
||||||
#endif
|
#endif
|
||||||
});
|
});
|
||||||
|
about::Populate(ui_->Window(gallery_window));
|
||||||
|
main->on_open_about_link([this](slint::SharedString url) {
|
||||||
|
const std::string link(url);
|
||||||
|
if (link.starts_with("https://")) OpenUrl(link);
|
||||||
|
});
|
||||||
main->on_open_website([this] { OpenUrl("https://crossdesk.cn"); });
|
main->on_open_website([this] { OpenUrl("https://crossdesk.cn"); });
|
||||||
main->on_open_afdian_sponsor([this] {
|
main->on_open_afdian_sponsor([this] {
|
||||||
OpenUrl("https://afdian.com/a/kunkundi");
|
OpenUrl("https://afdian.com/a/kunkundi");
|
||||||
@@ -2252,6 +2267,11 @@ void GuiApplication::Tick() {
|
|||||||
slint::quit_event_loop();
|
slint::quit_event_loop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if _WIN32
|
||||||
|
if (ui_->shutdown_guard) {
|
||||||
|
ui_->shutdown_guard->Update(ui_->shutdown_reason);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
PollUpdateCheck();
|
PollUpdateCheck();
|
||||||
HandleSessionCleanup();
|
HandleSessionCleanup();
|
||||||
HandlePasswordChangeResult();
|
HandlePasswordChangeResult();
|
||||||
@@ -2809,6 +2829,15 @@ void GuiApplication::UpdateLocalization() {
|
|||||||
if (!ui_ || ui_->localized_language == language) {
|
if (!ui_ || ui_->localized_language == language) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if _WIN32
|
||||||
|
// All views refer to static literals; selecting the text needs no allocation
|
||||||
|
// and only runs when the language changes. Tick still checks connection state.
|
||||||
|
ui_->shutdown_reason = language == 0
|
||||||
|
? L"远程控制连接中,保留连接以确认或取消关机。"
|
||||||
|
: language == 2
|
||||||
|
? L"Удалённый сеанс активен. Соединение сохранено для подтверждения или отмены выключения."
|
||||||
|
: L"Remote session active. Keeping the connection available to confirm or cancel shutdown.";
|
||||||
|
#endif
|
||||||
ui_localization::ApplyMainWindowStrings(ui_->main, language);
|
ui_localization::ApplyMainWindowStrings(ui_->main, language);
|
||||||
ui_localization::ApplyStreamWindowStrings(ui_->main, language);
|
ui_localization::ApplyStreamWindowStrings(ui_->main, language);
|
||||||
ui_localization::ApplyMainWindowStrings(ui_->wall, language);
|
ui_localization::ApplyMainWindowStrings(ui_->wall, language);
|
||||||
@@ -6185,6 +6214,9 @@ void GuiApplication::Cleanup() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ui_->file_dialog_lifetime.reset();
|
ui_->file_dialog_lifetime.reset();
|
||||||
|
#if _WIN32
|
||||||
|
ui_->shutdown_guard.reset();
|
||||||
|
#endif
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
CancelNativeFileDialog();
|
CancelNativeFileDialog();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-10-21
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "about_catalog.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "about_resources.h"
|
||||||
|
|
||||||
|
namespace crossdesk::about {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
slint::SharedString Text(const nlohmann::json& object, const char* key) {
|
||||||
|
return slint::SharedString(object.value(key, std::string{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view TrimLayoutWhitespace(std::string_view line) {
|
||||||
|
const auto first = line.find_first_not_of(" \t\r");
|
||||||
|
if (first == std::string_view::npos) return {};
|
||||||
|
return line.substr(first, line.find_last_not_of(" \t\r") - first + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
slint::SharedString DocumentText(const nlohmann::json& document) {
|
||||||
|
const auto title = document.value("title", std::string{});
|
||||||
|
const auto text = document.value("text", std::string{});
|
||||||
|
if (title != "GNU GPL v3" && title != "GNU LGPL v3") {
|
||||||
|
return slint::SharedString(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GNU's plain-text copies use fixed-width wrapping and space-aligned headings.
|
||||||
|
// Reflow prose for the reader, retaining paragraph boundaries, notice examples,
|
||||||
|
// and every non-whitespace character. The embedded source stays untouched.
|
||||||
|
std::string formatted;
|
||||||
|
std::vector<std::string_view> paragraph;
|
||||||
|
auto append_paragraph = [&] {
|
||||||
|
if (paragraph.empty()) return;
|
||||||
|
if (!formatted.empty()) formatted += "\n\n";
|
||||||
|
const bool preserve_lines = paragraph.front().starts_with("GNU ") ||
|
||||||
|
paragraph.front().starts_with("<");
|
||||||
|
for (size_t index = 0; index < paragraph.size(); ++index) {
|
||||||
|
if (index > 0) {
|
||||||
|
const bool after_copyright =
|
||||||
|
paragraph[index - 1].starts_with("Copyright (C)");
|
||||||
|
formatted += preserve_lines || after_copyright ? '\n' : ' ';
|
||||||
|
}
|
||||||
|
formatted += paragraph[index];
|
||||||
|
}
|
||||||
|
paragraph.clear();
|
||||||
|
};
|
||||||
|
std::string_view remaining(text);
|
||||||
|
while (!remaining.empty()) {
|
||||||
|
const auto end = remaining.find('\n');
|
||||||
|
const auto line = TrimLayoutWhitespace(remaining.substr(0, end));
|
||||||
|
if (line.empty()) {
|
||||||
|
append_paragraph();
|
||||||
|
} else {
|
||||||
|
paragraph.push_back(line);
|
||||||
|
}
|
||||||
|
if (end == std::string_view::npos) break;
|
||||||
|
remaining.remove_prefix(end + 1);
|
||||||
|
}
|
||||||
|
append_paragraph();
|
||||||
|
return slint::SharedString(formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Populate(const slint::ComponentHandle<ui::MainWindow>& window) {
|
||||||
|
// Both the main window and DeskHub share the immutable, embedded catalog.
|
||||||
|
static const auto catalog = nlohmann::json::parse(
|
||||||
|
std::begin(kAboutResources), std::end(kAboutResources));
|
||||||
|
static const auto model = [] {
|
||||||
|
std::vector<ui::AboutComponent> components;
|
||||||
|
for (const auto& item : catalog.at("components")) {
|
||||||
|
ui::AboutComponent component;
|
||||||
|
component.id = Text(item, "id");
|
||||||
|
component.name = Text(item, "name");
|
||||||
|
component.version = Text(item, "version");
|
||||||
|
component.license = Text(item, "license");
|
||||||
|
component.source_url = Text(item, "sourceURL");
|
||||||
|
std::vector<ui::AboutDocument> documents;
|
||||||
|
for (const auto& document : item.at("documents")) {
|
||||||
|
ui::AboutDocument entry;
|
||||||
|
entry.title = Text(document, "title");
|
||||||
|
entry.text = DocumentText(document);
|
||||||
|
documents.push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
component.documents =
|
||||||
|
std::make_shared<slint::VectorModel<ui::AboutDocument>>(documents);
|
||||||
|
components.push_back(std::move(component));
|
||||||
|
}
|
||||||
|
return std::make_shared<slint::VectorModel<ui::AboutComponent>>(components);
|
||||||
|
}();
|
||||||
|
window->set_about_components(model);
|
||||||
|
for (size_t index = 0; index < model->row_count(); ++index) {
|
||||||
|
if (model->row_data(index)->id == "openfec") {
|
||||||
|
window->set_about_openfec_index(static_cast<int>(index));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window->set_about_revision(Text(catalog, "revision"));
|
||||||
|
window->set_about_source_url(Text(catalog, "sourceURL"));
|
||||||
|
window->set_about_build_url(Text(catalog, "buildURL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk::about
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-10-21
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _ABOUT_CATALOG_H_
|
||||||
|
#define _ABOUT_CATALOG_H_
|
||||||
|
|
||||||
|
#include "crossdesk_ui.h"
|
||||||
|
|
||||||
|
namespace crossdesk::about {
|
||||||
|
|
||||||
|
void Populate(const slint::ComponentHandle<ui::MainWindow>& window);
|
||||||
|
|
||||||
|
} // namespace crossdesk::about
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { ScrollView, TextEdit } from "std-widgets.slint";
|
||||||
|
import { SettingsDialog, SettingsButton } from "settings_controls.slint";
|
||||||
|
import { WorkbenchStyle } from "workbench_controls.slint";
|
||||||
|
import { UiRadius } from "ui_radius.slint";
|
||||||
|
import { GalleryStrings } from "device_strings.slint";
|
||||||
|
import { FontAwesomeIcons } from "common.slint";
|
||||||
|
import "../assets/fonts/message_spacing.ttf";
|
||||||
|
|
||||||
|
export enum AboutPage { overview, components, component, document, source }
|
||||||
|
export struct AboutDocument { title: string, text: string }
|
||||||
|
export struct AboutComponent {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
version: string,
|
||||||
|
license: string,
|
||||||
|
source-url: string,
|
||||||
|
documents: [AboutDocument],
|
||||||
|
}
|
||||||
|
|
||||||
|
export global AboutStrings {
|
||||||
|
out property <string> software-license: GalleryStrings.tr("软件许可", "Software license", "Лицензия программы");
|
||||||
|
out property <string> components: GalleryStrings.tr("开源组件", "Open-source components", "Компоненты с открытым кодом");
|
||||||
|
out property <string> source: GalleryStrings.tr("源码与构建说明", "Source code and build instructions", "Исходный код и сборка");
|
||||||
|
out property <string> notices: GalleryStrings.tr("许可与版权声明", "Licenses and copyright notices", "Лицензии и авторские права");
|
||||||
|
out property <string> view-source: GalleryStrings.tr("查看源码", "View source code", "Открыть исходный код");
|
||||||
|
out property <string> back: GalleryStrings.tr("返回", "Back", "Назад");
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AboutNavigationGroup inherits Rectangle {
|
||||||
|
height: rows.preferred-height + 2px;
|
||||||
|
background: WorkbenchStyle.surface;
|
||||||
|
border-radius: UiRadius.control;
|
||||||
|
border-width: 1px; border-color: WorkbenchStyle.separator;
|
||||||
|
clip: true;
|
||||||
|
rows := VerticalLayout {
|
||||||
|
x: 1px; y: 1px; width: parent.width - 2px;
|
||||||
|
height: self.preferred-height; padding: 0px; spacing: 0px;
|
||||||
|
@children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AboutRowSeparator inherits Rectangle {
|
||||||
|
height: 1px;
|
||||||
|
Rectangle {
|
||||||
|
x: 14px; width: parent.width - 30px; height: 1px;
|
||||||
|
background: WorkbenchStyle.separator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AboutNavigationRow inherits Rectangle {
|
||||||
|
in property <string> title;
|
||||||
|
in property <string> subtitle;
|
||||||
|
in property <string> icon;
|
||||||
|
in property <string> detail;
|
||||||
|
in property <bool> grouped: false;
|
||||||
|
in property <bool> external: false;
|
||||||
|
callback clicked;
|
||||||
|
min-width: 0px;
|
||||||
|
min-height: max(44px, labels.preferred-height + 20px);
|
||||||
|
background: touch.pressed ? WorkbenchStyle.pressed-surface
|
||||||
|
: touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface;
|
||||||
|
border-radius: root.grouped ? 0px : UiRadius.control;
|
||||||
|
border-width: root.grouped ? 0px : 1px; border-color: WorkbenchStyle.separator;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: root.title + (root.subtitle != "" ? ", " + root.subtitle : "")
|
||||||
|
+ (root.detail != "" ? ", " + root.detail : "");
|
||||||
|
accessible-action-default => { root.clicked(); }
|
||||||
|
if root.icon != "": Rectangle {
|
||||||
|
x: 12px; y: (parent.height - self.height) / 2;
|
||||||
|
width: 28px; height: 28px;
|
||||||
|
border-radius: UiRadius.control;
|
||||||
|
background: WorkbenchStyle.field-surface;
|
||||||
|
Text {
|
||||||
|
text: root.icon; font-family: "Font Awesome 6 Free"; font-weight: 900;
|
||||||
|
font-size: 13px; color: WorkbenchStyle.muted;
|
||||||
|
horizontal-alignment: center; vertical-alignment: center;
|
||||||
|
accessible-role: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels := VerticalLayout {
|
||||||
|
x: root.icon != "" ? 52px : 14px;
|
||||||
|
y: (parent.height - self.height) / 2;
|
||||||
|
width: parent.width - self.x - 36px - (root.detail != "" ? detail-label.width + 12px : 0px);
|
||||||
|
height: self.preferred-height; spacing: 4px;
|
||||||
|
Text { text: root.title; color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap; }
|
||||||
|
if root.subtitle != "": Text {
|
||||||
|
text: root.subtitle; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detail-label := Text {
|
||||||
|
x: parent.width - self.width - 36px; height: parent.height;
|
||||||
|
width: min(100px, self.preferred-width);
|
||||||
|
text: root.detail; color: WorkbenchStyle.muted; font-size: 11px;
|
||||||
|
vertical-alignment: center; overflow: elide;
|
||||||
|
accessible-role: none;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
x: parent.width - 30px; width: 18px; height: parent.height;
|
||||||
|
text: root.external ? FontAwesomeIcons.arrow-up-right-from-square : FontAwesomeIcons.angle-right;
|
||||||
|
font-family: "Font Awesome 6 Free"; font-weight: 900;
|
||||||
|
color: touch.has-hover ? WorkbenchStyle.accent : WorkbenchStyle.muted;
|
||||||
|
font-size: 11px; vertical-alignment: center; horizontal-alignment: center;
|
||||||
|
accessible-role: none;
|
||||||
|
}
|
||||||
|
touch := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(); } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AboutPanel inherits SettingsDialog {
|
||||||
|
show-close-button: false;
|
||||||
|
in-out property <AboutPage> page: AboutPage.components;
|
||||||
|
in-out property <int> component-index: 0;
|
||||||
|
private property <int> document-index: 0;
|
||||||
|
in property <[AboutComponent]> components;
|
||||||
|
in property <int> openfec-index: -1;
|
||||||
|
in property <string> revision;
|
||||||
|
in property <string> source-url;
|
||||||
|
in property <string> build-url;
|
||||||
|
private property <AboutComponent> selected: root.components[root.component-index];
|
||||||
|
callback back;
|
||||||
|
callback open-link(string);
|
||||||
|
title: root.page == AboutPage.components ? AboutStrings.components
|
||||||
|
: root.page == AboutPage.source ? AboutStrings.source
|
||||||
|
: root.page == AboutPage.document ? AboutStrings.notices
|
||||||
|
: root.selected.name;
|
||||||
|
|
||||||
|
if root.page != AboutPage.document: ScrollView {
|
||||||
|
x: 24px; y: root.content-top; width: parent.width - 48px + 22px;
|
||||||
|
height: footer.y - self.y - 16px;
|
||||||
|
viewport-width: self.visible-width;
|
||||||
|
viewport-height: content.preferred-height;
|
||||||
|
horizontal-scrollbar-policy: always-off;
|
||||||
|
vertical-scrollbar-policy: as-needed;
|
||||||
|
content := VerticalLayout {
|
||||||
|
width: parent.width; height: self.preferred-height;
|
||||||
|
padding: 0px; padding-right: 22px; spacing: 10px;
|
||||||
|
if root.page == AboutPage.components: VerticalLayout {
|
||||||
|
padding: 0px; spacing: 10px;
|
||||||
|
Text {
|
||||||
|
text: GalleryStrings.tr("以下列出本版本的主要开源组件。各组件遵循各自的开源许可。", "Main open-source components used in this build. Each component is covered by its own license.", "Основные открытые компоненты этой сборки. Каждый распространяется по своей лицензии.");
|
||||||
|
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: notice.preferred-height + 32px;
|
||||||
|
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
|
||||||
|
notice := VerticalLayout {
|
||||||
|
x: 16px; y: 16px; width: parent.width - 32px;
|
||||||
|
height: self.preferred-height; spacing: 12px;
|
||||||
|
Text { text: GalleryStrings.tr("OpenFEC 告知", "OpenFEC notice", "Уведомление OpenFEC"); color: WorkbenchStyle.text; font-size: 14px; font-weight: 600; }
|
||||||
|
Text {
|
||||||
|
text: GalleryStrings.tr("本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。", "This application uses OpenFEC for forward error correction under CeCILL-C 1.0, which provides limited warranties and limits liability. See the full license for details.", "Приложение использует OpenFEC для прямого исправления ошибок по лицензии CeCILL-C 1.0 с ограниченной гарантией и ответственностью. Подробности — в тексте лицензии.");
|
||||||
|
color: WorkbenchStyle.muted; font-size: 13px; wrap: word-wrap;
|
||||||
|
font-family: "CrossDesk Message Spacing";
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "(c) Copyright 2009 - 2012 INRIA - All rights reserved";
|
||||||
|
color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap;
|
||||||
|
font-family: "CrossDesk Message Spacing";
|
||||||
|
}
|
||||||
|
if root.openfec-index >= 0: AboutNavigationRow {
|
||||||
|
title: GalleryStrings.tr("OpenFEC 许可与源码", "OpenFEC license and source", "Лицензия и исходный код OpenFEC");
|
||||||
|
clicked => { root.component-index = root.openfec-index; root.page = AboutPage.component; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for component[index] in root.components: VerticalLayout {
|
||||||
|
padding: 0px; spacing: 0px;
|
||||||
|
if component.id != "crossdesk" && component.id != "openfec": AboutNavigationRow {
|
||||||
|
title: component.name;
|
||||||
|
subtitle: component.version + " · " + component.license;
|
||||||
|
clicked => { root.component-index = index; root.page = AboutPage.component; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if root.page == AboutPage.component: VerticalLayout {
|
||||||
|
padding: 0px; spacing: 12px;
|
||||||
|
Text { text: root.selected.version + " · " + root.selected.license; color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; }
|
||||||
|
AboutNavigationRow {
|
||||||
|
title: AboutStrings.view-source; external: true;
|
||||||
|
clicked => { root.open-link(root.selected.source-url); }
|
||||||
|
}
|
||||||
|
Text { text: AboutStrings.notices; color: WorkbenchStyle.text; font-size: 13px; font-weight: 600; }
|
||||||
|
for document[index] in root.selected.documents: AboutNavigationRow {
|
||||||
|
title: document.title;
|
||||||
|
clicked => { root.document-index = index; root.page = AboutPage.document; }
|
||||||
|
}
|
||||||
|
if root.selected.documents.length == 0: Text {
|
||||||
|
text: GalleryStrings.tr("本组件未内置许可文本,请通过“查看源码”查阅许可与版权声明。", "License text is not bundled for this component. Use View source code to read its license and copyright notices.", "Текст лицензии не включён. Откройте исходный код для просмотра лицензии и авторских прав.");
|
||||||
|
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if root.page == AboutPage.source: VerticalLayout {
|
||||||
|
padding: 0px; spacing: 12px;
|
||||||
|
Text {
|
||||||
|
text: root.revision != "" ? GalleryStrings.tr("源码版本:", "Source revision: ", "Ревизия исходного кода: ") + root.revision
|
||||||
|
: GalleryStrings.tr("本版本源码信息暂不可用。", "Source revision is unavailable for this build.", "Ревизия исходного кода этой сборки недоступна.");
|
||||||
|
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
|
||||||
|
}
|
||||||
|
AboutNavigationRow { title: AboutStrings.view-source; external: true; clicked => { root.open-link(root.source-url); } }
|
||||||
|
AboutNavigationRow {
|
||||||
|
title: GalleryStrings.tr("构建说明", "Build instructions", "Инструкции по сборке"); external: true;
|
||||||
|
clicked => { root.open-link(root.build-url); }
|
||||||
|
}
|
||||||
|
AboutNavigationRow {
|
||||||
|
title: GalleryStrings.tr("项目主页", "Project website", "Сайт проекта"); external: true;
|
||||||
|
clicked => { root.open-link("https://crossdesk.cn"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if root.page == AboutPage.document: VerticalLayout {
|
||||||
|
x: 24px; y: root.content-top; width: parent.width - 48px;
|
||||||
|
height: footer.y - self.y - 16px; spacing: 12px;
|
||||||
|
Text { text: root.selected.documents[root.document-index].title; color: WorkbenchStyle.text; font-size: 13px; font-weight: 600; wrap: word-wrap; }
|
||||||
|
TextEdit {
|
||||||
|
text: root.selected.documents[root.document-index].text;
|
||||||
|
read-only: true; wrap: word-wrap; font-size: 14px;
|
||||||
|
// Use the same 1.5em line spacing as other long-form reading views.
|
||||||
|
font-family: "CrossDesk Message Spacing";
|
||||||
|
vertical-stretch: 1;
|
||||||
|
key-pressed(event) => {
|
||||||
|
if event.text == Key.Escape { root.back(); return accept; }
|
||||||
|
return reject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
footer := VerticalLayout {
|
||||||
|
x: 24px; y: parent.height - self.height - 24px;
|
||||||
|
width: parent.width - 48px; height: self.preferred-height; spacing: 12px;
|
||||||
|
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: end;
|
||||||
|
SettingsButton { text: AboutStrings.back; clicked => { root.back(); } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,9 +32,11 @@ export global FontAwesomeIcons {
|
|||||||
out property <string> angle-right: "\u{f105}";
|
out property <string> angle-right: "\u{f105}";
|
||||||
out property <string> angle-up: "\u{f106}";
|
out property <string> angle-up: "\u{f106}";
|
||||||
out property <string> arrow-right-long: "\u{f178}";
|
out property <string> arrow-right-long: "\u{f178}";
|
||||||
|
out property <string> arrow-up-right-from-square: "\u{f35d}";
|
||||||
out property <string> bars: "\u{f0c9}";
|
out property <string> bars: "\u{f0c9}";
|
||||||
out property <string> check: "\u{f00c}";
|
out property <string> check: "\u{f00c}";
|
||||||
out property <string> circle-arrow-up: "\u{f0aa}";
|
out property <string> circle-arrow-up: "\u{f0aa}";
|
||||||
|
out property <string> code: "\u{f121}";
|
||||||
out property <string> compress: "\u{f066}";
|
out property <string> compress: "\u{f066}";
|
||||||
out property <string> computer-mouse: "\u{f8cc}";
|
out property <string> computer-mouse: "\u{f8cc}";
|
||||||
out property <string> copy: "\u{f0c5}";
|
out property <string> copy: "\u{f0c5}";
|
||||||
@@ -42,8 +44,10 @@ export global FontAwesomeIcons {
|
|||||||
out property <string> expand: "\u{f065}";
|
out property <string> expand: "\u{f065}";
|
||||||
out property <string> eye: "\u{f06e}";
|
out property <string> eye: "\u{f06e}";
|
||||||
out property <string> eye-slash: "\u{f070}";
|
out property <string> eye-slash: "\u{f070}";
|
||||||
|
out property <string> file-lines: "\u{f15c}";
|
||||||
out property <string> folder-open: "\u{f07c}";
|
out property <string> folder-open: "\u{f07c}";
|
||||||
out property <string> keyboard: "\u{f11c}";
|
out property <string> keyboard: "\u{f11c}";
|
||||||
|
out property <string> layer-group: "\u{f5fd}";
|
||||||
out property <string> minus: "\u{f068}";
|
out property <string> minus: "\u{f068}";
|
||||||
out property <string> pen: "\u{f304}";
|
out property <string> pen: "\u{f304}";
|
||||||
out property <string> rotate: "\u{f2f1}";
|
out property <string> rotate: "\u{f2f1}";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Single AOT entry point. Generating one header for the entire UI avoids
|
// Single AOT entry point. Generating one header for the entire UI avoids
|
||||||
// duplicate imported-component definitions when C++ uses all three windows.
|
// duplicate imported-component definitions when C++ uses all three windows.
|
||||||
export { AnnouncementItem, MessageStrings } from "message_panel.slint";
|
export { AnnouncementItem, MessageStrings } from "message_panel.slint";
|
||||||
|
export { AboutComponent, AboutDocument, AboutPage } from "about_panel.slint";
|
||||||
export { MainWindow, RecentConnection, UpdateCheckStatus, AutostartStatus, UiStrings } from "main_window.slint";
|
export { MainWindow, RecentConnection, UpdateCheckStatus, AutostartStatus, UiStrings } from "main_window.slint";
|
||||||
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
||||||
export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "server_window.slint";
|
export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "server_window.slint";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint";
|
||||||
import { UiRadius } from "ui_radius.slint";
|
import { UiRadius } from "ui_radius.slint";
|
||||||
import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint";
|
import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint";
|
||||||
import { GalleryStrings } from "device_strings.slint";
|
import { GalleryStrings } from "device_strings.slint";
|
||||||
@@ -536,6 +537,20 @@ export component MainWindow inherits Window {
|
|||||||
if !root.self-host-settings-open { root.self-host-settings-error = ""; }
|
if !root.self-host-settings-open { root.self-host-settings-error = ""; }
|
||||||
}
|
}
|
||||||
in-out property <bool> about-open: false;
|
in-out property <bool> about-open: false;
|
||||||
|
in-out property <AboutPage> about-page: overview;
|
||||||
|
private property <length> about-scroll-y: 0px;
|
||||||
|
in-out property <int> about-component-index: 0;
|
||||||
|
in property <[AboutComponent]> about-components;
|
||||||
|
in property <int> about-openfec-index: -1;
|
||||||
|
in property <string> about-revision;
|
||||||
|
in property <string> about-source-url;
|
||||||
|
in property <string> about-build-url;
|
||||||
|
callback open-about-link(string);
|
||||||
|
function about-back() {
|
||||||
|
if root.about-page == AboutPage.document { root.about-page = AboutPage.component; }
|
||||||
|
else if root.about-page == AboutPage.component && root.about-component-index != 0 { root.about-page = AboutPage.components; }
|
||||||
|
else { root.about-page = AboutPage.overview; }
|
||||||
|
}
|
||||||
in-out property <bool> sponsorship-open: false;
|
in-out property <bool> sponsorship-open: false;
|
||||||
// Automatic prompts must not interrupt another dialog or menu.
|
// Automatic prompts must not interrupt another dialog or menu.
|
||||||
private property <bool> sponsorship-unobstructed: !root.gallery-mode
|
private property <bool> sponsorship-unobstructed: !root.gallery-mode
|
||||||
@@ -627,6 +642,15 @@ export component MainWindow inherits Window {
|
|||||||
|
|
||||||
changed about-open => {
|
changed about-open => {
|
||||||
if root.about-open { root.dialog-focus-pending = true; }
|
if root.about-open { root.dialog-focus-pending = true; }
|
||||||
|
else {
|
||||||
|
root.about-page = AboutPage.overview;
|
||||||
|
root.about-component-index = 0;
|
||||||
|
root.about-scroll-y = 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed about-page => {
|
||||||
|
// Restore dialog shortcuts after leaving the selectable license text.
|
||||||
|
if root.about-open { root.dialog-focus-pending = true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
changed sponsorship-open => {
|
changed sponsorship-open => {
|
||||||
@@ -855,7 +879,10 @@ export component MainWindow inherits Window {
|
|||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if root.about-open && !root.update-open {
|
if root.about-open && !root.update-open {
|
||||||
if event.text == Key.Escape { root.about-open = false; }
|
if event.text == Key.Escape {
|
||||||
|
if root.about-page != AboutPage.overview { root.about-back(); }
|
||||||
|
else { root.about-open = false; }
|
||||||
|
}
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if root.sponsorship-open && root.sponsorship-unobstructed {
|
if root.sponsorship-open && root.sponsorship-unobstructed {
|
||||||
@@ -1459,25 +1486,24 @@ export component MainWindow inherits Window {
|
|||||||
text: root.current-version;
|
text: root.current-version;
|
||||||
font-size: 11px; color: WorkbenchStyle.muted; vertical-alignment: center;
|
font-size: 11px; color: WorkbenchStyle.muted; vertical-alignment: center;
|
||||||
}
|
}
|
||||||
if !root.signal-connected && !root.signal-tls-error: Rectangle {
|
if !root.signal-connected && !root.signal-tls-error: WorkbenchButton {
|
||||||
x: parent.width - self.width - 20px;
|
x: parent.width - self.width - 20px;
|
||||||
y: 0px;
|
y: (parent.height - self.height) / 2;
|
||||||
height: parent.height;
|
width: max(64px, reconnect-label-metrics.preferred-width + 22px);
|
||||||
width: reconnect-label.preferred-width;
|
height: 22px;
|
||||||
reconnect-label := Text {
|
text: UiStrings.signal-reconnect;
|
||||||
text: UiStrings.signal-reconnect;
|
text-size: 11px;
|
||||||
color: WorkbenchStyle.accent;
|
text-color: WorkbenchStyle.accent;
|
||||||
font-size: 11px;
|
clicked => {
|
||||||
horizontal-alignment: center;
|
root.copy-feedback = CopyFeedback.idle;
|
||||||
vertical-alignment: center;
|
root.signal-reconnect-requested();
|
||||||
}
|
}
|
||||||
TouchArea {
|
reconnect-label-metrics := Text {
|
||||||
mouse-cursor: pointer;
|
visible: false;
|
||||||
clicked => {
|
text: UiStrings.signal-reconnect;
|
||||||
root.copy-feedback = CopyFeedback.idle;
|
font-size: 11px;
|
||||||
root.signal-reconnect-requested();
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2365,17 +2391,36 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if root.about-open: Rectangle {
|
if root.about-open: Rectangle {
|
||||||
|
// Use the same bounds throughout the About navigation stack.
|
||||||
|
private property <length> dialog-width: min(480px, self.width - 40px);
|
||||||
|
private property <length> dialog-height: min(520px, self.height - 32px);
|
||||||
background: #15223850;
|
background: #15223850;
|
||||||
z: 46;
|
z: 46;
|
||||||
TouchArea { clicked => { focus-sink.focus(); } }
|
TouchArea { clicked => { focus-sink.focus(); } }
|
||||||
SettingsDialog {
|
if root.about-page != AboutPage.overview: AboutPanel {
|
||||||
x: (parent.width - self.width) / 2;
|
x: (parent.width - self.width) / 2;
|
||||||
y: (parent.height - self.height) / 2;
|
y: (parent.height - self.height) / 2;
|
||||||
width: min(480px, parent.width - 40px);
|
width: parent.dialog-width;
|
||||||
|
min-height: 0px; max-height: parent.height - 32px;
|
||||||
|
height: parent.dialog-height;
|
||||||
|
page <=> root.about-page;
|
||||||
|
component-index <=> root.about-component-index;
|
||||||
|
components: root.about-components;
|
||||||
|
openfec-index: root.about-openfec-index;
|
||||||
|
revision: root.about-revision;
|
||||||
|
source-url: root.about-source-url;
|
||||||
|
build-url: root.about-build-url;
|
||||||
|
back => { root.about-back(); }
|
||||||
|
open-link(url) => { root.open-about-link(url); }
|
||||||
|
}
|
||||||
|
if root.about-page == AboutPage.overview: SettingsDialog {
|
||||||
|
x: (parent.width - self.width) / 2;
|
||||||
|
y: (parent.height - self.height) / 2;
|
||||||
|
width: parent.dialog-width;
|
||||||
// The overlay excludes any custom titlebar on every platform.
|
// The overlay excludes any custom titlebar on every platform.
|
||||||
min-height: 0px; max-height: parent.height - 32px;
|
min-height: 0px; max-height: parent.height - 32px;
|
||||||
// Keep the dialog stable as update status and release details change.
|
// Keep the dialog stable as update status and release details change.
|
||||||
height: min(488px, parent.height - 32px);
|
height: parent.dialog-height;
|
||||||
title: UiStrings.about;
|
title: UiStrings.about;
|
||||||
close-label: MessageStrings.close;
|
close-label: MessageStrings.close;
|
||||||
close => { root.about-open = false; }
|
close => { root.about-open = false; }
|
||||||
@@ -2388,6 +2433,8 @@ export component MainWindow inherits Window {
|
|||||||
height: about-footer.y - self.y - 16px;
|
height: about-footer.y - self.y - 16px;
|
||||||
viewport-width: self.visible-width;
|
viewport-width: self.visible-width;
|
||||||
viewport-height: about-content.preferred-height;
|
viewport-height: about-content.preferred-height;
|
||||||
|
// Keep the position when navigation recreates the overview.
|
||||||
|
viewport-y <=> root.about-scroll-y;
|
||||||
horizontal-scrollbar-policy: always-off;
|
horizontal-scrollbar-policy: always-off;
|
||||||
vertical-scrollbar-policy: as-needed;
|
vertical-scrollbar-policy: as-needed;
|
||||||
about-content := VerticalLayout {
|
about-content := VerticalLayout {
|
||||||
@@ -2465,6 +2512,26 @@ export component MainWindow inherits Window {
|
|||||||
Text { text: UiStrings.copyright; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
|
Text { text: UiStrings.copyright; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
|
||||||
Text { text: UiStrings.license; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
|
Text { text: UiStrings.license; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
|
||||||
}
|
}
|
||||||
|
AboutNavigationGroup {
|
||||||
|
AboutNavigationRow {
|
||||||
|
grouped: true;
|
||||||
|
title: AboutStrings.software-license;
|
||||||
|
detail: "GPL v3";
|
||||||
|
clicked => { root.about-component-index = 0; root.about-page = AboutPage.component; }
|
||||||
|
}
|
||||||
|
AboutRowSeparator { }
|
||||||
|
AboutNavigationRow {
|
||||||
|
grouped: true;
|
||||||
|
title: AboutStrings.components;
|
||||||
|
clicked => { root.about-page = AboutPage.components; }
|
||||||
|
}
|
||||||
|
AboutRowSeparator { }
|
||||||
|
AboutNavigationRow {
|
||||||
|
grouped: true;
|
||||||
|
title: AboutStrings.source;
|
||||||
|
clicked => { root.about-page = AboutPage.source; }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
about-footer := VerticalLayout {
|
about-footer := VerticalLayout {
|
||||||
@@ -2472,17 +2539,14 @@ export component MainWindow inherits Window {
|
|||||||
width: parent.width - 48px; height: self.preferred-height;
|
width: parent.width - 48px; height: self.preferred-height;
|
||||||
spacing: 16px;
|
spacing: 16px;
|
||||||
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
|
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
|
||||||
|
if root.update-status-text != "": Text {
|
||||||
|
text: root.update-status-text;
|
||||||
|
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e
|
||||||
|
: root.update-check-status == UpdateCheckStatus.up-to-date ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
||||||
|
font-size: 12px; wrap: word-wrap;
|
||||||
|
}
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
alignment: end; spacing: 8px;
|
alignment: end; spacing: 8px;
|
||||||
// Status updates use the spare footer space without reflowing the content.
|
|
||||||
Text {
|
|
||||||
min-width: 0px; height: 32px; horizontal-stretch: 1;
|
|
||||||
text: root.update-status-text;
|
|
||||||
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e
|
|
||||||
: root.update-check-status == UpdateCheckStatus.up-to-date ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
|
||||||
font-size: 12px; wrap: word-wrap; overflow: elide;
|
|
||||||
vertical-alignment: center;
|
|
||||||
}
|
|
||||||
if root.update-available: WorkbenchButton {
|
if root.update-available: WorkbenchButton {
|
||||||
width: max(80px, about-update-button-metrics.preferred-width + 24px);
|
width: max(80px, about-update-button-metrics.preferred-width + 24px);
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#include "shutdown_session_guard.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "rd_log.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
constexpr wchar_t kShutdownWindowClass[] = L"CrossDesk.ShutdownSessionGuard";
|
||||||
|
}
|
||||||
|
|
||||||
|
ShutdownSessionGuard::ShutdownSessionGuard(
|
||||||
|
std::shared_ptr<std::atomic<bool>> connected)
|
||||||
|
: connected_(std::move(connected)) {}
|
||||||
|
|
||||||
|
ShutdownSessionGuard::~ShutdownSessionGuard() {
|
||||||
|
ClearReason();
|
||||||
|
if (window_) DestroyWindow(window_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShutdownSessionGuard::ReportError(const char* operation) {
|
||||||
|
const DWORD error = GetLastError();
|
||||||
|
const ULONGLONG now = GetTickCount64();
|
||||||
|
if (last_error_tick_ == 0 || now - last_error_tick_ >= 5000) {
|
||||||
|
LOG_WARN("Windows shutdown session guard {} failed, error={}", operation, error);
|
||||||
|
last_error_tick_ = now;
|
||||||
|
}
|
||||||
|
SetLastError(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShutdownSessionGuard::Start() {
|
||||||
|
if (window_) return true;
|
||||||
|
WNDCLASSW klass{};
|
||||||
|
klass.lpfnWndProc = WindowProc;
|
||||||
|
klass.hInstance = GetModuleHandleW(nullptr);
|
||||||
|
klass.lpszClassName = kShutdownWindowClass;
|
||||||
|
if (!RegisterClassW(&klass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
|
||||||
|
return ReportError("register window");
|
||||||
|
// A top-level window receives session-end broadcasts even when the Slint
|
||||||
|
// windows are hidden in the tray. HWND_MESSAGE does not receive them.
|
||||||
|
window_ = CreateWindowExW(WS_EX_TOOLWINDOW, kShutdownWindowClass, L"CrossDesk",
|
||||||
|
WS_OVERLAPPED, 0, 0, 0, 0, nullptr, nullptr, klass.hInstance, this);
|
||||||
|
if (!window_) return ReportError("create window");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShutdownSessionGuard::ClearReason() {
|
||||||
|
if (!reason_registered_) return true;
|
||||||
|
if (window_ && !ShutdownBlockReasonDestroy(window_))
|
||||||
|
return ReportError("clear reason");
|
||||||
|
reason_registered_ = false;
|
||||||
|
LOG_INFO("Windows shutdown session guard released");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShutdownSessionGuard::SyncReason() {
|
||||||
|
if (shutdown_allowed_ || !connected_ ||
|
||||||
|
!connected_->load(std::memory_order_relaxed)) {
|
||||||
|
return ClearReason();
|
||||||
|
}
|
||||||
|
if (reason_registered_) return true;
|
||||||
|
if (!ShutdownBlockReasonCreate(window_, reason_.c_str()))
|
||||||
|
return ReportError("register reason");
|
||||||
|
reason_registered_ = true;
|
||||||
|
LOG_INFO("Windows shutdown session guard armed for connected controller");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShutdownSessionGuard::Update(std::wstring_view reason) {
|
||||||
|
if (!Start()) return false;
|
||||||
|
if (std::wstring_view(reason_) != reason) {
|
||||||
|
// Keep the old registration and text on failure so the next tick retries
|
||||||
|
// removal before registering the newly selected language.
|
||||||
|
if (!ClearReason()) return false;
|
||||||
|
reason_.assign(reason);
|
||||||
|
}
|
||||||
|
return SyncReason();
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK ShutdownSessionGuard::WindowProc(
|
||||||
|
HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
const auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
||||||
|
auto* guard = static_cast<ShutdownSessionGuard*>(create->lpCreateParams);
|
||||||
|
guard->window_ = window;
|
||||||
|
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(guard));
|
||||||
|
}
|
||||||
|
auto* guard = reinterpret_cast<ShutdownSessionGuard*>(
|
||||||
|
GetWindowLongPtrW(window, GWLP_USERDATA));
|
||||||
|
if (guard && message == WM_QUERYENDSESSION) {
|
||||||
|
const auto flags = static_cast<DWORD>(lparam);
|
||||||
|
// "Shut down anyway", logoff and Restart Manager must remain available.
|
||||||
|
if ((flags & (ENDSESSION_CRITICAL | ENDSESSION_LOGOFF | ENDSESSION_CLOSEAPP)) != 0)
|
||||||
|
guard->shutdown_allowed_ = true;
|
||||||
|
guard->SyncReason();
|
||||||
|
const bool retain = !guard->shutdown_allowed_ && guard->reason_registered_ &&
|
||||||
|
guard->connected_ && guard->connected_->load(std::memory_order_relaxed);
|
||||||
|
LOG_INFO("Windows shutdown query: flags=0x{:x}, retain_remote_session={}", flags, retain);
|
||||||
|
return retain ? FALSE : TRUE;
|
||||||
|
}
|
||||||
|
if (guard && message == WM_ENDSESSION) {
|
||||||
|
LOG_INFO("Windows session end: confirmed={}, flags=0x{:x}",
|
||||||
|
wparam != FALSE, static_cast<DWORD>(lparam));
|
||||||
|
guard->shutdown_allowed_ = wparam != FALSE;
|
||||||
|
guard->SyncReason();
|
||||||
|
// A cancelled shutdown must leave transport, capture and input running.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (guard && message == WM_NCDESTROY) {
|
||||||
|
guard->ClearReason();
|
||||||
|
// Destruction removes the native registration even if explicit removal
|
||||||
|
// failed. A later window must not inherit the destroyed window's state.
|
||||||
|
guard->reason_registered_ = false;
|
||||||
|
guard->window_ = nullptr;
|
||||||
|
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||||
|
}
|
||||||
|
return DefWindowProcW(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-10-22
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _SHUTDOWN_SESSION_GUARD_H_
|
||||||
|
#define _SHUTDOWN_SESSION_GUARD_H_
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// Owned and pumped by the UI thread. The transport may revoke the shared
|
||||||
|
// connection flag from another thread, including between UI ticks.
|
||||||
|
class ShutdownSessionGuard {
|
||||||
|
public:
|
||||||
|
explicit ShutdownSessionGuard(std::shared_ptr<std::atomic<bool>> connected);
|
||||||
|
~ShutdownSessionGuard();
|
||||||
|
ShutdownSessionGuard(const ShutdownSessionGuard&) = delete;
|
||||||
|
ShutdownSessionGuard& operator=(const ShutdownSessionGuard&) = delete;
|
||||||
|
|
||||||
|
bool Update(std::wstring_view reason);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool Start();
|
||||||
|
bool SyncReason();
|
||||||
|
bool ClearReason();
|
||||||
|
bool ReportError(const char* operation);
|
||||||
|
static LRESULT CALLBACK WindowProc(HWND window, UINT message,
|
||||||
|
WPARAM wparam, LPARAM lparam);
|
||||||
|
|
||||||
|
std::shared_ptr<std::atomic<bool>> connected_;
|
||||||
|
HWND window_ = nullptr;
|
||||||
|
std::wstring reason_;
|
||||||
|
bool reason_registered_ = false;
|
||||||
|
bool shutdown_allowed_ = false;
|
||||||
|
ULONGLONG last_error_tick_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: DI JUNKUN
|
* @Author: DI JUNKUN
|
||||||
* @Date: 2026-09-07
|
* @Date: 2026-10-22
|
||||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -19,6 +19,15 @@ inline bool IsDesktopTransitionInputError(DWORD error) {
|
|||||||
error == ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION;
|
error == ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only call with an observation from the interactive session, never Session 0.
|
||||||
|
// Process presence (including LogonUI) cannot prove that Default is accessible:
|
||||||
|
// shutdown blocking UI may be active while WTS still reports an unlocked user.
|
||||||
|
inline bool IsSecureInputDesktop(bool available, const std::string& name,
|
||||||
|
DWORD error) {
|
||||||
|
if (!available) return error == ERROR_ACCESS_DENIED;
|
||||||
|
return !name.empty() && _stricmp(name.c_str(), "Default") != 0;
|
||||||
|
}
|
||||||
|
|
||||||
inline std::wstring GetDesktopNameW(HDESK desktop) {
|
inline std::wstring GetDesktopNameW(HDESK desktop) {
|
||||||
DWORD bytes_needed = 0;
|
DWORD bytes_needed = 0;
|
||||||
if (desktop == nullptr) {
|
if (desktop == nullptr) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "secure_desktop_frame_pacer.h"
|
#include "secure_desktop_frame_pacer.h"
|
||||||
#include "secure_desktop_nv12_cache.h"
|
#include "secure_desktop_nv12_cache.h"
|
||||||
#include "service_lifetime.h"
|
#include "service_lifetime.h"
|
||||||
|
#include "shutdown_order.h"
|
||||||
#include "shared_capture_lifecycle.h"
|
#include "shared_capture_lifecycle.h"
|
||||||
#include "shared_capture_configuration.h"
|
#include "shared_capture_configuration.h"
|
||||||
#include "usbmmidd_policy.h"
|
#include "usbmmidd_policy.h"
|
||||||
@@ -398,13 +399,8 @@ void UpdateHelperState(HelperState* helper_state) {
|
|||||||
// Process enumeration can straddle an unlock. Observe the input desktop
|
// Process enumeration can straddle an unlock. Observe the input desktop
|
||||||
// afterwards, and expose the sample's start time for service-side rejection.
|
// afterwards, and expose the sample's start time for service-side rejection.
|
||||||
InputDesktopInfo desktop_info = GetInputDesktopInfo();
|
InputDesktopInfo desktop_info = GetInputDesktopInfo();
|
||||||
const bool input_desktop_is_winlogon =
|
const bool secure_desktop_active = crossdesk::IsSecureInputDesktop(
|
||||||
_stricmp(desktop_info.name.c_str(), "Winlogon") == 0;
|
desktop_info.available, desktop_info.name, desktop_info.error_code);
|
||||||
const bool inaccessible_secure_input_desktop =
|
|
||||||
!desktop_info.available &&
|
|
||||||
desktop_info.error_code == ERROR_ACCESS_DENIED && !logon_ui_visible;
|
|
||||||
const bool secure_desktop_active = input_desktop_is_winlogon ||
|
|
||||||
inaccessible_secure_input_desktop;
|
|
||||||
bool session_locked = false;
|
bool session_locked = false;
|
||||||
if (!QuerySessionLockState(helper_state->session_id, &session_locked)) {
|
if (!QuerySessionLockState(helper_state->session_id, &session_locked)) {
|
||||||
session_locked =
|
session_locked =
|
||||||
@@ -2043,9 +2039,15 @@ void PrintUsage() {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
|
const DWORD shutdown_error = crossdesk::ConfigureWindowsShutdownOrder(
|
||||||
|
crossdesk::WindowsShutdownRole::helper);
|
||||||
EnablePerMonitorDpiAwareness();
|
EnablePerMonitorDpiAwareness();
|
||||||
|
|
||||||
InitializeHelperLogger();
|
InitializeHelperLogger();
|
||||||
|
if (shutdown_error != ERROR_SUCCESS) {
|
||||||
|
LOG_WARN("Could not configure Windows helper shutdown order, error={}",
|
||||||
|
shutdown_error);
|
||||||
|
}
|
||||||
|
|
||||||
bool run_helper = false;
|
bool run_helper = false;
|
||||||
bool run_secure_input_helper = false;
|
bool run_secure_input_helper = false;
|
||||||
@@ -2158,6 +2160,8 @@ int main(int argc, char* argv[]) {
|
|||||||
bool last_consent_ui = false;
|
bool last_consent_ui = false;
|
||||||
bool last_secure_desktop = false;
|
bool last_secure_desktop = false;
|
||||||
bool last_session_locked = false;
|
bool last_session_locked = false;
|
||||||
|
bool last_input_desktop_available = false;
|
||||||
|
DWORD last_input_desktop_error = ERROR_SUCCESS;
|
||||||
std::string last_stage;
|
std::string last_stage;
|
||||||
while (true) {
|
while (true) {
|
||||||
UpdateHelperState(&helper_state);
|
UpdateHelperState(&helper_state);
|
||||||
@@ -2165,6 +2169,7 @@ int main(int argc, char* argv[]) {
|
|||||||
std::string desktop_name;
|
std::string desktop_name;
|
||||||
bool session_locked = false;
|
bool session_locked = false;
|
||||||
bool input_desktop_available = false;
|
bool input_desktop_available = false;
|
||||||
|
DWORD input_desktop_error = ERROR_SUCCESS;
|
||||||
bool lock_app_visible = false;
|
bool lock_app_visible = false;
|
||||||
bool logon_ui_running = false;
|
bool logon_ui_running = false;
|
||||||
bool consent_ui_visible = false;
|
bool consent_ui_visible = false;
|
||||||
@@ -2174,6 +2179,7 @@ int main(int argc, char* argv[]) {
|
|||||||
desktop_name = helper_state.input_desktop_name;
|
desktop_name = helper_state.input_desktop_name;
|
||||||
session_locked = helper_state.session_locked;
|
session_locked = helper_state.session_locked;
|
||||||
input_desktop_available = helper_state.input_desktop_available;
|
input_desktop_available = helper_state.input_desktop_available;
|
||||||
|
input_desktop_error = helper_state.input_desktop_error_code;
|
||||||
lock_app_visible = helper_state.lock_app_visible;
|
lock_app_visible = helper_state.lock_app_visible;
|
||||||
logon_ui_running = helper_state.logon_ui_visible;
|
logon_ui_running = helper_state.logon_ui_visible;
|
||||||
consent_ui_visible = helper_state.consent_ui_visible;
|
consent_ui_visible = helper_state.consent_ui_visible;
|
||||||
@@ -2188,6 +2194,8 @@ int main(int argc, char* argv[]) {
|
|||||||
lock_app_visible, credential_ui_visible, secure_desktop_active);
|
lock_app_visible, credential_ui_visible, secure_desktop_active);
|
||||||
|
|
||||||
if (desktop_name != last_desktop_name ||
|
if (desktop_name != last_desktop_name ||
|
||||||
|
input_desktop_available != last_input_desktop_available ||
|
||||||
|
input_desktop_error != last_input_desktop_error ||
|
||||||
session_locked != last_session_locked ||
|
session_locked != last_session_locked ||
|
||||||
lock_app_visible != last_lock_app ||
|
lock_app_visible != last_lock_app ||
|
||||||
logon_ui_running != last_logon_ui ||
|
logon_ui_running != last_logon_ui ||
|
||||||
@@ -2195,11 +2203,15 @@ int main(int argc, char* argv[]) {
|
|||||||
secure_desktop_active != last_secure_desktop || stage != last_stage) {
|
secure_desktop_active != last_secure_desktop || stage != last_stage) {
|
||||||
LOG_INFO(
|
LOG_INFO(
|
||||||
"Session helper state: session_id={}, input_desktop='{}', "
|
"Session helper state: session_id={}, input_desktop='{}', "
|
||||||
|
"input_desktop_available={}, input_desktop_error={}, "
|
||||||
"session_locked={}, lock_app_visible={}, logon_ui_running={}, "
|
"session_locked={}, lock_app_visible={}, logon_ui_running={}, "
|
||||||
"consent_ui_visible={}, secure_desktop_active={}, stage={}",
|
"consent_ui_visible={}, secure_desktop_active={}, stage={}",
|
||||||
current_session_id, desktop_name, session_locked, lock_app_visible,
|
current_session_id, desktop_name, input_desktop_available,
|
||||||
|
input_desktop_error, session_locked, lock_app_visible,
|
||||||
logon_ui_running, consent_ui_visible, secure_desktop_active, stage);
|
logon_ui_running, consent_ui_visible, secure_desktop_active, stage);
|
||||||
last_desktop_name = desktop_name;
|
last_desktop_name = desktop_name;
|
||||||
|
last_input_desktop_available = input_desktop_available;
|
||||||
|
last_input_desktop_error = input_desktop_error;
|
||||||
last_session_locked = session_locked;
|
last_session_locked = session_locked;
|
||||||
last_lock_app = lock_app_visible;
|
last_lock_app = lock_app_visible;
|
||||||
last_logon_ui = logon_ui_running;
|
last_logon_ui = logon_ui_running;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-10-22
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _SHUTDOWN_ORDER_H_
|
||||||
|
#define _SHUTDOWN_ORDER_H_
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
enum class WindowsShutdownRole { host, helper };
|
||||||
|
|
||||||
|
// Windows shuts processes down from high to low (the default is 0x280).
|
||||||
|
// Keep the remote host alive while ordinary applications ask the user to
|
||||||
|
// resolve a blocked shutdown. The helpers must outlive the host. Stay inside
|
||||||
|
// the application range; 0x000-0x0ff is reserved for the operating system.
|
||||||
|
inline constexpr DWORD WindowsShutdownLevel(WindowsShutdownRole role) {
|
||||||
|
return role == WindowsShutdownRole::host ? 0x101 : 0x100;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline DWORD ConfigureWindowsShutdownOrder(WindowsShutdownRole role) {
|
||||||
|
// Change only the order. The GUI's session guard separately defers shutdown
|
||||||
|
// while a controller is connected; a confirmed shutdown still ends CrossDesk.
|
||||||
|
if (SetProcessShutdownParameters(WindowsShutdownLevel(role), 0))
|
||||||
|
return ERROR_SUCCESS;
|
||||||
|
const DWORD error = GetLastError();
|
||||||
|
return error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -49,6 +49,9 @@ function setup_options_and_dependencies()
|
|||||||
end
|
end
|
||||||
|
|
||||||
add_requireconfs("**.python", {version = "3.12", override = true, configs = {pgo = false}})
|
add_requireconfs("**.python", {version = "3.12", override = true, configs = {pgo = false}})
|
||||||
|
-- Keep application and build-tool dependencies on the same OpenSSL release.
|
||||||
|
add_requireconfs("**.openssl", "**.openssl3",
|
||||||
|
{version = "3.3.2", system = false, override = true})
|
||||||
add_requires("spdlog 1.14.1", {system = false})
|
add_requires("spdlog 1.14.1", {system = false})
|
||||||
add_requires("slint 1.17.1", {configs = {shared = true}})
|
add_requires("slint 1.17.1", {configs = {shared = true}})
|
||||||
add_requires("libsdl3 3.2.26", {configs = {shared = false}})
|
add_requires("libsdl3 3.2.26", {configs = {shared = false}})
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
-- Embed notices so the About pages also work in offline/portable installations.
|
||||||
|
rule("crossdesk.about_resources")
|
||||||
|
after_load(function(target)
|
||||||
|
target:add("includedirs", path.join(target:autogendir(), "about"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
before_build(function(target)
|
||||||
|
import("core.base.json")
|
||||||
|
import("core.project.config")
|
||||||
|
|
||||||
|
local root = os.projectdir()
|
||||||
|
local function read(relative)
|
||||||
|
return assert(io.readfile(path.join(root, relative)))
|
||||||
|
end
|
||||||
|
local function git(directory, args)
|
||||||
|
local result
|
||||||
|
try {function() result = os.iorunv("git", table.join({"-C", directory}, args)) end}
|
||||||
|
return result and result:trim() or ""
|
||||||
|
end
|
||||||
|
local repository = "https://github.com/kunkundi/crossdesk"
|
||||||
|
local revision = git(root, {"rev-parse", "HEAD"})
|
||||||
|
local source = repository .. (revision ~= "" and "/tree/" .. revision or "")
|
||||||
|
local mini_root = path.join(root, "deps/submodules/minirtc")
|
||||||
|
local mini_revision = git(mini_root, {"rev-parse", "HEAD"})
|
||||||
|
local mini_source = "https://github.com/kunkundi/minirtc" ..
|
||||||
|
(mini_revision ~= "" and "/tree/" .. mini_revision or "")
|
||||||
|
local catalog = json.decode(read("apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json"))
|
||||||
|
local known = {}
|
||||||
|
for _, component in ipairs(catalog.components) do known[component.id] = component end
|
||||||
|
|
||||||
|
-- These desktop dependencies are not part of the shared mobile catalog.
|
||||||
|
local desktop = {
|
||||||
|
slint = {"Slint", "https://github.com/slint-ui/slint/tree/v"},
|
||||||
|
libsdl3 = {"SDL", "https://github.com/libsdl-org/SDL/tree/release-"},
|
||||||
|
tinyfiledialogs = {"tinyfiledialogs", "https://sourceforge.net/projects/tinyfiledialogs/"},
|
||||||
|
["cpp-httplib"] = {"cpp-httplib", "https://github.com/yhirose/cpp-httplib/tree/v"},
|
||||||
|
openh264 = {"OpenH264", "https://github.com/cisco/openh264/tree/v"},
|
||||||
|
miniaudio = {"miniaudio", "https://github.com/mackron/miniaudio/tree/"},
|
||||||
|
aom = {"libaom", "https://aomedia.googlesource.com/aom/+/v"}
|
||||||
|
}
|
||||||
|
local components = {{
|
||||||
|
id = "crossdesk", name = "CrossDesk", version = config.get("CROSSDESK_VERSION") or "0.0.0",
|
||||||
|
license = "GPL-3.0-only", sourceURL = source,
|
||||||
|
documents = {{title = "GNU GPL v3", text = read("LICENSE")}}
|
||||||
|
}, {
|
||||||
|
id = "minirtc", name = "MiniRTC", version = mini_revision:sub(1, 12),
|
||||||
|
license = "LGPL-3.0-only", sourceURL = mini_source,
|
||||||
|
documents = {{title = "GNU LGPL v3", text = read("deps/submodules/minirtc/LICENSE")},
|
||||||
|
{title = "GNU GPL v3", text = read("LICENSE")}}
|
||||||
|
}}
|
||||||
|
local seen = {}
|
||||||
|
for _, dependency in ipairs(table.join({target}, target:orderdeps())) do
|
||||||
|
for _, package in ipairs(dependency:orderpkgs()) do
|
||||||
|
local name = package:name()
|
||||||
|
if not seen[name] then
|
||||||
|
seen[name] = true
|
||||||
|
local version = package:get("version") or ""
|
||||||
|
local shared = known[name]
|
||||||
|
local extra = desktop[name]
|
||||||
|
local item = {id = name, name = shared and shared.name or extra and extra[1] or name,
|
||||||
|
version = version, license = package:license() or "",
|
||||||
|
sourceURL = "https://github.com/xmake-io/xmake-repo/tree/master/packages/" ..
|
||||||
|
name:sub(1, 1) .. "/" .. name,
|
||||||
|
documents = json.mark_as_array({})}
|
||||||
|
if shared and shared.version:gsub("^v", "") == version:gsub("^v", "") then
|
||||||
|
item.license = shared.license
|
||||||
|
item.sourceURL = shared.sourceURL
|
||||||
|
item.documents = shared.documents
|
||||||
|
elseif extra then
|
||||||
|
item.sourceURL = extra[2] .. (name == "tinyfiledialogs" and "" or version:gsub("^v", ""))
|
||||||
|
end
|
||||||
|
-- Prefer installed notices when a package is absent from the shared catalog.
|
||||||
|
if #item.documents == 0 then
|
||||||
|
local install = package:installdir()
|
||||||
|
if install then
|
||||||
|
for _, file in ipairs(os.files(path.join(install, "share/licenses/**"))) do
|
||||||
|
local text = io.readfile(file)
|
||||||
|
if text and not text:find("\0", 1, true) then
|
||||||
|
table.insert(item.documents, {title = path.relative(file, install), text = text})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local notice_header = name == "tinyfiledialogs" and "include/tinyfiledialogs.h"
|
||||||
|
or name == "openh264" and "include/wels/codec_api.h"
|
||||||
|
if notice_header then
|
||||||
|
local header_path = path.join(install, notice_header)
|
||||||
|
local header = os.isfile(header_path) and io.readfile(header_path)
|
||||||
|
local notice = header and header:match("^/%*.-%*/")
|
||||||
|
if notice then
|
||||||
|
table.insert(item.documents, {title = notice_header, text = notice})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if name == "slint" then
|
||||||
|
table.insert(item.documents, {title = "GNU GPL v3", text = read("LICENSE")})
|
||||||
|
end
|
||||||
|
table.insert(components, item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(components, function(a, b)
|
||||||
|
if a.id == b.id then return false end
|
||||||
|
if a.id == "crossdesk" then return true end
|
||||||
|
if b.id == "crossdesk" then return false end
|
||||||
|
return a.name:lower() < b.name:lower()
|
||||||
|
end)
|
||||||
|
local data = json.encode({components = components, revision = revision:sub(1, 12),
|
||||||
|
sourceURL = source,
|
||||||
|
buildURL = repository .. "/blob/" .. (revision ~= "" and revision or "main") .. "/docs/BUILD_EN.md"},
|
||||||
|
{pure = true, orderkeys = true})
|
||||||
|
-- A byte array avoids MSVC's limit on individual/concatenated string literals.
|
||||||
|
local bytes = {}
|
||||||
|
for index = 1, #data do
|
||||||
|
bytes[index] = tostring(data:byte(index)) .. (index % 32 == 0 and "\n" or "")
|
||||||
|
end
|
||||||
|
local output = [[/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-10-21
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _ABOUT_RESOURCES_H_
|
||||||
|
#define _ABOUT_RESOURCES_H_
|
||||||
|
|
||||||
|
// Generated by the desktop build. Do not edit.
|
||||||
|
inline constexpr unsigned char kAboutResources[] = {
|
||||||
|
]] .. table.concat(bytes, ",") .. "\n};\n\n#endif\n"
|
||||||
|
local destination = path.join(target:autogendir(), "about", "about_resources.h")
|
||||||
|
if not os.isfile(destination) or io.readfile(destination) ~= output then
|
||||||
|
os.mkdir(path.directory(destination))
|
||||||
|
io.writefile(destination, output)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
rule_end()
|
||||||
@@ -20,10 +20,6 @@ function setup_targets()
|
|||||||
end
|
end
|
||||||
table.insert(windows_resource_defines,
|
table.insert(windows_resource_defines,
|
||||||
"CROSSDESK_VERSION_NUMERIC=" .. table.concat(version_parts, ","))
|
"CROSSDESK_VERSION_NUMERIC=" .. table.concat(version_parts, ","))
|
||||||
local windows_helper_resource_defines = table.join(windows_resource_defines)
|
|
||||||
if is_config("CROSSDESK_PORTABLE", true) then
|
|
||||||
table.insert(windows_helper_resource_defines, "CROSSDESK_PORTABLE=1")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function copy_slint_runtime(target)
|
local function copy_slint_runtime(target)
|
||||||
if not target:is_plat("windows") then
|
if not target:is_plat("windows") then
|
||||||
@@ -560,6 +556,7 @@ function setup_targets()
|
|||||||
|
|
||||||
target("gui")
|
target("gui")
|
||||||
set_kind("object")
|
set_kind("object")
|
||||||
|
add_rules("crossdesk.about_resources")
|
||||||
set_languages("c++20")
|
set_languages("c++20")
|
||||||
-- spdlog 1.14 bundles fmt 10, whose consteval parser is rejected by
|
-- spdlog 1.14 bundles fmt 10, whose consteval parser is rejected by
|
||||||
-- current Apple Clang in C++20 mode. Keep the established dependency
|
-- current Apple Clang in C++20 mode. Keep the established dependency
|
||||||
@@ -585,6 +582,7 @@ function setup_targets()
|
|||||||
add_files("apps/desktop/src/gui/render.cpp", "apps/desktop/src/gui/application/gui_application.cpp",
|
add_files("apps/desktop/src/gui/render.cpp", "apps/desktop/src/gui/application/gui_application.cpp",
|
||||||
"apps/desktop/src/gui/rendering/*.cpp",
|
"apps/desktop/src/gui/rendering/*.cpp",
|
||||||
"apps/desktop/src/gui/runtime/*.cpp",
|
"apps/desktop/src/gui/runtime/*.cpp",
|
||||||
|
"apps/desktop/src/gui/features/about/*.cpp",
|
||||||
"apps/desktop/src/platform/common/gui/service_status_runtime.cpp",
|
"apps/desktop/src/platform/common/gui/service_status_runtime.cpp",
|
||||||
"apps/desktop/src/gui/features/devices/*.cpp", "apps/desktop/src/gui/features/input/*.cpp",
|
"apps/desktop/src/gui/features/devices/*.cpp", "apps/desktop/src/gui/features/input/*.cpp",
|
||||||
"apps/desktop/src/gui/features/clipboard/*.cpp", "apps/desktop/src/gui/features/file_transfer/*.cpp",
|
"apps/desktop/src/gui/features/clipboard/*.cpp", "apps/desktop/src/gui/features/file_transfer/*.cpp",
|
||||||
@@ -605,6 +603,7 @@ function setup_targets()
|
|||||||
add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp",
|
add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp",
|
||||||
"apps/desktop/src/platform/windows/gui/slint_backend.cpp",
|
"apps/desktop/src/platform/windows/gui/slint_backend.cpp",
|
||||||
"apps/desktop/src/platform/windows/gui/window_icons.cpp",
|
"apps/desktop/src/platform/windows/gui/window_icons.cpp",
|
||||||
|
"apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp",
|
||||||
"apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp",
|
"apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp",
|
||||||
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
|
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
|
||||||
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
|
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
|
||||||
@@ -679,7 +678,7 @@ function setup_targets()
|
|||||||
add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp")
|
add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp")
|
||||||
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp")
|
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp")
|
||||||
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
|
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
|
||||||
{defines = windows_helper_resource_defines})
|
{defines = windows_resource_defines})
|
||||||
add_includedirs("apps/desktop/src/common",
|
add_includedirs("apps/desktop/src/common",
|
||||||
"apps/desktop/src/platform/windows/service",
|
"apps/desktop/src/platform/windows/service",
|
||||||
"apps/desktop/src/platform/windows/screen_capturer",
|
"apps/desktop/src/platform/windows/screen_capturer",
|
||||||
|
|||||||
@@ -43,6 +43,58 @@ def write_json(path, value):
|
|||||||
write(path, json.dumps(value, ensure_ascii=False, indent=2) + '\n')
|
write(path, json.dumps(value, ensure_ascii=False, indent=2) + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def write_about_resources(output, version):
|
||||||
|
"""Supply the embedded catalog required by the shared desktop About UI."""
|
||||||
|
def git(directory, *args):
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(['git', '-C', str(directory), *args],
|
||||||
|
text=True, stderr=subprocess.DEVNULL).strip()
|
||||||
|
except (OSError, subprocess.CalledProcessError):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
revision = git(ROOT, 'rev-parse', 'HEAD')
|
||||||
|
mini = git(ROOT / 'deps/submodules/minirtc', 'rev-parse', 'HEAD')
|
||||||
|
repository = 'https://github.com/kunkundi/crossdesk'
|
||||||
|
source = repository + (f'/tree/{revision}' if revision else '')
|
||||||
|
gpl = {'title': 'GNU GPL v3', 'text': (ROOT / 'LICENSE').read_text()}
|
||||||
|
components = [
|
||||||
|
{'id': 'crossdesk', 'name': 'CrossDesk', 'version': version,
|
||||||
|
'license': 'GPL-3.0-only', 'sourceURL': source, 'documents': [gpl]},
|
||||||
|
{'id': 'minirtc', 'name': 'MiniRTC', 'version': mini[:12],
|
||||||
|
'license': 'LGPL-3.0-only',
|
||||||
|
'sourceURL': 'https://github.com/kunkundi/minirtc' + (f'/tree/{mini}' if mini else ''),
|
||||||
|
'documents': [{'title': 'GNU LGPL v3',
|
||||||
|
'text': (ROOT / 'deps/submodules/minirtc/LICENSE').read_text()}, gpl]},
|
||||||
|
{'id': 'slint', 'name': 'Slint', 'version': '1.17.1', 'license': 'GPL-3.0-only',
|
||||||
|
'sourceURL': 'https://github.com/slint-ui/slint/tree/v1.17.1',
|
||||||
|
'documents': [{'title': 'Slint license', 'text': (SOURCE / 'LICENSE.md').read_text()}, gpl]},
|
||||||
|
{'id': 'cpp-httplib', 'name': 'cpp-httplib', 'version': 'v0.26.0', 'license': 'MIT',
|
||||||
|
'sourceURL': 'https://github.com/yhirose/cpp-httplib/tree/v0.26.0', 'documents': []}]
|
||||||
|
shared = json.loads((ROOT / 'apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json').read_text())['components']
|
||||||
|
shared += json.loads((ROOT / 'apps/android/licenses/additional-notices.json').read_text())
|
||||||
|
seen = {item['id'] for item in components}
|
||||||
|
for item in shared:
|
||||||
|
name = item['id']
|
||||||
|
package = APP / '.native/packages' / name[0] / name
|
||||||
|
if name not in seen and any((package / tag).is_dir() for tag in
|
||||||
|
[item['version'], item['version'].removeprefix('v'),
|
||||||
|
'v' + item['version'].removeprefix('v')]):
|
||||||
|
components.append(item)
|
||||||
|
seen.add(name)
|
||||||
|
components.sort(key=lambda item: (item['id'] != 'crossdesk', item['name'].lower()))
|
||||||
|
data = json.dumps({'components': components, 'revision': revision[:12],
|
||||||
|
'sourceURL': source,
|
||||||
|
'buildURL': f'{repository}/blob/{revision or "main"}/apps/harmony/README.md'},
|
||||||
|
ensure_ascii=False, sort_keys=True).encode()
|
||||||
|
body = '\n'.join(','.join(str(byte) for byte in data[i:i + 32]) + ','
|
||||||
|
for i in range(0, len(data), 32))
|
||||||
|
write(output, '/*\n * @Author: DI JUNKUN\n * @Date: 2026-10-21\n'
|
||||||
|
' * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.\n */\n\n'
|
||||||
|
'#ifndef _ABOUT_RESOURCES_H_\n#define _ABOUT_RESOURCES_H_\n\n'
|
||||||
|
'// Generated by the Harmony desktop build. Do not edit.\n'
|
||||||
|
'inline constexpr unsigned char kAboutResources[] = {\n' + body + '\n};\n\n#endif\n')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
bundle = json.loads((APP / 'AppScope/app.json5').read_text())['app']['bundleName']
|
bundle = json.loads((APP / 'AppScope/app.json5').read_text())['app']['bundleName']
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
@@ -157,6 +209,7 @@ def main():
|
|||||||
urllib.request.urlretrieve('https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.26.0/httplib.h', header)
|
urllib.request.urlretrieve('https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.26.0/httplib.h', header)
|
||||||
if hashlib.sha256(header.read_bytes()).hexdigest() != '4a42da28ce477d06f53942c172197d3b83b4fe42b7e37b8af78c70e063c06da7':
|
if hashlib.sha256(header.read_bytes()).hexdigest() != '4a42da28ce477d06f53942c172197d3b83b4fe42b7e37b8af78c70e063c06da7':
|
||||||
raise SystemExit('cpp-httplib source checksum mismatch')
|
raise SystemExit('cpp-httplib source checksum mismatch')
|
||||||
|
write_about_resources(generated / 'about_resources.h', args.version)
|
||||||
legacy_types = PROJECT / 'entry/src/main/cpp/types/libslint_probe'
|
legacy_types = PROJECT / 'entry/src/main/cpp/types/libslint_probe'
|
||||||
if legacy_types.exists():
|
if legacy_types.exists():
|
||||||
run([studio / 'tools/hvigor/bin/hvigorw', 'clean', '--no-daemon'], cwd=PROJECT, env=env)
|
run([studio / 'tools/hvigor/bin/hvigorw', 'clean', '--no-daemon'], cwd=PROJECT, env=env)
|
||||||
|
|||||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: 5ac8e11b6f...c1b268f20f
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
-- Python's recipe looks up its dependency as "openssl". Keep that name while
|
||||||
|
-- using the OpenSSL 3 recipe, including its sources, patches and build scripts.
|
||||||
|
package("openssl")
|
||||||
|
set_base("openssl3")
|
||||||
|
-- Rebuild consumers cached against the old OpenSSL 1.x ABI.
|
||||||
|
set_policy("package.strict_compatibility", true)
|
||||||
|
package_end()
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
includes("cpp-httplib", "rust", "slint")
|
includes("cpp-httplib", "openssl", "rust", "slint")
|
||||||
|
|||||||
@@ -54,11 +54,27 @@ GitHub Actions 从分支手动打包时,在 `Build and Release` 的
|
|||||||
构建使用 `0.0.0-日期`;版本标签构建继续使用标签中的版本号。
|
构建使用 `0.0.0-日期`;版本标签构建继续使用标签中的版本号。
|
||||||
`desktop_version` 只影响桌面安装包,不改变移动端版本。
|
`desktop_version` 只影响桌面安装包,不改变移动端版本。
|
||||||
|
|
||||||
|
`prepare-version` 前置任务统一生成完整桌面版本,通过 job outputs 传给
|
||||||
|
Linux、macOS、Windows 和发布任务。无日期标签使用前置任务运行当天的
|
||||||
|
北京时间日期,显式日期标签使用标签中的日期;后续任务不再重新计算。
|
||||||
|
构建跨午夜或单独重跑发布任务时,继续使用前置任务确定的版本。
|
||||||
|
|
||||||
不要使用 `vauto-update-20261020` 这类分支名作为版本号验证升级。
|
不要使用 `vauto-update-20261020` 这类分支名作为版本号验证升级。
|
||||||
新版本必须增加数字版本或 patch;只改变日期不视为升级。例如先手动安装
|
新版本必须增加数字版本或 patch;只改变日期不视为升级。例如先手动安装
|
||||||
`v1.7.2-20261020`,再验证到 `v1.7.3-20261020` 的升级。
|
`v1.7.2-20261020`,再验证到 `v1.7.3-20261020` 的升级。
|
||||||
分支手动构建只上传 Actions 构建产物,不会自动发布版本清单。
|
分支手动构建只上传 Actions 构建产物,不会自动发布版本清单。
|
||||||
|
|
||||||
|
发布脚本按数字版本和 patch 比较版本,忽略构建日期。编辑或重新发布旧的
|
||||||
|
桌面 Release 时,保留服务器现有清单并在 Actions 日志中说明跳过原因;
|
||||||
|
同版本仍可更新发布说明,新版本正常发布。各平台下载记录也不会被较低
|
||||||
|
版本覆盖,包括独立移动端发布和桌面 Release 附带的移动端安装包。
|
||||||
|
|
||||||
|
桌面 Release 中所有桌面安装包及便携包的完整版本必须与标签去掉 `v`
|
||||||
|
前缀后的版本一致,包括日期和 patch 的位置;不能只按数字版本比较。
|
||||||
|
任一包不匹配时,脚本报告标签、平台、文件名及双方版本,终止清单生成,
|
||||||
|
不写入输出文件。手动发布时应使用与安装包一致的完整版本标签,例如
|
||||||
|
`v1.7.4-20261021`。iOS、Android 安装包保持独立版本。
|
||||||
|
|
||||||
本地构建可以设置同名环境变量,或使用:
|
本地构建可以设置同名环境变量,或使用:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ if desktop_platform then
|
|||||||
includes(path.join(source_dir, "apps/desktop/xmake/options.lua"))
|
includes(path.join(source_dir, "apps/desktop/xmake/options.lua"))
|
||||||
includes(path.join(source_dir, "apps/desktop/xmake/platform.lua"))
|
includes(path.join(source_dir, "apps/desktop/xmake/platform.lua"))
|
||||||
includes(path.join(source_dir, "apps/desktop/xmake/rules/slint.lua"))
|
includes(path.join(source_dir, "apps/desktop/xmake/rules/slint.lua"))
|
||||||
|
includes(path.join(source_dir, "apps/desktop/xmake/rules/about_resources.lua"))
|
||||||
includes(path.join(source_dir, "apps/desktop/xmake/targets.lua"))
|
includes(path.join(source_dir, "apps/desktop/xmake/targets.lua"))
|
||||||
|
|
||||||
setup_options_and_dependencies()
|
setup_options_and_dependencies()
|
||||||
|
|||||||
Reference in New Issue
Block a user