iOS 0.5.42: F1 and Viaplay hubs under Events (no player changes).
This commit is contained in:
parent
89ceccb557
commit
2da59616e8
6 changed files with 1871 additions and 5 deletions
|
|
@ -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)";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
1635
apps/ios/VMC/UI/EventHubsScreens.swift
Normal file
1635
apps/ios/VMC/UI/EventHubsScreens.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue