iOS 0.5.42: F1 and Viaplay hubs under Events (no player changes).

This commit is contained in:
Jos Vooges | STH 2026-09-27 23:58:07 +02:00
parent 89ceccb557
commit 2da59616e8
6 changed files with 1871 additions and 5 deletions

View file

@ -33,6 +33,7 @@
668CADCCEA404B50BB19A46E /* DetailScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A435950EF040DE83F30779 /* DetailScreens.swift */; };
A72DE8139DAE48EC9975E0CF /* LiveTvScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E3A2A592E64C998AB1B72A /* LiveTvScreen.swift */; };
E5F6A7B8C9D0E1F2A3B4C5D6 /* EventsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A2B3C4D5E6F7A8B9C0D1E2 /* EventsScreen.swift */; };
B7C3E9F1B2D4068E5A1C3E71 /* EventHubsScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C3E9F1B2D4068E5A1C3E71 /* EventHubsScreens.swift */; };
A1CFC814F3894773A5BEA652 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470B4DF416DE4B3FB7A60DDB /* SettingsScreen.swift */; };
B8D4E1F2A3C54B6D9E0F1A2B /* DevicesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D2E3F4A5B647C8D9E0F1A3 /* DevicesScreen.swift */; };
B7C2E91A4F8D4A1E9C3B6D20 /* QualityBadges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A1F82E6B9C4E7A8F1D3C50 /* QualityBadges.swift */; };
@ -69,6 +70,7 @@
F0A435950EF040DE83F30779 /* DetailScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailScreens.swift; sourceTree = "<group>"; };
98E3A2A592E64C998AB1B72A /* LiveTvScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTvScreen.swift; sourceTree = "<group>"; };
F1A2B3C4D5E6F7A8B9C0D1E2 /* EventsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsScreen.swift; sourceTree = "<group>"; };
A7C3E9F1B2D4068E5A1C3E71 /* EventHubsScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHubsScreens.swift; sourceTree = "<group>"; };
470B4DF416DE4B3FB7A60DDB /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = "<group>"; };
C1D2E3F4A5B647C8D9E0F1A3 /* DevicesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesScreen.swift; sourceTree = "<group>"; };
D4A1F82E6B9C4E7A8F1D3C50 /* QualityBadges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualityBadges.swift; sourceTree = "<group>"; };
@ -186,6 +188,7 @@
D4A1F82E6B9C4E7A8F1D3C50 /* QualityBadges.swift */,
98E3A2A592E64C998AB1B72A /* LiveTvScreen.swift */,
F1A2B3C4D5E6F7A8B9C0D1E2 /* EventsScreen.swift */,
A7C3E9F1B2D4068E5A1C3E71 /* EventHubsScreens.swift */,
470B4DF416DE4B3FB7A60DDB /* SettingsScreen.swift */,
C1D2E3F4A5B647C8D9E0F1A3 /* DevicesScreen.swift */,
);
@ -291,6 +294,7 @@
B7C2E91A4F8D4A1E9C3B6D20 /* QualityBadges.swift in Sources */,
A72DE8139DAE48EC9975E0CF /* LiveTvScreen.swift in Sources */,
E5F6A7B8C9D0E1F2A3B4C5D6 /* EventsScreen.swift in Sources */,
B7C3E9F1B2D4068E5A1C3E71 /* EventHubsScreens.swift in Sources */,
A1CFC814F3894773A5BEA652 /* SettingsScreen.swift in Sources */,
B8D4E1F2A3C54B6D9E0F1A2B /* DevicesScreen.swift in Sources */,
);
@ -341,7 +345,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = X25NYX3K49;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@ -356,7 +360,7 @@
"$(inherited)",
"@executable_path/Frameworks"
);
MARKETING_VERSION = 0.5.41;
MARKETING_VERSION = 0.5.42;
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -374,7 +378,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 54;
CURRENT_PROJECT_VERSION = 55;
DEVELOPMENT_TEAM = X25NYX3K49;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@ -389,7 +393,7 @@
"$(inherited)",
"@executable_path/Frameworks"
);
MARKETING_VERSION = 0.5.41;
MARKETING_VERSION = 0.5.42;
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -344,6 +344,39 @@ actor ApiClient {
return try await get("/api/v1/client/events/\(enc)/play")
}
func eventHubs() async throws -> [EventHub] {
struct Wrap: Decodable { let hubs: [EventHub]? }
let wrap: Wrap = try await get("/api/v1/client/hubs")
return wrap.hubs ?? []
}
func f1Home() async throws -> F1HubPayload {
try await get("/api/v1/client/f1/home")
}
func f1Meeting(meetingKey: String) async throws -> F1MeetingDetail {
let enc = Self.encodePathSegment(meetingKey)
return try await get("/api/v1/client/f1/meetings/\(enc)")
}
func f1ContentPlay(contentId: String) async throws -> IptvPlayInfo {
let enc = Self.encodePathSegment(contentId)
return try await get("/api/v1/client/f1/content/\(enc)/play")
}
func viaplayHome() async throws -> ViaplayHubPayload {
try await get("/api/v1/client/viaplay/home")
}
func viaplayContentPlay(guid: String, productUrl: String?) async throws -> IptvPlayInfo {
let enc = Self.encodePathSegment(guid)
var q = ""
if let productUrl = productUrl?.trimmingCharacters(in: .whitespacesAndNewlines), !productUrl.isEmpty {
q = "?productUrl=\(Self.encodePathSegment(productUrl))"
}
return try await get("/api/v1/client/viaplay/content/\(enc)/play\(q)")
}
/// Encode path segment (incl. `:` in `custom:uuid`) — strenger dan urlPathAllowed.
private static func encodePathSegment(_ raw: String) -> String {
var allowed = CharacterSet.alphanumerics

View file

@ -593,3 +593,172 @@ struct ApiErrorBody: Decodable {
let error: ErrorObj?
}
// MARK: - Event hubs (F1 / Viaplay)
struct EventHub: Decodable, Identifiable, Hashable {
let id: String
let label: String
let subtitle: String?
}
struct F1Meeting: Decodable, Identifiable, Hashable {
var id: String { meetingKey }
let meetingKey: String
let title: String
let meetingNumber: Int?
let location: String?
let startMs: Double?
let endMs: Double?
let imageUrl: String?
enum CodingKeys: String, CodingKey {
case meetingKey, title, meetingNumber, location, startMs, endMs, imageUrl
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
meetingKey = try decodeFlexibleString(c, forKey: .meetingKey)
title = try c.decodeIfPresent(String.self, forKey: .title) ?? ""
meetingNumber = try? c.decodeIfPresent(Int.self, forKey: .meetingNumber)
location = try c.decodeIfPresent(String.self, forKey: .location)
startMs = try? c.decodeIfPresent(Double.self, forKey: .startMs)
endMs = try? c.decodeIfPresent(Double.self, forKey: .endMs)
imageUrl = try c.decodeIfPresent(String.self, forKey: .imageUrl)
}
}
struct F1ReplayGroup: Decodable, Identifiable, Hashable {
var id: String { meetingKey }
let meetingKey: String
let title: String
let location: String?
let meetingNumber: Int?
let imageUrl: String?
let latestStart: String?
let sessions: [ScheduleEvent]
enum CodingKeys: String, CodingKey {
case meetingKey, title, location, meetingNumber, imageUrl, latestStart, sessions
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
meetingKey = try decodeFlexibleString(c, forKey: .meetingKey)
title = try c.decodeIfPresent(String.self, forKey: .title) ?? ""
location = try c.decodeIfPresent(String.self, forKey: .location)
meetingNumber = try? c.decodeIfPresent(Int.self, forKey: .meetingNumber)
imageUrl = try c.decodeIfPresent(String.self, forKey: .imageUrl)
latestStart = try c.decodeIfPresent(String.self, forKey: .latestStart)
sessions = try c.decodeIfPresent([ScheduleEvent].self, forKey: .sessions) ?? []
}
}
struct F1HubPayload: Decodable {
let live: [ScheduleEvent]
let upcoming: [ScheduleEvent]
let recentReplays: [ScheduleEvent]
let replayGroups: [F1ReplayGroup]
let meetings: [F1Meeting]
let nextMeeting: F1Meeting?
enum CodingKeys: String, CodingKey {
case live, upcoming, recentReplays, replayGroups, meetings, nextMeeting
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
live = try c.decodeIfPresent([ScheduleEvent].self, forKey: .live) ?? []
upcoming = try c.decodeIfPresent([ScheduleEvent].self, forKey: .upcoming) ?? []
recentReplays = try c.decodeIfPresent([ScheduleEvent].self, forKey: .recentReplays) ?? []
replayGroups = try c.decodeIfPresent([F1ReplayGroup].self, forKey: .replayGroups) ?? []
meetings = try c.decodeIfPresent([F1Meeting].self, forKey: .meetings) ?? []
nextMeeting = try c.decodeIfPresent(F1Meeting.self, forKey: .nextMeeting)
}
}
struct F1MeetingDetail: Decodable {
let meeting: F1Meeting?
let sessions: [ScheduleEvent]
let replays: [ScheduleEvent]
let upcoming: [ScheduleEvent]
let live: [ScheduleEvent]
enum CodingKeys: String, CodingKey {
case meeting, sessions, replays, upcoming, live
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
meeting = try c.decodeIfPresent(F1Meeting.self, forKey: .meeting)
sessions = try c.decodeIfPresent([ScheduleEvent].self, forKey: .sessions) ?? []
replays = try c.decodeIfPresent([ScheduleEvent].self, forKey: .replays) ?? []
upcoming = try c.decodeIfPresent([ScheduleEvent].self, forKey: .upcoming) ?? []
live = try c.decodeIfPresent([ScheduleEvent].self, forKey: .live) ?? []
}
}
struct ViaplaySport: Decodable, Identifiable, Hashable {
let id: String
let label: String
let count: Int?
}
struct ViaplayHubItem: Decodable, Identifiable, Hashable {
let id: String
let guid: String
let title: String
let subtitle: String?
let imageUrl: String?
let kind: String?
let openAs: String?
let playable: Bool?
let durationLabel: String?
let start: String?
let badge: String?
let pageUrl: String?
var opensAsEvent: Bool { (openAs ?? "").lowercased() == "event" }
}
struct ViaplayHubRail: Decodable, Identifiable, Hashable {
let id: String
let title: String
let items: [ViaplayHubItem]
enum CodingKeys: String, CodingKey { case id, title, items }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id)
title = try c.decodeIfPresent(String.self, forKey: .title) ?? ""
items = (try? c.decodeIfPresent([ViaplayHubItem].self, forKey: .items)) ?? []
}
}
struct ViaplayHubPayload: Decodable {
let live: [ScheduleEvent]
let today: [ScheduleEvent]
let weekend: [ScheduleEvent]
let later: [ScheduleEvent]
let catchup: [ScheduleEvent]
let sports: [ViaplaySport]
let events: [ScheduleEvent]
let rails: [ViaplayHubRail]
enum CodingKeys: String, CodingKey {
case live, today, weekend, later, catchup, sports, events, rails
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
live = try c.decodeIfPresent([ScheduleEvent].self, forKey: .live) ?? []
today = try c.decodeIfPresent([ScheduleEvent].self, forKey: .today) ?? []
weekend = try c.decodeIfPresent([ScheduleEvent].self, forKey: .weekend) ?? []
later = try c.decodeIfPresent([ScheduleEvent].self, forKey: .later) ?? []
catchup = try c.decodeIfPresent([ScheduleEvent].self, forKey: .catchup) ?? []
sports = try c.decodeIfPresent([ViaplaySport].self, forKey: .sports) ?? []
events = try c.decodeIfPresent([ScheduleEvent].self, forKey: .events) ?? []
rails = try c.decodeIfPresent([ViaplayHubRail].self, forKey: .rails) ?? []
}
}

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,7 @@ struct EventsScreen: View {
@EnvironmentObject private var app: AppModel
@State private var events: [ScheduleEvent] = []
@State private var hubs: [EventHub] = []
@State private var loading = true
@State private var error: String?
@State private var sportFilter: String?
@ -93,6 +94,10 @@ struct EventsScreen: View {
EventDetailScreen(eventId: eventId)
.environmentObject(app)
}
.navigationDestination(for: EventHubRoute.self) { route in
EventHubDestination(route: route)
.environmentObject(app)
}
}
.task { await refresh() }
.onAppear {
@ -149,6 +154,9 @@ struct EventsScreen: View {
ScrollView {
VStack(alignment: .leading, spacing: 22) {
headerBlock
if !hubs.isEmpty {
hubRow
}
filterChips
if filtered.isEmpty {
emptyBlock
@ -215,6 +223,20 @@ struct EventsScreen: View {
.padding(.top, 8)
}
private var hubRow: some View {
HStack(spacing: 12) {
ForEach(hubs) { hub in
if let route = EventHubRoute.forHub(hub.id) {
NavigationLink(value: route) {
EventHubEntryCard(hub: hub)
}
.buttonStyle(HubPressStyle())
}
}
}
.padding(.horizontal, 16)
}
private var subtitleLine: String {
if let sportFilter {
return EventSport.label(for: sportFilter)
@ -359,6 +381,9 @@ struct EventsScreen: View {
self.error = error.localizedDescription
}
}
if let list = try? await app.api.eventHubs() {
hubs = list.filter { EventHubRoute.forHub($0.id) != nil }
}
loading = false
}
}

View file

@ -96,7 +96,7 @@ struct SettingsScreen: View {
Section("Info") {
LabeledContent("App", value: "VMC iOS")
LabeledContent("Versie", value: "0.5.41")
LabeledContent("Versie", value: "0.5.42")
LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit")
LabeledContent("Events", value: app.eventsEnabled ? "Aan" : "Uit")
}