diff --git a/.forgejo/workflows/build-windows-avalonia.yml b/.forgejo/workflows/build-windows-avalonia.yml
new file mode 100644
index 0000000..f4d410a
--- /dev/null
+++ b/.forgejo/workflows/build-windows-avalonia.yml
@@ -0,0 +1,139 @@
+name: Build Windows Avalonia
+
+on:
+ push:
+ branches:
+ - master
+ - develop
+ tags:
+ - '*'
+ workflow_dispatch:
+
+env:
+ DOTNET_VERSION: 10.0.x
+ PROJECT_PATH: imagecatalog/ImageCatalog 2.csproj
+ PUBLISH_DIR: artifacts/publish/win-x64
+ ARTIFACT_NAME: imagecatalog-windows-avalonia
+
+jobs:
+ build:
+ runs-on: docker
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Restore
+ run: dotnet restore "${{ env.PROJECT_PATH }}" -r win-x64
+
+ - name: Publish Windows Avalonia build
+ run: |
+ set -eu
+ dotnet publish "${{ env.PROJECT_PATH }}" \
+ -c Release \
+ -r win-x64 \
+ --self-contained true \
+ -p:AvaloniaWindowsCrossPublish=true \
+ -p:PublishSingleFile=true \
+ -p:PublishTrimmed=false \
+ -p:PublishReadyToRun=false \
+ -o "${{ env.PUBLISH_DIR }}"
+
+ - name: Validate published executable
+ run: |
+ set -eu
+ exe_count="$(find "${{ env.PUBLISH_DIR }}" -maxdepth 1 -type f -iname '*.exe' | wc -l | tr -d ' ')"
+ if [ "${exe_count}" -eq 0 ]; then
+ echo "No Windows executable produced in ${{ env.PUBLISH_DIR }}"
+ exit 1
+ fi
+
+ - name: Upload publish artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.ARTIFACT_NAME }}
+ path: ${{ env.PUBLISH_DIR }}
+ if-no-files-found: error
+
+ release:
+ if: startsWith(github.ref, 'refs/tags/')
+ needs: build
+ runs-on: docker
+ env:
+ FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
+
+ steps:
+ - name: Download publish artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: ${{ env.ARTIFACT_NAME }}
+ path: artifacts/release
+
+ - name: Validate release token
+ run: |
+ set -eu
+ if [ -z "${FORGEJO_TOKEN}" ]; then
+ echo "secrets.FORGEJO_TOKEN is required for tagged releases"
+ exit 1
+ fi
+
+ - name: Create or reuse release
+ run: |
+ set -eu
+ api_base="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
+ tag="${GITHUB_REF_NAME}"
+
+ http_code="$(curl -sS -o release.json -w '%{http_code}' \
+ -H "Authorization: token ${FORGEJO_TOKEN}" \
+ "${api_base}/releases/tags/${tag}")"
+
+ if [ "${http_code}" = "200" ]; then
+ release_id="$(sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' release.json | head -n1)"
+ elif [ "${http_code}" = "404" ]; then
+ payload="$(printf '{"tag_name":"%s","name":"%s","draft":false,"prerelease":false}' "${tag}" "${tag}")"
+ curl -fsS \
+ -H "Authorization: token ${FORGEJO_TOKEN}" \
+ -H "Content-Type: application/json" \
+ -X POST \
+ -d "${payload}" \
+ "${api_base}/releases" \
+ -o release.json
+ release_id="$(sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' release.json | head -n1)"
+ else
+ echo "Unexpected response while loading release for tag ${tag}: ${http_code}"
+ cat release.json
+ exit 1
+ fi
+
+ if [ -z "${release_id}" ]; then
+ echo "Unable to resolve Forgejo release id"
+ cat release.json
+ exit 1
+ fi
+
+ echo "RELEASE_ID=${release_id}" >> "${GITHUB_ENV}"
+
+ - name: Upload executable to release
+ run: |
+ set -eu
+ api_base="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
+ executable_path="$(find artifacts/release -maxdepth 1 -type f -iname '*.exe' | head -n1)"
+
+ if [ -z "${executable_path}" ]; then
+ echo "No executable found in downloaded artifact"
+ exit 1
+ fi
+
+ short_sha="$(printf '%s' "${GITHUB_SHA}" | cut -c1-12)"
+ asset_name="ImageCatalog-avalonia-win-x64-${GITHUB_REF_NAME}-${short_sha}.exe"
+
+ curl -fsS \
+ -H "Authorization: token ${FORGEJO_TOKEN}" \
+ -H "Content-Type: application/octet-stream" \
+ --data-binary @"${executable_path}" \
+ "${api_base}/releases/${RELEASE_ID}/assets?name=${asset_name}"
\ No newline at end of file
diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj
index f87ea98..d10dce5 100644
--- a/imagecatalog/ImageCatalog 2.csproj
+++ b/imagecatalog/ImageCatalog 2.csproj
@@ -6,6 +6,7 @@
ImageCatalog
default
+ false
@@ -20,6 +21,15 @@
net10.0
Exe
+
+ WinExe
+ win-x64
+ true
+ true
+ true
+ false
+ false
+