Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
70 changes: 70 additions & 0 deletions FRIENDS_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <issuer-uuid>
```

`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:
Expand Down
32 changes: 32 additions & 0 deletions docs/download.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,38 @@ <h2>iPad App</h2>
</div>
</section>

<!-- ===== Preview build (revealed only when a pre-release exists) ===== -->
<section class="preview-section" id="preview-section" hidden>
<div class="container">
<div class="preview-card">
<div class="preview-head">
<span class="limitations-badge" id="preview-version">preview</span>
<h2>Preview build for testers</h2>
</div>
<p class="preview-warning">
<strong>This is a test build, not a release.</strong> It is here so testers can try
new hardware paths before they are signed off. Expect rough edges, and please report
what breaks.
</p>
<p class="preview-parity">
The Windows host and the iPad app must both come from <em>this same preview</em>.
Mixing a preview host with a release app (or the reverse) will not stream &mdash; each
side will tell you to update the other. The matching iPad build is distributed through
TestFlight; ask for an invite.
</p>
<div class="download-btn-wrap">
<a href="https://github.com/whoisaldo/EternalMonitor/releases" id="preview-btn"
class="btn-ghost" target="_blank" rel="noopener">Download preview for Windows</a>
<p class="download-meta" id="preview-meta"></p>
</div>
<div class="sha256">
<span class="sha256-label">SHA-256</span>
<code id="preview-sha256">Available on the release page</code>
</div>
</div>
</div>
</section>

<!-- ===== Build from Source ===== -->
<section class="build-section">
<div class="container">
Expand Down
64 changes: 64 additions & 0 deletions docs/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
'<p><strong>Looks like you\'re on a Mac.</strong>' +
'EternalMonitor is Windows-only for now. The good news: macOS already does this ' +
'for free with <a href="https://support.apple.com/en-us/102386" target="_blank" rel="noopener">Sidecar</a>, ' +
'which turns your iPad into a second display natively and works well.</p>' +
'<button type="button" class="mac-notice-close" aria-label="Dismiss">&times;</button>';
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) {
Expand Down Expand Up @@ -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. */
});
})();
93 changes: 93 additions & 0 deletions docs/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
8 changes: 7 additions & 1 deletion ios/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion scripts/QUICKSTART.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------
Expand Down