mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-24 05:33:08 +08:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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()}
|
||||
|
||||
|
||||
def version_info(version, fallback_date=None):
|
||||
def version_fields(version):
|
||||
version = version.removeprefix("v")
|
||||
base = r"[0-9]+(?:\.[0-9]+){1,3}"
|
||||
for pattern in (
|
||||
@@ -60,18 +60,41 @@ def version_info(version, fallback_date=None):
|
||||
):
|
||||
match = re.fullmatch(pattern, version)
|
||||
if match:
|
||||
fields = 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)}
|
||||
return match.groupdict()
|
||||
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):
|
||||
downloads = {}
|
||||
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"):
|
||||
raise ValueError(f"Expected a version tag, got {tag!r}")
|
||||
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.pop("platforms", None)
|
||||
result["downloads"] = {**current["downloads"], **downloads}
|
||||
result["downloads"] = {**current["downloads"], **accepted_downloads}
|
||||
if not mobile:
|
||||
result.update({**metadata, "releaseName": name, "releaseNotes": notes, "tagName": tag})
|
||||
return result
|
||||
@@ -159,11 +206,16 @@ def main():
|
||||
parser.error("Supply --release-json or --tag, --name and --notes")
|
||||
tag, name, notes = args.tag, args.name, args.notes.read_text()
|
||||
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)
|
||||
result = merge_release(current, tag, name, notes, downloads, published_date)
|
||||
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
print(f"Updated {tag}: {', '.join(downloads)}")
|
||||
if result == current:
|
||||
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__":
|
||||
|
||||
+73
-219
@@ -36,9 +36,68 @@ env:
|
||||
CROSSDESK_DESKTOP_VERSION: ${{ inputs.desktop_version }}
|
||||
|
||||
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:
|
||||
name: Build Linux (${{ matrix.arch }})
|
||||
needs: prepare-version
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
LEGAL_VERSION: ${{ needs.prepare-version.outputs.version }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -58,69 +117,6 @@ jobs:
|
||||
options: --user root
|
||||
|
||||
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
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -218,8 +214,10 @@ jobs:
|
||||
# macOS
|
||||
build-macos:
|
||||
name: Build on macOS
|
||||
needs: prepare-version
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
VERSION_NUM: ${{ needs.prepare-version.outputs.version }}
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
EXPECTED_MACOS_SDK: "26.5"
|
||||
strategy:
|
||||
@@ -239,56 +237,6 @@ jobs:
|
||||
package_script: ./apps/desktop/scripts/macos/pkg_arm64.sh
|
||||
|
||||
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
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -380,62 +328,12 @@ jobs:
|
||||
# Windows
|
||||
build-windows-x64:
|
||||
name: Build on Windows x64
|
||||
needs: prepare-version
|
||||
runs-on: windows-2022
|
||||
env:
|
||||
VERSION_NUM: ${{ needs.prepare-version.outputs.version }}
|
||||
XMAKE_GLOBALDIR: D:\xmake_global
|
||||
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
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -636,7 +534,7 @@ jobs:
|
||||
release:
|
||||
name: Publish Release
|
||||
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
|
||||
concurrency:
|
||||
group: publish-release-metadata
|
||||
@@ -651,50 +549,6 @@ jobs:
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/release-notes.md" << EOF
|
||||
@@ -724,18 +578,18 @@ jobs:
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
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-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ 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-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-macos-arm64-${{ needs.prepare-version.outputs.version }}.pkg
|
||||
for ARCH in amd64 arm64; do
|
||||
for PACKAGE in crossdesk crossdesk-virtual-desktop-xfce; do
|
||||
FILE="${PACKAGE}-linux-${ARCH}-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
|
||||
cp "artifacts/crossdesk-linux-${ARCH}-${{ steps.version.outputs.VERSION_WITH_V }}/${FILE}" "release/${FILE}"
|
||||
FILE="${PACKAGE}-linux-${ARCH}-${{ needs.prepare-version.outputs.version }}.deb"
|
||||
cp "artifacts/crossdesk-linux-${ARCH}-${{ needs.prepare-version.outputs.version }}/${FILE}" "release/${FILE}"
|
||||
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-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-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||
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-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-win-x64-portable-${{ needs.prepare-version.outputs.version }}.zip
|
||||
|
||||
- name: List release files
|
||||
run: ls -lh release/
|
||||
@@ -743,10 +597,10 @@ jobs:
|
||||
- name: Upload to Versioned GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
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.
|
||||
target_commitish: ${{ github.event.repository.default_branch }}
|
||||
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
name: Release ${{ needs.prepare-version.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: release/*
|
||||
@@ -789,7 +643,7 @@ jobs:
|
||||
|
||||
- name: Generate version.json for all platforms
|
||||
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 }}
|
||||
run: |
|
||||
python3 .github/scripts/update_version_manifest.py \
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
#include "fa_solid_900.h"
|
||||
#include "features/about/about_catalog.h"
|
||||
#include "features/file_transfer/transfer_notification_layout.h"
|
||||
#include "features/gallery/gallery_fullscreen_transition.h"
|
||||
#include "features/gallery/gallery_window_state.h"
|
||||
@@ -1785,6 +1786,11 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
|
||||
window->set_update_install_busy(updater_->busy());
|
||||
#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_afdian_sponsor([this] {
|
||||
OpenUrl("https://afdian.com/a/kunkundi");
|
||||
|
||||
@@ -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-up: "\u{f106}";
|
||||
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> check: "\u{f00c}";
|
||||
out property <string> circle-arrow-up: "\u{f0aa}";
|
||||
out property <string> code: "\u{f121}";
|
||||
out property <string> compress: "\u{f066}";
|
||||
out property <string> computer-mouse: "\u{f8cc}";
|
||||
out property <string> copy: "\u{f0c5}";
|
||||
@@ -42,8 +44,10 @@ export global FontAwesomeIcons {
|
||||
out property <string> expand: "\u{f065}";
|
||||
out property <string> eye: "\u{f06e}";
|
||||
out property <string> eye-slash: "\u{f070}";
|
||||
out property <string> file-lines: "\u{f15c}";
|
||||
out property <string> folder-open: "\u{f07c}";
|
||||
out property <string> keyboard: "\u{f11c}";
|
||||
out property <string> layer-group: "\u{f5fd}";
|
||||
out property <string> minus: "\u{f068}";
|
||||
out property <string> pen: "\u{f304}";
|
||||
out property <string> rotate: "\u{f2f1}";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Single AOT entry point. Generating one header for the entire UI avoids
|
||||
// duplicate imported-component definitions when C++ uses all three windows.
|
||||
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 { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_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 { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.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 = ""; }
|
||||
}
|
||||
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;
|
||||
// Automatic prompts must not interrupt another dialog or menu.
|
||||
private property <bool> sponsorship-unobstructed: !root.gallery-mode
|
||||
@@ -627,6 +642,15 @@ export component MainWindow inherits Window {
|
||||
|
||||
changed about-open => {
|
||||
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 => {
|
||||
@@ -855,7 +879,10 @@ export component MainWindow inherits Window {
|
||||
return accept;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if root.sponsorship-open && root.sponsorship-unobstructed {
|
||||
@@ -1459,25 +1486,24 @@ export component MainWindow inherits Window {
|
||||
text: root.current-version;
|
||||
font-size: 11px; color: WorkbenchStyle.muted; vertical-alignment: center;
|
||||
}
|
||||
if !root.signal-connected && !root.signal-tls-error: Rectangle {
|
||||
x: parent.width - self.width - 20px;
|
||||
y: 0px;
|
||||
height: parent.height;
|
||||
width: reconnect-label.preferred-width;
|
||||
reconnect-label := Text {
|
||||
text: UiStrings.signal-reconnect;
|
||||
color: WorkbenchStyle.accent;
|
||||
font-size: 11px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.copy-feedback = CopyFeedback.idle;
|
||||
root.signal-reconnect-requested();
|
||||
}
|
||||
}
|
||||
if !root.signal-connected && !root.signal-tls-error: WorkbenchButton {
|
||||
x: parent.width - self.width - 20px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: max(64px, reconnect-label-metrics.preferred-width + 22px);
|
||||
height: 22px;
|
||||
text: UiStrings.signal-reconnect;
|
||||
text-size: 11px;
|
||||
text-color: WorkbenchStyle.accent;
|
||||
clicked => {
|
||||
root.copy-feedback = CopyFeedback.idle;
|
||||
root.signal-reconnect-requested();
|
||||
}
|
||||
reconnect-label-metrics := Text {
|
||||
visible: false;
|
||||
text: UiStrings.signal-reconnect;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2365,17 +2391,36 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
|
||||
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;
|
||||
z: 46;
|
||||
TouchArea { clicked => { focus-sink.focus(); } }
|
||||
SettingsDialog {
|
||||
if root.about-page != AboutPage.overview: AboutPanel {
|
||||
x: (parent.width - self.width) / 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.
|
||||
min-height: 0px; max-height: parent.height - 32px;
|
||||
// Keep the dialog stable as update status and release details change.
|
||||
height: min(488px, parent.height - 32px);
|
||||
height: parent.dialog-height;
|
||||
title: UiStrings.about;
|
||||
close-label: MessageStrings.close;
|
||||
close => { root.about-open = false; }
|
||||
@@ -2388,6 +2433,8 @@ export component MainWindow inherits Window {
|
||||
height: about-footer.y - self.y - 16px;
|
||||
viewport-width: self.visible-width;
|
||||
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;
|
||||
vertical-scrollbar-policy: as-needed;
|
||||
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.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 {
|
||||
@@ -2472,17 +2539,14 @@ export component MainWindow inherits Window {
|
||||
width: parent.width - 48px; height: self.preferred-height;
|
||||
spacing: 16px;
|
||||
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 {
|
||||
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 {
|
||||
width: max(80px, about-update-button-metrics.preferred-width + 24px);
|
||||
height: 32px;
|
||||
|
||||
@@ -49,6 +49,9 @@ function setup_options_and_dependencies()
|
||||
end
|
||||
|
||||
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("slint 1.17.1", {configs = {shared = true}})
|
||||
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()
|
||||
@@ -560,6 +560,7 @@ function setup_targets()
|
||||
|
||||
target("gui")
|
||||
set_kind("object")
|
||||
add_rules("crossdesk.about_resources")
|
||||
set_languages("c++20")
|
||||
-- spdlog 1.14 bundles fmt 10, whose consteval parser is rejected by
|
||||
-- current Apple Clang in C++20 mode. Keep the established dependency
|
||||
@@ -585,6 +586,7 @@ function setup_targets()
|
||||
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/runtime/*.cpp",
|
||||
"apps/desktop/src/gui/features/about/*.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/clipboard/*.cpp", "apps/desktop/src/gui/features/file_transfer/*.cpp",
|
||||
|
||||
@@ -43,6 +43,58 @@ def write_json(path, value):
|
||||
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():
|
||||
bundle = json.loads((APP / 'AppScope/app.json5').read_text())['app']['bundleName']
|
||||
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)
|
||||
if hashlib.sha256(header.read_bytes()).hexdigest() != '4a42da28ce477d06f53942c172197d3b83b4fe42b7e37b8af78c70e063c06da7':
|
||||
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'
|
||||
if legacy_types.exists():
|
||||
run([studio / 'tools/hvigor/bin/hvigorw', 'clean', '--no-daemon'], cwd=PROJECT, env=env)
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: 5ac8e11b6f...2e15671370
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-日期`;版本标签构建继续使用标签中的版本号。
|
||||
`desktop_version` 只影响桌面安装包,不改变移动端版本。
|
||||
|
||||
`prepare-version` 前置任务统一生成完整桌面版本,通过 job outputs 传给
|
||||
Linux、macOS、Windows 和发布任务。无日期标签使用前置任务运行当天的
|
||||
北京时间日期,显式日期标签使用标签中的日期;后续任务不再重新计算。
|
||||
构建跨午夜或单独重跑发布任务时,继续使用前置任务确定的版本。
|
||||
|
||||
不要使用 `vauto-update-20261020` 这类分支名作为版本号验证升级。
|
||||
新版本必须增加数字版本或 patch;只改变日期不视为升级。例如先手动安装
|
||||
`v1.7.2-20261020`,再验证到 `v1.7.3-20261020` 的升级。
|
||||
分支手动构建只上传 Actions 构建产物,不会自动发布版本清单。
|
||||
|
||||
发布脚本按数字版本和 patch 比较版本,忽略构建日期。编辑或重新发布旧的
|
||||
桌面 Release 时,保留服务器现有清单并在 Actions 日志中说明跳过原因;
|
||||
同版本仍可更新发布说明,新版本正常发布。各平台下载记录也不会被较低
|
||||
版本覆盖,包括独立移动端发布和桌面 Release 附带的移动端安装包。
|
||||
|
||||
桌面 Release 中所有桌面安装包及便携包的完整版本必须与标签去掉 `v`
|
||||
前缀后的版本一致,包括日期和 patch 的位置;不能只按数字版本比较。
|
||||
任一包不匹配时,脚本报告标签、平台、文件名及双方版本,终止清单生成,
|
||||
不写入输出文件。手动发布时应使用与安装包一致的完整版本标签,例如
|
||||
`v1.7.4-20261021`。iOS、Android 安装包保持独立版本。
|
||||
|
||||
本地构建可以设置同名环境变量,或使用:
|
||||
|
||||
```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/platform.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"))
|
||||
|
||||
setup_options_and_dependencies()
|
||||
|
||||
Reference in New Issue
Block a user