diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a24530b..4961737 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,14 +113,30 @@ jobs: run: | $tag = $env:GITHUB_REF_NAME $sha = "${{ steps.hash.outputs.sha256 }}" + # A semver pre-release tag (v0.2.0-rc1, v0.2.0-beta2) publishes as a + # GitHub pre-release: testers get a real download link, while + # /releases/latest — which the website reads — keeps pointing at the + # newest stable build instead of advertising an RC to everyone. + $isPrerelease = $tag -match "-" $lines = @( "EternalMonitor $tag — see [RELEASE_NOTES.md](https://github.com/whoisaldo/EternalMonitor/blob/main/RELEASE_NOTES.md) for what's new.", "", + "**The Windows host and the iPad app must be from the same release.** Protocol v2 is a clean break: a v0.1.x app cannot stream from a v0.2.x host, or the reverse. Each side says so rather than showing broken video.", + "", "**EternalMonitor-Setup.exe SHA-256:**", '```', $sha, '```' ) - gh release create $tag "build\out\EternalMonitor-Setup.exe" ` - --title "EternalMonitor $tag" ` - --notes ($lines -join "`n") + # NOT $args — that is a PowerShell automatic variable. + $releaseArgs = @( + $tag, + "build\out\EternalMonitor-Setup.exe", + "--title", "EternalMonitor $tag", + "--notes", ($lines -join "`n") + ) + if ($isPrerelease) { + Write-Host "Tag $tag is a pre-release; it will not become /releases/latest." + $releaseArgs += "--prerelease" + } + gh release create @releaseArgs diff --git a/FRIENDS_TESTING.md b/FRIENDS_TESTING.md index 170cd08..68ef9fc 100644 --- a/FRIENDS_TESTING.md +++ b/FRIENDS_TESTING.md @@ -13,6 +13,76 @@ steps. To bundle the driver, drop the signed setup into `installer/vendor/vdd/` that folder's `README.txt`); without it the build still works but produces an app-only installer and the extended-display option won't appear. +## Getting a tester onto the iPad app (TestFlight) + +A tester needs no Xcode and no developer account. They need the TestFlight +app from the App Store and a link from you. + +### One-time, on your Mac + +Build number must be higher than anything already uploaded. `ios/project.yml` +carries `CURRENT_PROJECT_VERSION`; builds 3 and 4 are used, so 0.2.0 ships as +build 5. If you upload twice for the same version, bump it again. + +The simplest path is Xcode: + +1. `cd ios && xcodegen generate`, then open `EternalMonitor.xcodeproj`. +2. Select "Any iOS Device" as the destination, then Product, then Archive. +3. In the Organizer window that opens: Distribute App, then TestFlight and + App Store Connect, then Upload. Signing is automatic against team + `9X79V37Q89`. + +The same thing without the GUI, if you have an App Store Connect API key: + +```bash +cd ios +xcodegen generate +xcodebuild -project EternalMonitor.xcodeproj -scheme EternalMonitor \ + -configuration Release -destination 'generic/platform=iOS' \ + -archivePath build/EternalMonitor.xcarchive archive +xcodebuild -exportArchive \ + -archivePath build/EternalMonitor.xcarchive \ + -exportOptionsPlist exportOptions.plist \ + -exportPath build/export \ + -authenticationKeyPath ~/private_keys/AuthKey_XXXXXX.p8 \ + -authenticationKeyID XXXXXX -authenticationKeyIssuerID +``` + +`exportOptions.plist` is already set to `app-store-connect` with +`destination: upload`, so the export step performs the upload. + +### Then, in App Store Connect + +1. TestFlight tab, wait for the build to finish processing (usually a few + minutes; you get an email). +2. Export compliance is already answered in the app + (`ITSAppUsesNonExemptEncryption` is false), so it will not ask per build. +3. Create an external testing group, add the build, and fill in "What to + Test" (see the AMD notes below). +4. Submit for Beta App Review. The first build for external testers is + reviewed by Apple, usually inside a day. +5. Once approved, enable the group's public link and send that to your + tester. Anyone with the link can install; you can cap the number of + testers on the same screen. + +Internal testers skip review entirely and get builds immediately, but they +must be users on your App Store Connect team, so that route only makes sense +for people you want inside the developer account. + +### What to send the tester + +Two links, and they must match: + +- The Windows installer for the same version. While a build is still in + testing it is published as a GitHub pre-release and appears on + eternalmonitor.dev/download.html under "Preview build for testers", + marked as a test build. +- The TestFlight public link for the matching iPad build. + +Protocol v2 is a clean break, so a preview host with a release app (or the +reverse) will not stream. Both sides say so plainly rather than showing +broken video, but it still wastes a tester's evening. + ## Extended display vs mirror By default the iPad mirrors the primary screen. To test the iPad as a real extended desktop: diff --git a/docs/download.html b/docs/download.html index 3a26196..bc90af2 100644 --- a/docs/download.html +++ b/docs/download.html @@ -91,6 +91,38 @@

iPad App

