feat(apple): gamepad ui
apple / swift (push) Successful in 1m5s
audit / cargo-audit (push) Successful in 1m19s
android / android (push) Successful in 4m21s
ci / web (push) Successful in 58s
ci / docs-site (push) Successful in 58s
ci / rust (push) Successful in 8m40s
release / apple (push) Successful in 9m10s
ci / bench (push) Successful in 4m39s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 29s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 3m50s
apple / screenshots (push) Successful in 5m38s
flatpak / build-publish (push) Successful in 4m12s
windows-host / package (push) Successful in 19m17s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m15s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m5s
docker / deploy-docs (push) Successful in 18s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m1s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m53s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 3m24s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m30s

This commit is contained in:
2026-07-01 15:14:19 +02:00
parent c8be614d9a
commit ecbbff5544
22 changed files with 1782 additions and 74 deletions
@@ -92,7 +92,8 @@ public enum LibraryClient {
throw LibraryError.unreachable(
(error as? LocalizedError)?.errorDescription ?? error.localizedDescription)
}
let delegate = LibraryTLSDelegate(identity: identity, pinnedHostFingerprint: hostFingerprint)
let delegate = LibraryTLSDelegate(
identity: identity, pinnedHostFingerprint: hostFingerprint, host: address, port: port)
let session = URLSession(configuration: .ephemeral, delegate: delegate, delegateQueue: nil)
defer { session.finishTasksAndInvalidate() }
@@ -108,7 +109,16 @@ public enum LibraryClient {
}
switch http.statusCode {
case 200:
return try JSONDecoder().decode([GameEntry].self, from: data)
var games = try JSONDecoder().decode([GameEntry].self, from: data)
// Steam art now comes back as host-relative proxy paths (`/api/v1/library/art/...`,
// see the host's `library::steam_art`) so they work the same regardless of which
// interface/port the client reached the host on. Resolve them against THIS host now,
// so every other consumer just sees ordinary absolute URLs.
let base = url
for i in games.indices {
games[i].art = games[i].art.resolved(against: base)
}
return games
case 401:
throw LibraryError.unauthorized
default:
@@ -116,3 +126,43 @@ public enum LibraryClient {
}
}
}
extension Artwork {
/// Rewrite any host-relative field (one starting with `/`) into an absolute URL against `base`.
/// External CDN URLs (GOG/Heroic/Xbox) and `data:` URLs (Lutris) already don't start with `/`,
/// so they pass through unchanged. `internal` (not `fileprivate`) so `LibraryClientTests` can
/// exercise it directly without a live host.
func resolved(against base: URL) -> Artwork {
func abs(_ s: String?) -> String? {
guard let s, s.hasPrefix("/") else { return s }
return URL(string: s, relativeTo: base)?.absoluteString ?? s
}
var a = self
a.portrait = abs(a.portrait)
a.hero = abs(a.hero)
a.logo = abs(a.logo)
a.header = abs(a.header)
return a
}
}
/// Builds the authenticated `URLSession` the library UI uses to fetch cover-art images the same
/// paired identity + host pinning as [`LibraryClient.fetch`], reused across a whole grid's worth of
/// poster loads (this session is NOT one-shot: callers own its lifetime and should invalidate it
/// when the view goes away). Safe to use for every candidate URL a `GameEntry`'s `Artwork` carries:
/// `LibraryTLSDelegate` only pins/presents-cert for the host itself, deferring to normal system
/// trust + no client cert for any other origin (an external CDN URL).
public enum LibraryImageLoader {
public static func session(
address: String,
port: UInt16 = punktfunkDefaultMgmtPort,
certPEM: String,
keyPEM: String,
hostFingerprint: Data?
) throws -> URLSession {
let identity = try ClientTLS.makeIdentity(certPEM: certPEM, keyPEM: keyPEM)
let delegate = LibraryTLSDelegate(
identity: identity, pinnedHostFingerprint: hostFingerprint, host: address, port: port)
return URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
}
}