diff --git a/.github/workflows/_reusable-ui-smoke-tests.yml b/.github/workflows/_reusable-ui-smoke-tests.yml index 0c44ff5..103828b 100644 --- a/.github/workflows/_reusable-ui-smoke-tests.yml +++ b/.github/workflows/_reusable-ui-smoke-tests.yml @@ -71,7 +71,7 @@ jobs: set -euo pipefail tool_root="$GITHUB_WORKSPACE" - ROOT_DIR="$GITHUB_WORKSPACE" TOOL_ROOT="$tool_root" "$tool_root/scripts/dev/bootstrap.sh" + ROOT_DIR="$GITHUB_WORKSPACE" TOOL_ROOT="$tool_root" "$tool_root/scripts/dev/bootstrap.sh" --profile ui-smoke - name: Cache Xcode metadata uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 diff --git a/.github/workflows/_reusable-unit-tests.yml b/.github/workflows/_reusable-unit-tests.yml index ace5c5c..e66b6b0 100644 --- a/.github/workflows/_reusable-unit-tests.yml +++ b/.github/workflows/_reusable-unit-tests.yml @@ -51,7 +51,7 @@ jobs: set -euo pipefail tool_root="$GITHUB_WORKSPACE" - ROOT_DIR="$GITHUB_WORKSPACE" TOOL_ROOT="$tool_root" "$tool_root/scripts/dev/bootstrap.sh" + ROOT_DIR="$GITHUB_WORKSPACE" TOOL_ROOT="$tool_root" "$tool_root/scripts/dev/bootstrap.sh" --profile unit - name: Cache SwiftPM build artifacts uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84461a9..a72192d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ on: - 'Package.resolved' - '**/Package.resolved' - 'mise.toml' + - 'mise.lock' - 'Brewfile' - '.swiftformat' - '.swiftlint.yml' @@ -129,7 +130,7 @@ jobs: set -euo pipefail tool_root="$GITHUB_WORKSPACE" - ROOT_DIR="$GITHUB_WORKSPACE" TOOL_ROOT="$tool_root" "$tool_root/scripts/dev/bootstrap.sh" + ROOT_DIR="$GITHUB_WORKSPACE" TOOL_ROOT="$tool_root" "$tool_root/scripts/dev/bootstrap.sh" --profile static - name: Validate static gates shell: bash @@ -174,7 +175,7 @@ jobs: set -euo pipefail tool_root="$GITHUB_WORKSPACE" - ROOT_DIR="$GITHUB_WORKSPACE" TOOL_ROOT="$tool_root" "$tool_root/scripts/dev/bootstrap.sh" + ROOT_DIR="$GITHUB_WORKSPACE" TOOL_ROOT="$tool_root" "$tool_root/scripts/dev/bootstrap.sh" --profile xcode - name: Run doctor checks shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d15bcf4..247d2d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,7 @@ on: - 'scripts/lib/**' - 'scripts/release/**' - 'mise.toml' + - 'mise.lock' - 'Brewfile' - '.github/workflows/release.yml' - '.github/actions/xcode-select/action.yml' diff --git a/Sources/VoidDisplayApp/Bootstrap/VoidDisplayApp.swift b/Sources/VoidDisplayApp/Bootstrap/VoidDisplayApp.swift index 8fa7d65..1ffde94 100644 --- a/Sources/VoidDisplayApp/Bootstrap/VoidDisplayApp.swift +++ b/Sources/VoidDisplayApp/Bootstrap/VoidDisplayApp.swift @@ -11,6 +11,7 @@ import VoidDisplayFoundation // // +import AppKit import Foundation import SwiftUI @@ -26,6 +27,7 @@ package struct AppEnvironment { } public struct VoidDisplayApplication: App { + @NSApplicationDelegateAdaptor(VoidDisplayApplicationDelegate.self) private var appDelegate @State private var capture: CaptureController @State private var sharing: SharingController @State private var virtualDisplay: VirtualDisplayController @@ -45,6 +47,9 @@ public struct VoidDisplayApplication: App { _navigation = State(initialValue: AppNavigationController()) _feedbackController = State(initialValue: env.feedbackController) observability = env.observability + AppTerminationCleanup.install { + env.sharing.stopWebService() + } } public var body: some Scene { @@ -104,6 +109,28 @@ public struct VoidDisplayApplication: App { } } +@MainActor +private enum AppTerminationCleanup { + private static var handler: (() -> Void)? + + static func install(_ handler: @escaping () -> Void) { + self.handler = handler + } + + static func run() { + guard let handler else { return } + self.handler = nil + handler() + } +} + +@MainActor +private final class VoidDisplayApplicationDelegate: NSObject, NSApplicationDelegate { + func applicationWillTerminate(_: Notification) { + AppTerminationCleanup.run() + } +} + @MainActor package enum AppBootstrap { private static let xCTestConfigurationEnvironmentKey = "XCTestConfigurationFilePath" diff --git a/Sources/VoidDisplaySharing/Resources/displayPage.css b/Sources/VoidDisplaySharing/Resources/displayPage.css new file mode 100644 index 0000000..fdf9306 --- /dev/null +++ b/Sources/VoidDisplaySharing/Resources/displayPage.css @@ -0,0 +1,249 @@ +:root { + color-scheme: dark; + --panel: rgba(255, 255, 255, 0.08); + --text: #f7f9fd; + --muted: rgba(247, 249, 253, 0.65); +} +* { box-sizing: border-box; } +body { + margin: 0; + height: 100vh; + font-family: "SF Pro Display", "Helvetica Neue", sans-serif; + background: + radial-gradient(circle at top left, rgba(143, 211, 255, 0.18), transparent 32%), + radial-gradient(circle at bottom right, rgba(51, 101, 196, 0.18), transparent 38%), + linear-gradient(160deg, #080b11, #0d1728 55%, #060910); + color: var(--text); + display: grid; + place-items: center; + padding: 12px; + overflow: hidden; +} +.shell { + width: 98vw; + height: calc(100vh - 24px); + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 12px; +} +.hero { + display: grid; + grid-template-columns: minmax(12rem, 1fr) minmax(0, auto); + align-items: center; + gap: 18px; + padding: 18px 22px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + background: var(--panel); + backdrop-filter: blur(18px); +} +.hero-brand { + min-width: 0; +} +.hero-metadata { + display: flex; + min-width: 0; + align-items: center; + justify-content: flex-end; + justify-self: end; + gap: 18px; +} +.hero-actions { + display: flex; + min-width: 0; + align-items: center; + justify-content: flex-end; +} +.eyebrow { + margin: 0; + font-size: 12px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); +} +.controls { + display: flex; + gap: 8px; +} +.control-btn { + appearance: none; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 999px; + padding: 8px 12px; + color: var(--text); + background: rgba(255, 255, 255, 0.06); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.02em; + cursor: pointer; +} +.control-btn:hover { + background: rgba(255, 255, 255, 0.12); +} +.stage { + position: relative; + border-radius: 28px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.08); + background: #000; + min-height: 0; + box-shadow: 0 36px 90px rgba(0, 0, 0, 0.45); +} +video { + width: 100%; + height: 100%; + display: block; + object-fit: contain; + background: #000; +} +.video-info { + flex: 0 1 clamp(28rem, 42vw, 42rem); + width: clamp(28rem, 42vw, 42rem); + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; + font-size: 12px; + font-weight: 650; + line-height: 1.3; + opacity: 0.92; + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum" 1; +} +.video-info::before { + content: ""; + display: inline-block; + width: 6px; + height: 6px; + margin-right: 8px; + border-radius: 999px; + background: #5ee089; + box-shadow: 0 0 12px rgba(94, 224, 137, 0.5); + vertical-align: 1px; +} +.video-info:empty { + display: none; +} +.video-info:empty::before { + content: none; +} +.overlay { + position: absolute; + z-index: 2; + inset: 0; + display: grid; + place-items: center; + padding: 24px; + background: linear-gradient(180deg, rgba(6, 9, 16, 0.22), rgba(6, 9, 16, 0.72)); + text-align: center; +} +.overlay[hidden] { display: none; } +.loading-spinner { + width: 46px; + height: 46px; + border: 3px solid rgba(255, 255, 255, 0.2); + border-top-color: rgba(255, 255, 255, 0.92); + border-radius: 50%; + animation: loading-spin 860ms linear infinite; +} +@keyframes loading-spin { + to { transform: rotate(360deg); } +} +.footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 0 8px; + min-height: 36px; +} +.footnote { + margin: 0; + font-size: 13px; + color: var(--muted); +} +.connection-status { + display: grid; + justify-items: start; + width: clamp(14rem, 24vw, 24rem); + max-width: min(520px, 52vw); + margin-left: auto; + padding: 0; + color: var(--text); + overflow-wrap: anywhere; + text-align: right; +} +.connection-status-title { + width: 100%; + font-size: 12px; + font-weight: 700; + line-height: 1.3; +} +.connection-status-detail { + width: 100%; + margin-top: 2px; + color: var(--muted); + font-size: 12px; + line-height: 1.3; +} +.connection-status-detail[hidden] { display: none; } +@media (max-width: 760px) { + .hero { + grid-template-columns: 1fr; + align-items: flex-start; + } + .hero-brand { + grid-column: 1; + grid-row: 1; + } + .hero-metadata { + grid-column: 1; + grid-row: 2; + width: 100%; + justify-content: flex-end; + flex-wrap: wrap; + } + .video-info { + order: 2; + flex-basis: 100%; + width: 100%; + text-align: right; + } + .footer { + align-items: stretch; + flex-direction: column; + } + .connection-status { + width: 100%; + text-align: right; + } +} +body.mode-native { + height: auto; + min-height: 100vh; + overflow: auto; +} +body.mode-native .shell { + height: auto; + min-height: calc(100vh - 24px); +} +body.mode-native .stage { + overflow: auto; + display: block; +} +body.mode-native video { + width: auto; + height: auto; + max-width: none; + max-height: none; + margin: 0 auto; +} +@media (max-height: 860px) { + .hero { + padding: 12px 16px; + } + .footnote { + font-size: 12px; + } +} diff --git a/Sources/VoidDisplaySharing/Resources/displayPage.html b/Sources/VoidDisplaySharing/Resources/displayPage.html index bf27469..1704f6c 100644 --- a/Sources/VoidDisplaySharing/Resources/displayPage.html +++ b/Sources/VoidDisplaySharing/Resources/displayPage.html @@ -5,183 +5,22 @@
VoidDisplay Live
Use `1:1` for original size and `Fullscreen` for immersive view.
+