+ + +
diff --git a/docs/script.js b/docs/script.js index 70c43c4..460f49b 100644 --- a/docs/script.js +++ b/docs/script.js @@ -3,6 +3,25 @@ (function () { 'use strict'; + /* --- macOS Notice --- */ + // iPadOS reports platform 'MacIntel' too, so require a non-touch device. + var isMac = /Mac/.test(navigator.platform) && navigator.maxTouchPoints <= 1; + if (isMac && sessionStorage.getItem('mac-notice-dismissed') !== '1') { + var notice = document.createElement('div'); + notice.className = 'mac-notice'; + notice.innerHTML = + '

Looks like you\'re on a Mac.' + + 'EternalMonitor is Windows-only for now. The good news: macOS already does this ' + + 'for free with Sidecar, ' + + 'which turns your iPad into a second display natively and works well.

' + + ''; + notice.querySelector('.mac-notice-close').addEventListener('click', function () { + sessionStorage.setItem('mac-notice-dismissed', '1'); + notice.remove(); + }); + document.body.appendChild(notice); + } + /* --- Scroll Reveal (IntersectionObserver) --- */ var reveals = document.querySelectorAll('.reveal'); if (reveals.length && 'IntersectionObserver' in window) { @@ -76,4 +95,49 @@ downloadBtn.textContent = 'View Releases on GitHub'; if (metaEl) metaEl.textContent = 'Visit GitHub for the latest release'; }); + + /* --- Preview build --- + /releases/latest above deliberately skips pre-releases, so testers would + never see a build that has not shipped yet. Look for the newest one and + reveal the preview card only if it exists; once a stable release + supersedes it, the card disappears on its own. */ + var previewSection = document.getElementById('preview-section'); + if (!previewSection) return; + + fetch('https://api.github.com/repos/whoisaldo/EternalMonitor/releases?per_page=10', { + headers: { 'Accept': 'application/vnd.github.v3+json' } + }) + .then(function (res) { + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.json(); + }) + .then(function (releases) { + var preview = releases.find(function (r) { + return r.prerelease && !r.draft; + }); + if (!preview) return; // nothing in testing right now + + var asset = preview.assets.find(function (a) { + return a.name.endsWith('.exe') || a.name.endsWith('.zip') || a.name.endsWith('.msi'); + }); + if (!asset) return; + + var btn = document.getElementById('preview-btn'); + var version = document.getElementById('preview-version'); + var meta = document.getElementById('preview-meta'); + var sha = document.getElementById('preview-sha256'); + + btn.href = asset.browser_download_url; + btn.textContent = 'Download ' + asset.name; + if (version) version.textContent = preview.tag_name; + if (meta) meta.textContent = asset.name + ' \u00B7 ' + formatBytes(asset.size); + if (sha && preview.body) { + var match = preview.body.match(/[a-fA-F0-9]{64}/); + if (match) sha.textContent = match[0]; + } + previewSection.hidden = false; + }) + .catch(function () { + /* No preview, or the API is unreachable: leave the card hidden. */ + }); })(); diff --git a/docs/style.css b/docs/style.css index 4436d96..d386116 100644 --- a/docs/style.css +++ b/docs/style.css @@ -986,3 +986,96 @@ section { padding: 120px 0; } } + +/* --- macOS Notice Bar --- */ +.mac-notice { + position: fixed; + left: 16px; + right: 16px; + bottom: 16px; + z-index: 200; + display: flex; + align-items: flex-start; + gap: 16px; + max-width: 720px; + margin: 0 auto; + padding: 24px 28px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); +} + +.mac-notice p { + margin: 0; + font-size: 1rem; + line-height: 1.6; + color: var(--muted); +} + +.mac-notice strong { + display: block; + margin-bottom: 6px; + color: var(--text); + font-family: var(--font-display); + font-weight: 700; + font-size: 1.15rem; +} + +.mac-notice a { + color: var(--accent); +} + +.mac-notice-close { + flex-shrink: 0; + margin-left: auto; + padding: 4px 8px; + background: none; + border: none; + color: var(--muted); + font-size: 1.1rem; + line-height: 1; + cursor: pointer; +} + +.mac-notice-close:hover { + color: var(--text); +} + +/* --- Preview build (pre-release, testers only) --- */ +.preview-section { + padding: 0 0 40px; +} + +.preview-card { + background: rgba(var(--accent-rgb), 0.03); + border: 1px solid rgba(var(--accent-rgb), 0.1); + border-left: 3px solid var(--accent); + border-radius: var(--radius-md); + padding: 28px; +} + +.preview-head { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.preview-head h2 { + font-size: 1.25rem; + margin: 0; +} + +.preview-warning { + color: var(--text); + font-size: 0.95rem; + margin-bottom: 10px; +} + +.preview-parity { + color: var(--muted); + font-size: 0.9rem; + margin-bottom: 22px; +} diff --git a/ios/project.yml b/ios/project.yml index f8387fe..a2c92c9 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -29,7 +29,13 @@ targets: CODE_SIGN_STYLE: Automatic ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor - MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE + configs: + # Shader source and Metal debug info belong in local builds only — + # under `base` this shipped inside every TestFlight/App Store build. + debug: + MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE + release: + MTL_ENABLE_DEBUG_INFO: NO EternalMonitorTests: type: bundle.unit-test diff --git a/scripts/QUICKSTART.txt b/scripts/QUICKSTART.txt index 7fba62a..75e66cf 100644 --- a/scripts/QUICKSTART.txt +++ b/scripts/QUICKSTART.txt @@ -3,7 +3,12 @@ EternalMonitor - Quick Start (Windows host) This turns your Windows PC into a second screen for your iPad over your local Wi-Fi. This is the WINDOWS side only. You also need the EternalMonitor -app on an iPad (the sender will give you a separate invite for that). +app on the iPad, which arrives as a TestFlight invite link: install Apple's +free "TestFlight" app from the App Store first, then open the link. + +IMPORTANT: the Windows host and the iPad app must be from the SAME version. +If they are not, the app will tell you to update one of them instead of +showing video. What you need -------------