From fd7372a2e9df0d6f8f965fb214c9d7214c722241 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:41:36 +0530 Subject: [PATCH] added liveactivity for ios downloads --- app.json | 1 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 + .../Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + ios/LiveActivity/Color+hex.swift | 37 + ios/LiveActivity/Date+toTimerInterval.swift | 7 + ios/LiveActivity/Image+dynamic.swift | 33 + ios/LiveActivity/Info.plist | 11 + ios/LiveActivity/LiveActivity.entitlements | 5 + .../LiveActivityDebug.entitlements | 5 + ios/LiveActivity/LiveActivityView.swift | 247 ++++++ ios/LiveActivity/LiveActivityWidget.swift | 169 ++++ .../LiveActivityWidgetBundle.swift | 9 + ios/LiveActivity/View+applyIfPresent.swift | 12 + ios/LiveActivity/View+applyWidgetURL.swift | 24 + ios/LiveActivity/ViewHelpers.swift | 33 + ios/Nuvio.xcodeproj/project.pbxproj | 767 ++++++++++++++++-- ios/Nuvio/AppDelegate.swift | 9 + ios/Nuvio/Info.plist | 6 +- ios/Nuvio/Nuvio-Bridging-Header.h | 1 + ios/Nuvio/Supporting/Expo.plist | 2 +- ios/Podfile.lock | 6 + ios/Podfile.properties.json | 2 +- live.md | 379 +++++++++ package-lock.json | 12 + package.json | 1 + src/components/StreamCard.tsx | 6 + src/components/TabletStreamsLayout.tsx | 1 + src/contexts/DownloadsContext.tsx | 172 +++- .../streams/components/StreamsList.tsx | 1 + src/services/liveActivityService.ts | 86 ++ src/utils/version.ts | 2 +- 33 files changed, 2038 insertions(+), 71 deletions(-) create mode 100644 ios/LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ios/LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/LiveActivity/Assets.xcassets/Contents.json create mode 100644 ios/LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 ios/LiveActivity/Color+hex.swift create mode 100644 ios/LiveActivity/Date+toTimerInterval.swift create mode 100644 ios/LiveActivity/Image+dynamic.swift create mode 100644 ios/LiveActivity/Info.plist create mode 100644 ios/LiveActivity/LiveActivity.entitlements create mode 100644 ios/LiveActivity/LiveActivityDebug.entitlements create mode 100644 ios/LiveActivity/LiveActivityView.swift create mode 100644 ios/LiveActivity/LiveActivityWidget.swift create mode 100644 ios/LiveActivity/LiveActivityWidgetBundle.swift create mode 100644 ios/LiveActivity/View+applyIfPresent.swift create mode 100644 ios/LiveActivity/View+applyWidgetURL.swift create mode 100644 ios/LiveActivity/ViewHelpers.swift create mode 100644 live.md create mode 100644 src/services/liveActivityService.ts diff --git a/app.json b/app.json index edcd738a..52f01dd0 100644 --- a/app.json +++ b/app.json @@ -67,6 +67,7 @@ }, "owner": "nayifleo", "plugins": [ + "expo-live-activity", [ "@sentry/react-native/expo", { diff --git a/ios/LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..0afb3cf0 --- /dev/null +++ b/ios/LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors": [ + { + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..c70a5bff --- /dev/null +++ b/ios/LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images": [ + { + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ], + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/LiveActivity/Assets.xcassets/Contents.json b/ios/LiveActivity/Assets.xcassets/Contents.json new file mode 100644 index 00000000..74d6a722 --- /dev/null +++ b/ios/LiveActivity/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json b/ios/LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000..0afb3cf0 --- /dev/null +++ b/ios/LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors": [ + { + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ios/LiveActivity/Color+hex.swift b/ios/LiveActivity/Color+hex.swift new file mode 100644 index 00000000..a93994a2 --- /dev/null +++ b/ios/LiveActivity/Color+hex.swift @@ -0,0 +1,37 @@ +import SwiftUI + +extension Color { + init(hex: String) { + var cString: String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + + if cString.hasPrefix("#") { + cString.remove(at: cString.startIndex) + } + + if (cString.count) != 6, (cString.count) != 8 { + self.init(.white) + return + } + + var rgbValue: UInt64 = 0 + Scanner(string: cString).scanHexInt64(&rgbValue) + + if (cString.count) == 8 { + self.init( + .sRGB, + red: Double((rgbValue >> 24) & 0xFF) / 255, + green: Double((rgbValue >> 16) & 0xFF) / 255, + blue: Double((rgbValue >> 08) & 0xFF) / 255, + opacity: Double((rgbValue >> 00) & 0xFF) / 255 + ) + } else { + self.init( + .sRGB, + red: Double((rgbValue >> 16) & 0xFF) / 255, + green: Double((rgbValue >> 08) & 0xFF) / 255, + blue: Double((rgbValue >> 00) & 0xFF) / 255, + opacity: 1 + ) + } + } +} diff --git a/ios/LiveActivity/Date+toTimerInterval.swift b/ios/LiveActivity/Date+toTimerInterval.swift new file mode 100644 index 00000000..3571bc8e --- /dev/null +++ b/ios/LiveActivity/Date+toTimerInterval.swift @@ -0,0 +1,7 @@ +import SwiftUI + +extension Date { + static func toTimerInterval(miliseconds: Double) -> ClosedRange { + now ... max(now, Date(timeIntervalSince1970: miliseconds / 1000)) + } +} diff --git a/ios/LiveActivity/Image+dynamic.swift b/ios/LiveActivity/Image+dynamic.swift new file mode 100644 index 00000000..f4814939 --- /dev/null +++ b/ios/LiveActivity/Image+dynamic.swift @@ -0,0 +1,33 @@ +import SwiftUI +import UIKit + +extension Image { + static func dynamic(assetNameOrPath: String) -> Self { + if let container = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.expoLiveActivity.sharedData" + ) { + let contentsOfFile = container.appendingPathComponent(assetNameOrPath).path + + if let uiImage = UIImage(contentsOfFile: contentsOfFile) { + return Image(uiImage: uiImage) + } + } + + return Image(assetNameOrPath) + } +} + +extension UIImage { + /// Attempts to load a UIImage either from the shared app group container or the main bundle. + static func dynamic(assetNameOrPath: String) -> UIImage? { + if let container = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.expoLiveActivity.sharedData" + ) { + let contentsOfFile = container.appendingPathComponent(assetNameOrPath).path + if let uiImage = UIImage(contentsOfFile: contentsOfFile) { + return uiImage + } + } + return UIImage(named: assetNameOrPath) + } +} diff --git a/ios/LiveActivity/Info.plist b/ios/LiveActivity/Info.plist new file mode 100644 index 00000000..0f118fb7 --- /dev/null +++ b/ios/LiveActivity/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/ios/LiveActivity/LiveActivity.entitlements b/ios/LiveActivity/LiveActivity.entitlements new file mode 100644 index 00000000..f683276c --- /dev/null +++ b/ios/LiveActivity/LiveActivity.entitlements @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ios/LiveActivity/LiveActivityDebug.entitlements b/ios/LiveActivity/LiveActivityDebug.entitlements new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/ios/LiveActivity/LiveActivityDebug.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/ios/LiveActivity/LiveActivityView.swift b/ios/LiveActivity/LiveActivityView.swift new file mode 100644 index 00000000..1a00e921 --- /dev/null +++ b/ios/LiveActivity/LiveActivityView.swift @@ -0,0 +1,247 @@ +import SwiftUI +import WidgetKit + +#if canImport(ActivityKit) + + struct ConditionalForegroundViewModifier: ViewModifier { + let color: String? + + func body(content: Content) -> some View { + if let color = color { + content.foregroundStyle(Color(hex: color)) + } else { + content + } + } + } + + struct DebugLog: View { + #if DEBUG + private let message: String + init(_ message: String) { + self.message = message + print(message) + } + + var body: some View { + Text(message) + .font(.caption2) + .foregroundStyle(.red) + } + #else + init(_: String) {} + var body: some View { EmptyView() } + #endif + } + + struct LiveActivityView: View { + let contentState: LiveActivityAttributes.ContentState + let attributes: LiveActivityAttributes + @State private var imageContainerSize: CGSize? + + var progressViewTint: Color? { + attributes.progressViewTint.map { Color(hex: $0) } + } + + private var imageAlignment: Alignment { + switch attributes.imageAlign { + case "center": + return .center + case "bottom": + return .bottom + default: + return .top + } + } + + private func alignedImage(imageName: String) -> some View { + let defaultHeight: CGFloat = 64 + let defaultWidth: CGFloat = 64 + let containerHeight = imageContainerSize?.height + let containerWidth = imageContainerSize?.width + let hasWidthConstraint = (attributes.imageWidthPercent != nil) || (attributes.imageWidth != nil) + + let computedHeight: CGFloat? = { + if let percent = attributes.imageHeightPercent { + let clamped = min(max(percent, 0), 100) / 100.0 + // Use the row height as a base. Fallback to default when row height is not measured yet. + let base = (containerHeight ?? defaultHeight) + return base * clamped + } else if let size = attributes.imageHeight { + return CGFloat(size) + } else if hasWidthConstraint { + // Mimic CSS: when only width is set, keep height automatic to preserve aspect ratio + return nil + } else { + // Mimic CSS: this works against CSS but provides a better default behavior. + // When no width/height is set, use a default size (64pt) + // Width will adjust automatically base on aspect ratio + return defaultHeight + } + }() + + let computedWidth: CGFloat? = { + if let percent = attributes.imageWidthPercent { + let clamped = min(max(percent, 0), 100) / 100.0 + let base = (containerWidth ?? defaultWidth) + return base * clamped + } else if let size = attributes.imageWidth { + return CGFloat(size) + } else { + return nil // Keep aspect fit based on height + } + }() + + return ZStack(alignment: .center) { + Group { + let fit = attributes.contentFit ?? "cover" + switch fit { + case "contain": + Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFit().frame(width: computedWidth, height: computedHeight) + case "fill": + Image.dynamic(assetNameOrPath: imageName).resizable().frame( + width: computedWidth, + height: computedHeight + ) + case "none": + Image.dynamic(assetNameOrPath: imageName).renderingMode(.original).frame(width: computedWidth, height: computedHeight) + case "scale-down": + if let uiImage = UIImage.dynamic(assetNameOrPath: imageName) { + // Determine the target box. When width/height are nil, we use image's intrinsic dimension for comparison. + let targetHeight = computedHeight ?? uiImage.size.height + let targetWidth = computedWidth ?? uiImage.size.width + let shouldScaleDown = uiImage.size.height > targetHeight || uiImage.size.width > targetWidth + + if shouldScaleDown { + Image(uiImage: uiImage) + .resizable() + .scaledToFit() + .frame(width: computedWidth, height: computedHeight) + } else { + Image(uiImage: uiImage) + .renderingMode(.original) + .frame(width: min(uiImage.size.width, targetWidth), height: min(uiImage.size.height, targetHeight)) + } + } else { + DebugLog("⚠️[ExpoLiveActivity] assetNameOrPath couldn't resolve to UIImage") + } + case "cover": + Image.dynamic(assetNameOrPath: imageName).resizable().scaledToFill().frame( + width: computedWidth, + height: computedHeight + ).clipped() + default: + DebugLog("⚠️[ExpoLiveActivity] Unknown contentFit '\(fit)'") + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: imageAlignment) + .background( + GeometryReader { proxy in + Color.clear + .onAppear { + let s = proxy.size + if s.width > 0, s.height > 0 { imageContainerSize = s } + } + .onChange(of: proxy.size) { s in + if s.width > 0, s.height > 0 { imageContainerSize = s } + } + } + ) + } + + var body: some View { + let defaultPadding = 24 + + let top = CGFloat( + attributes.paddingDetails?.top + ?? attributes.paddingDetails?.vertical + ?? attributes.padding + ?? defaultPadding + ) + + let bottom = CGFloat( + attributes.paddingDetails?.bottom + ?? attributes.paddingDetails?.vertical + ?? attributes.padding + ?? defaultPadding + ) + + let leading = CGFloat( + attributes.paddingDetails?.left + ?? attributes.paddingDetails?.horizontal + ?? attributes.padding + ?? defaultPadding + ) + + let trailing = CGFloat( + attributes.paddingDetails?.right + ?? attributes.paddingDetails?.horizontal + ?? attributes.padding + ?? defaultPadding + ) + + VStack(alignment: .leading) { + let position = attributes.imagePosition ?? "right" + let isStretch = position.contains("Stretch") + let isLeftImage = position.hasPrefix("left") + let hasImage = contentState.imageName != nil + let effectiveStretch = isStretch && hasImage + + HStack(alignment: .center) { + if hasImage, isLeftImage { + if let imageName = contentState.imageName { + alignedImage(imageName: imageName) + } + } + + VStack(alignment: .leading, spacing: 2) { + Text(contentState.title) + .font(.title2) + .fontWeight(.semibold) + .modifier(ConditionalForegroundViewModifier(color: attributes.titleColor)) + + if let subtitle = contentState.subtitle { + Text(subtitle) + .font(.title3) + .modifier(ConditionalForegroundViewModifier(color: attributes.subtitleColor)) + } + + if effectiveStretch { + if let date = contentState.timerEndDateInMilliseconds { + ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date)) + .tint(progressViewTint) + .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor)) + } else if let progress = contentState.progress { + ProgressView(value: progress) + .tint(progressViewTint) + .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor)) + } + } + }.layoutPriority(1) + + if hasImage, !isLeftImage { // right side (default) + Spacer() + if let imageName = contentState.imageName { + alignedImage(imageName: imageName) + } + } + } + + if !effectiveStretch { + if let date = contentState.timerEndDateInMilliseconds { + ProgressView(timerInterval: Date.toTimerInterval(miliseconds: date)) + .tint(progressViewTint) + .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor)) + } else if let progress = contentState.progress { + ProgressView(value: progress) + .tint(progressViewTint) + .modifier(ConditionalForegroundViewModifier(color: attributes.progressViewLabelColor)) + } + } + } + .padding(EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing)) + } + } + +#endif diff --git a/ios/LiveActivity/LiveActivityWidget.swift b/ios/LiveActivity/LiveActivityWidget.swift new file mode 100644 index 00000000..24cd8c57 --- /dev/null +++ b/ios/LiveActivity/LiveActivityWidget.swift @@ -0,0 +1,169 @@ +import ActivityKit +import SwiftUI +import WidgetKit + +struct LiveActivityAttributes: ActivityAttributes { + struct ContentState: Codable, Hashable { + var title: String + var subtitle: String? + var timerEndDateInMilliseconds: Double? + var progress: Double? + var imageName: String? + var dynamicIslandImageName: String? + } + + var name: String + var backgroundColor: String? + var titleColor: String? + var subtitleColor: String? + var progressViewTint: String? + var progressViewLabelColor: String? + var deepLinkUrl: String? + var timerType: DynamicIslandTimerType? + var padding: Int? + var paddingDetails: PaddingDetails? + var imagePosition: String? + var imageWidth: Int? + var imageHeight: Int? + var imageWidthPercent: Double? + var imageHeightPercent: Double? + var imageAlign: String? + var contentFit: String? + + enum DynamicIslandTimerType: String, Codable { + case circular + case digital + } + + struct PaddingDetails: Codable, Hashable { + var top: Int? + var bottom: Int? + var left: Int? + var right: Int? + var vertical: Int? + var horizontal: Int? + } +} + +struct LiveActivityWidget: Widget { + var body: some WidgetConfiguration { + ActivityConfiguration(for: LiveActivityAttributes.self) { context in + LiveActivityView(contentState: context.state, attributes: context.attributes) + .activityBackgroundTint( + context.attributes.backgroundColor.map { Color(hex: $0) } + ) + .activitySystemActionForegroundColor(Color.black) + .applyWidgetURL(from: context.attributes.deepLinkUrl) + } dynamicIsland: { context in + DynamicIsland { + DynamicIslandExpandedRegion(.leading, priority: 1) { + dynamicIslandExpandedLeading(title: context.state.title, subtitle: context.state.subtitle) + .dynamicIsland(verticalPlacement: .belowIfTooWide) + .padding(.leading, 5) + .applyWidgetURL(from: context.attributes.deepLinkUrl) + } + DynamicIslandExpandedRegion(.trailing) { + if let imageName = context.state.imageName { + dynamicIslandExpandedTrailing(imageName: imageName) + .padding(.trailing, 5) + .applyWidgetURL(from: context.attributes.deepLinkUrl) + } + } + DynamicIslandExpandedRegion(.bottom) { + if let date = context.state.timerEndDateInMilliseconds { + dynamicIslandExpandedBottom( + endDate: date, progressViewTint: context.attributes.progressViewTint + ) + .padding(.horizontal, 5) + .applyWidgetURL(from: context.attributes.deepLinkUrl) + } + } + } compactLeading: { + if let dynamicIslandImageName = context.state.dynamicIslandImageName { + resizableImage(imageName: dynamicIslandImageName) + .frame(maxWidth: 23, maxHeight: 23) + .applyWidgetURL(from: context.attributes.deepLinkUrl) + } + } compactTrailing: { + if let date = context.state.timerEndDateInMilliseconds { + compactTimer( + endDate: date, + timerType: context.attributes.timerType ?? .circular, + progressViewTint: context.attributes.progressViewTint + ).applyWidgetURL(from: context.attributes.deepLinkUrl) + } + } minimal: { + if let date = context.state.timerEndDateInMilliseconds { + compactTimer( + endDate: date, + timerType: context.attributes.timerType ?? .circular, + progressViewTint: context.attributes.progressViewTint + ).applyWidgetURL(from: context.attributes.deepLinkUrl) + } + } + } + } + + @ViewBuilder + private func compactTimer( + endDate: Double, + timerType: LiveActivityAttributes.DynamicIslandTimerType, + progressViewTint: String? + ) -> some View { + if timerType == .digital { + Text(timerInterval: Date.toTimerInterval(miliseconds: endDate)) + .font(.system(size: 15)) + .minimumScaleFactor(0.8) + .fontWeight(.semibold) + .frame(maxWidth: 60) + .multilineTextAlignment(.trailing) + } else { + circularTimer(endDate: endDate) + .tint(progressViewTint.map { Color(hex: $0) }) + } + } + + private func dynamicIslandExpandedLeading(title: String, subtitle: String?) -> some View { + VStack(alignment: .leading) { + Spacer() + Text(title) + .font(.title2) + .foregroundStyle(.white) + .fontWeight(.semibold) + if let subtitle { + Text(subtitle) + .font(.title3) + .minimumScaleFactor(0.8) + .foregroundStyle(.white.opacity(0.75)) + } + Spacer() + } + } + + private func dynamicIslandExpandedTrailing(imageName: String) -> some View { + VStack { + Spacer() + resizableImage(imageName: imageName) + Spacer() + } + } + + private func dynamicIslandExpandedBottom(endDate: Double, progressViewTint: String?) -> some View { + ProgressView(timerInterval: Date.toTimerInterval(miliseconds: endDate)) + .foregroundStyle(.white) + .tint(progressViewTint.map { Color(hex: $0) }) + .padding(.top, 5) + } + + private func circularTimer(endDate: Double) -> some View { + ProgressView( + timerInterval: Date.toTimerInterval(miliseconds: endDate), + countsDown: false, + label: { EmptyView() }, + currentValueLabel: { + EmptyView() + } + ) + .progressViewStyle(.circular) + } +} diff --git a/ios/LiveActivity/LiveActivityWidgetBundle.swift b/ios/LiveActivity/LiveActivityWidgetBundle.swift new file mode 100644 index 00000000..913ff4c2 --- /dev/null +++ b/ios/LiveActivity/LiveActivityWidgetBundle.swift @@ -0,0 +1,9 @@ +import SwiftUI +import WidgetKit + +@main +struct LiveActivityWidgetBundle: WidgetBundle { + var body: some Widget { + LiveActivityWidget() + } +} diff --git a/ios/LiveActivity/View+applyIfPresent.swift b/ios/LiveActivity/View+applyIfPresent.swift new file mode 100644 index 00000000..413b45a7 --- /dev/null +++ b/ios/LiveActivity/View+applyIfPresent.swift @@ -0,0 +1,12 @@ +import SwiftUI + +extension View { + @ViewBuilder + func applyIfPresent(_ value: T?, transform: (Self, T) -> some View) -> some View { + if let value { + transform(self, value) + } else { + self + } + } +} diff --git a/ios/LiveActivity/View+applyWidgetURL.swift b/ios/LiveActivity/View+applyWidgetURL.swift new file mode 100644 index 00000000..5fb1aa99 --- /dev/null +++ b/ios/LiveActivity/View+applyWidgetURL.swift @@ -0,0 +1,24 @@ +import SwiftUI + +private let cachedScheme: String? = { + guard + let urlTypes = Bundle.main.infoDictionary?["CFBundleURLTypes"] as? [[String: Any]], + let schemes = urlTypes.first?["CFBundleURLSchemes"] as? [String], + let firstScheme = schemes.first + else { + return nil + } + + return firstScheme +}() + +extension View { + @ViewBuilder + func applyWidgetURL(from urlString: String?) -> some View { + applyIfPresent(urlString) { view, string in + applyIfPresent(cachedScheme) { view, scheme in + view.widgetURL(URL(string: scheme + "://" + string)) + } + } + } +} diff --git a/ios/LiveActivity/ViewHelpers.swift b/ios/LiveActivity/ViewHelpers.swift new file mode 100644 index 00000000..1360c07c --- /dev/null +++ b/ios/LiveActivity/ViewHelpers.swift @@ -0,0 +1,33 @@ +import SwiftUI + +func resizableImage(imageName: String) -> some View { + Image.dynamic(assetNameOrPath: imageName) + .resizable() + .scaledToFit() +} + +func resizableImage(imageName: String, height: CGFloat?, width: CGFloat?) -> some View { + resizableImage(imageName: imageName) + .frame(width: width, height: height) +} + +private struct ContainerSizeKey: PreferenceKey { + static var defaultValue: CGSize? + static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) { + value = nextValue() ?? value + } +} + +extension View { + func captureContainerSize() -> some View { + background( + GeometryReader { proxy in + Color.clear.preference(key: ContainerSizeKey.self, value: proxy.size) + } + ) + } + + func onContainerSize(_ perform: @escaping (CGSize?) -> Void) -> some View { + onPreferenceChange(ContainerSizeKey.self, perform: perform) + } +} diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index f4ef9d51..73dbe99c 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -7,46 +7,213 @@ objects = { /* Begin PBXBuildFile section */ + 0E96D7F769C7466B98E90CCF /* Color+hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8034143A77A946B5A793F967 /* Color+hex.swift */; }; 0FFC28FB1FEA74CCFA112268 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 1AAD147C01BE4F3095CBE18E /* Image+dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26957CDD392E4E9390811D0D /* Image+dynamic.swift */; }; + 25E2CD29119A42F8B4B10C94 /* ViewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3396D68881EF486E99FD480A /* ViewHelpers.swift */; }; 2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; - 7928F4CD932E4A58C167CEFA /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BA547FB5FF593C6C2371C8 /* libPods-Nuvio.a */; }; - 9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */; }; - 9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */; }; - 9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */; }; - 9FBA88F72E86ECD700892850 /* KSPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */; }; + 3ED3EBF92E41439593E50917 /* Date+toTimerInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48D8A298DD48928E8D0A02 /* Date+toTimerInterval.swift */; }; + 442B57EB40BD413AB3235C96 /* LiveActivity.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 174D9F016C3546FEB7D40900 /* LiveActivity.appex */; }; + 4BBD968F8EF647E08BD3AF50 /* LiveActivity.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DAECB06E5D1D4976B214EF20 /* LiveActivity.appex */; }; + 6B5330DC8F6D4F54B623833E /* View+applyIfPresent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324373F393774A9CA40DE22E /* View+applyIfPresent.swift */; }; + 730F1CD42F24B27100EF7E51 /* Color+hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8034143A77A946B5A793F967 /* Color+hex.swift */; }; + 730F1CD52F24B27100EF7E51 /* Date+toTimerInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48D8A298DD48928E8D0A02 /* Date+toTimerInterval.swift */; }; + 730F1CD62F24B27100EF7E51 /* Image+dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26957CDD392E4E9390811D0D /* Image+dynamic.swift */; }; + 730F1CD72F24B27100EF7E51 /* LiveActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F448294A36E433E924078C1 /* LiveActivityView.swift */; }; + 730F1CD82F24B27100EF7E51 /* LiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD48662BB71E4C9C9E340289 /* LiveActivityWidget.swift */; }; + 730F1CD92F24B27100EF7E51 /* LiveActivityWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83D742B36224176A0AB3B25 /* LiveActivityWidgetBundle.swift */; }; + 730F1CDA2F24B27100EF7E51 /* View+applyIfPresent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324373F393774A9CA40DE22E /* View+applyIfPresent.swift */; }; + 730F1CDB2F24B27100EF7E51 /* View+applyWidgetURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D1473F5A74CBC9DBD108B /* View+applyWidgetURL.swift */; }; + 730F1CDC2F24B27100EF7E51 /* ViewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3396D68881EF486E99FD480A /* ViewHelpers.swift */; }; + 730F1CDD2F24B27100EF7E51 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0F1D0037D1F24E60BDB57628 /* Assets.xcassets */; }; + 730F1CDE2F24B27100EF7E51 /* Color+hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8034143A77A946B5A793F967 /* Color+hex.swift */; }; + 730F1CDF2F24B27100EF7E51 /* Date+toTimerInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48D8A298DD48928E8D0A02 /* Date+toTimerInterval.swift */; }; + 730F1CE02F24B27100EF7E51 /* Image+dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26957CDD392E4E9390811D0D /* Image+dynamic.swift */; }; + 730F1CE12F24B27100EF7E51 /* LiveActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F448294A36E433E924078C1 /* LiveActivityView.swift */; }; + 730F1CE22F24B27100EF7E51 /* LiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD48662BB71E4C9C9E340289 /* LiveActivityWidget.swift */; }; + 730F1CE32F24B27100EF7E51 /* LiveActivityWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83D742B36224176A0AB3B25 /* LiveActivityWidgetBundle.swift */; }; + 730F1CE42F24B27100EF7E51 /* View+applyIfPresent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324373F393774A9CA40DE22E /* View+applyIfPresent.swift */; }; + 730F1CE52F24B27100EF7E51 /* View+applyWidgetURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D1473F5A74CBC9DBD108B /* View+applyWidgetURL.swift */; }; + 730F1CE62F24B27100EF7E51 /* ViewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3396D68881EF486E99FD480A /* ViewHelpers.swift */; }; + 730F1CE72F24B27100EF7E51 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0F1D0037D1F24E60BDB57628 /* Assets.xcassets */; }; + 730F1CE92F24B3B900EF7E51 /* Color+hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8034143A77A946B5A793F967 /* Color+hex.swift */; }; + 730F1CEA2F24B3B900EF7E51 /* Date+toTimerInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48D8A298DD48928E8D0A02 /* Date+toTimerInterval.swift */; }; + 730F1CEB2F24B3B900EF7E51 /* Image+dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26957CDD392E4E9390811D0D /* Image+dynamic.swift */; }; + 730F1CEC2F24B3B900EF7E51 /* LiveActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F448294A36E433E924078C1 /* LiveActivityView.swift */; }; + 730F1CED2F24B3B900EF7E51 /* LiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD48662BB71E4C9C9E340289 /* LiveActivityWidget.swift */; }; + 730F1CEE2F24B3B900EF7E51 /* LiveActivityWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83D742B36224176A0AB3B25 /* LiveActivityWidgetBundle.swift */; }; + 730F1CEF2F24B3B900EF7E51 /* View+applyIfPresent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324373F393774A9CA40DE22E /* View+applyIfPresent.swift */; }; + 730F1CF02F24B3B900EF7E51 /* View+applyWidgetURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D1473F5A74CBC9DBD108B /* View+applyWidgetURL.swift */; }; + 730F1CF12F24B3B900EF7E51 /* ViewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3396D68881EF486E99FD480A /* ViewHelpers.swift */; }; + 730F1CF22F24B3B900EF7E51 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0F1D0037D1F24E60BDB57628 /* Assets.xcassets */; }; + 9354055245994CB4B766CACB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0F1D0037D1F24E60BDB57628 /* Assets.xcassets */; }; + 9FBA88F42E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F32E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerViewManager.swift */; }; + 9FBA88F52E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerModule.swift */; }; + 9FBA88F62E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F02E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerManager.m */; }; + 9FBA88F72E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F22E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerView.swift */; }; + 9FE5BDAA6F674625BECBE154 /* LiveActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F448294A36E433E924078C1 /* LiveActivityView.swift */; }; + A0892AA96024D9EF7CA87A8A /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 349BFD3B214640DED8541999 /* libPods-Nuvio.a */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + C020F93B2978487F8ADD648C /* LiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD48662BB71E4C9C9E340289 /* LiveActivityWidget.swift */; }; + DA302E9BE499446E8C981931 /* LiveActivity.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F41133061DC54800BF38011F /* LiveActivity.appex */; }; + EC582C023B8B431C8F174188 /* View+applyWidgetURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D1473F5A74CBC9DBD108B /* View+applyWidgetURL.swift */; }; F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; + F285A1620F5847BA863124AF /* LiveActivity.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EF8716173E0148BD82B233B7 /* LiveActivity.appex */; }; + FAF84635E95E474983F04A85 /* LiveActivityWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83D742B36224176A0AB3B25 /* LiveActivityWidgetBundle.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 3B0AE48F9E2E42D497EE475D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DA7D9B4517F7460AA540FF21; + remoteInfo = LiveActivity; + }; + 42A51162762645A3AE54FE44 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1EBBAD76A33B4602849CA169; + remoteInfo = LiveActivity; + }; + 55A0DD628D7F4F4F88B4A001 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0EA489F2BF6143F1BA7B8485; + remoteInfo = LiveActivity; + }; + 7E1DE2E1924249A899602ABA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1192E8CDD15C43C592005B3F; + remoteInfo = LiveActivity; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 13CD9594FB5C4FE4A6794089 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 3447F08B99D9427E99FEE18E /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 4BBD968F8EF647E08BD3AF50 /* LiveActivity.appex in Embed Foundation Extensions */, + 442B57EB40BD413AB3235C96 /* LiveActivity.appex in Embed Foundation Extensions */, + F285A1620F5847BA863124AF /* LiveActivity.appex in Embed Foundation Extensions */, + DA302E9BE499446E8C981931 /* LiveActivity.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 571AD3FB23F14FC7BE6A1E44 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + BDCAC5D772944755921F3BCF /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 0DFF64A670930CED5EA4DF3A /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = ""; }; + 0E13CE4BDE2F4555806AE753 /* Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0F1D0037D1F24E60BDB57628 /* Assets.xcassets */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* Nuvio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nuvio.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = ""; }; + 174D9F016C3546FEB7D40900 /* LiveActivity.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; fileEncoding = 9; includeInIndex = 0; path = LiveActivity.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 26957CDD392E4E9390811D0D /* Image+dynamic.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = "Image+dynamic.swift"; sourceTree = ""; }; + 2DE29A8A87D24662BEFFF849 /* LiveActivity.entitlements */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; path = LiveActivity.entitlements; sourceTree = ""; }; + 2F448294A36E433E924078C1 /* LiveActivityView.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = LiveActivityView.swift; sourceTree = ""; }; + 324373F393774A9CA40DE22E /* View+applyIfPresent.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = "View+applyIfPresent.swift"; sourceTree = ""; }; + 3396D68881EF486E99FD480A /* ViewHelpers.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = ViewHelpers.swift; sourceTree = ""; }; + 349BFD3B214640DED8541999 /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 373D1473F5A74CBC9DBD108B /* View+applyWidgetURL.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = "View+applyWidgetURL.swift"; sourceTree = ""; }; + 3A48D8A298DD48928E8D0A02 /* Date+toTimerInterval.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = "Date+toTimerInterval.swift"; sourceTree = ""; }; 49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = ""; }; 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = ""; }; - 6E529CB0ACBADCCC9E9F1C34 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = ""; }; + 730F1CE82F24B29C00EF7E51 /* LiveActivityDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LiveActivityDebug.entitlements; sourceTree = ""; }; 73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; sourceTree = ""; }; - 872F6D9F073913A5EBC6DDAC /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = ""; }; - 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ../KSPlayer/RNBridge/KSPlayerManager.m; sourceTree = ""; }; - 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerModule.swift; sourceTree = ""; }; - 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerView.swift; sourceTree = ""; }; - 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerViewManager.swift; sourceTree = ""; }; + 8034143A77A946B5A793F967 /* Color+hex.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = "Color+hex.swift"; sourceTree = ""; }; + 9FBA88F02E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ../KSPlayer/RNBridge/KSPlayerManager.m; sourceTree = ""; }; + 9FBA88F12E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerModule.swift; sourceTree = ""; }; + 9FBA88F22E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerView.swift; sourceTree = ""; }; + 9FBA88F32E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerViewManager.swift; sourceTree = ""; }; + A83D742B36224176A0AB3B25 /* LiveActivityWidgetBundle.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = LiveActivityWidgetBundle.swift; sourceTree = ""; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Nuvio/SplashScreen.storyboard; sourceTree = ""; }; + AD48662BB71E4C9C9E340289 /* LiveActivityWidget.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = LiveActivityWidget.swift; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; - D0BA547FB5FF593C6C2371C8 /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + DAD634845937EAF8D64F20FC /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = ""; }; + DAECB06E5D1D4976B214EF20 /* LiveActivity.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; fileEncoding = 9; includeInIndex = 0; path = LiveActivity.appex; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + EF8716173E0148BD82B233B7 /* LiveActivity.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; fileEncoding = 9; includeInIndex = 0; path = LiveActivity.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Nuvio/AppDelegate.swift; sourceTree = ""; }; F11748442D0722820044C1D9 /* Nuvio-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Nuvio-Bridging-Header.h"; path = "Nuvio/Nuvio-Bridging-Header.h"; sourceTree = ""; }; + F41133061DC54800BF38011F /* LiveActivity.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; fileEncoding = 9; includeInIndex = 0; path = LiveActivity.appex; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 10B2169CBC8644C89561BDE0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7928F4CD932E4A58C167CEFA /* libPods-Nuvio.a in Frameworks */, + A0892AA96024D9EF7CA87A8A /* libPods-Nuvio.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2B2031F57AAE42E18BD48F61 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39FE147C4CF348D788956253 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C105694FF46449959CE16947 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; @@ -59,10 +226,10 @@ 73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */, F11748412D0307B40044C1D9 /* AppDelegate.swift */, F11748442D0722820044C1D9 /* Nuvio-Bridging-Header.h */, - 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */, - 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */, - 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */, - 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */, + 9FBA88F02E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerManager.m */, + 9FBA88F12E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerModule.swift */, + 9FBA88F22E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerView.swift */, + 9FBA88F32E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerViewManager.swift */, BB2F792B24A3F905000567C9 /* Supporting */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, @@ -76,7 +243,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - D0BA547FB5FF593C6C2371C8 /* libPods-Nuvio.a */, + 349BFD3B214640DED8541999 /* libPods-Nuvio.a */, ); name = Frameworks; sourceTree = ""; @@ -89,6 +256,13 @@ name = ExpoModulesProviders; sourceTree = ""; }; + 62B088ADB2A740DAB9E343F9 /* LiveActivity */ = { + isa = PBXGroup; + children = ( + ); + path = LiveActivity; + sourceTree = ""; + }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( @@ -105,6 +279,10 @@ 2D16E6871FA4F8E400B85C8A /* Frameworks */, D90A3959C97EE9926C513293 /* Pods */, 358C5C99C443A921C8EEDDC8 /* ExpoModulesProviders */, + E8C72B3DF7DB40A8896F56C9 /* LiveActivity */, + 62B088ADB2A740DAB9E343F9 /* LiveActivity */, + B9F3EB198DED443D980ADFB3 /* LiveActivity */, + C05E525650E143FB85ED7622 /* LiveActivity */, ); indentWidth = 2; sourceTree = ""; @@ -115,10 +293,21 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* Nuvio.app */, + DAECB06E5D1D4976B214EF20 /* LiveActivity.appex */, + 174D9F016C3546FEB7D40900 /* LiveActivity.appex */, + EF8716173E0148BD82B233B7 /* LiveActivity.appex */, + F41133061DC54800BF38011F /* LiveActivity.appex */, ); name = Products; sourceTree = ""; }; + B9F3EB198DED443D980ADFB3 /* LiveActivity */ = { + isa = PBXGroup; + children = ( + ); + path = LiveActivity; + sourceTree = ""; + }; BB2F792B24A3F905000567C9 /* Supporting */ = { isa = PBXGroup; children = ( @@ -128,15 +317,42 @@ path = Nuvio/Supporting; sourceTree = ""; }; + C05E525650E143FB85ED7622 /* LiveActivity */ = { + isa = PBXGroup; + children = ( + 8034143A77A946B5A793F967 /* Color+hex.swift */, + 3A48D8A298DD48928E8D0A02 /* Date+toTimerInterval.swift */, + 26957CDD392E4E9390811D0D /* Image+dynamic.swift */, + 2F448294A36E433E924078C1 /* LiveActivityView.swift */, + AD48662BB71E4C9C9E340289 /* LiveActivityWidget.swift */, + A83D742B36224176A0AB3B25 /* LiveActivityWidgetBundle.swift */, + 324373F393774A9CA40DE22E /* View+applyIfPresent.swift */, + 373D1473F5A74CBC9DBD108B /* View+applyWidgetURL.swift */, + 3396D68881EF486E99FD480A /* ViewHelpers.swift */, + 0E13CE4BDE2F4555806AE753 /* Info.plist */, + 0F1D0037D1F24E60BDB57628 /* Assets.xcassets */, + 2DE29A8A87D24662BEFFF849 /* LiveActivity.entitlements */, + ); + path = LiveActivity; + sourceTree = ""; + }; D90A3959C97EE9926C513293 /* Pods */ = { isa = PBXGroup; children = ( - 872F6D9F073913A5EBC6DDAC /* Pods-Nuvio.debug.xcconfig */, - 6E529CB0ACBADCCC9E9F1C34 /* Pods-Nuvio.release.xcconfig */, + DAD634845937EAF8D64F20FC /* Pods-Nuvio.debug.xcconfig */, + 0DFF64A670930CED5EA4DF3A /* Pods-Nuvio.release.xcconfig */, ); path = Pods; sourceTree = ""; }; + E8C72B3DF7DB40A8896F56C9 /* LiveActivity */ = { + isa = PBXGroup; + children = ( + 730F1CE82F24B29C00EF7E51 /* LiveActivityDebug.entitlements */, + ); + path = LiveActivity; + sourceTree = ""; + }; ECB31D9B6FF08C7E8E875650 /* Nuvio */ = { isa = PBXGroup; children = ( @@ -148,29 +364,105 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 0EA489F2BF6143F1BA7B8485 /* LiveActivity */ = { + isa = PBXNativeTarget; + buildConfigurationList = C95083D445BA485B82D2FFBC /* Build configuration list for PBXNativeTarget "LiveActivity" */; + buildPhases = ( + 6E9A0429F8E74948A82DEFF5 /* Sources */, + C105694FF46449959CE16947 /* Frameworks */, + 1E668E0B92C34E73AECDBE1A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LiveActivity; + productName = LiveActivity; + productReference = EF8716173E0148BD82B233B7 /* LiveActivity.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 1192E8CDD15C43C592005B3F /* LiveActivity */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEA87ACBA91F40F7AA726F19 /* Build configuration list for PBXNativeTarget "LiveActivity" */; + buildPhases = ( + 81117DEE4F2545729BDF03F7 /* Sources */, + 2B2031F57AAE42E18BD48F61 /* Frameworks */, + C2800F4B95084F8AA80AC611 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LiveActivity; + productName = LiveActivity; + productReference = F41133061DC54800BF38011F /* LiveActivity.appex */; + productType = "com.apple.product-type.app-extension"; + }; 13B07F861A680F5B00A75B9A /* Nuvio */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */; buildPhases = ( - 2F7776C5CF342CF6C593386C /* [CP] Check Pods Manifest.lock */, + 13C7A3175A582B3D4E9F198E /* [CP] Check Pods Manifest.lock */, 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */, - 0274FB5ED7475B1802656938 /* [CP] Embed Pods Frameworks */, - 8A1BEDBBF6815E406699791F /* [CP] Copy Pods Resources */, + E043D7E00F2210228303FC0B /* [CP] Embed Pods Frameworks */, + 7F1DFB9D902E2DBC35F3FB84 /* [CP] Copy Pods Resources */, + 3447F08B99D9427E99FEE18E /* Embed Foundation Extensions */, + BDCAC5D772944755921F3BCF /* Embed Foundation Extensions */, + 571AD3FB23F14FC7BE6A1E44 /* Embed Foundation Extensions */, + 13CD9594FB5C4FE4A6794089 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 978589C2E9184DC9AD20A587 /* PBXTargetDependency */, + 60E6A74A9C404C8FBD39B48B /* PBXTargetDependency */, + 8410CAE82E604DD1A187EDA2 /* PBXTargetDependency */, + D012EFA90D7F4BD5B0693671 /* PBXTargetDependency */, ); name = Nuvio; productName = Nuvio; productReference = 13B07F961A680F5B00A75B9A /* Nuvio.app */; productType = "com.apple.product-type.application"; }; + 1EBBAD76A33B4602849CA169 /* LiveActivity */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2FD0D2A66BEF47A0B8CEE6D9 /* Build configuration list for PBXNativeTarget "LiveActivity" */; + buildPhases = ( + 7207870795924249B8C6B3F0 /* Sources */, + 10B2169CBC8644C89561BDE0 /* Frameworks */, + 26E6801BEE58441EA991EE4D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LiveActivity; + productName = LiveActivity; + productReference = DAECB06E5D1D4976B214EF20 /* LiveActivity.appex */; + productType = "com.apple.product-type.app-extension"; + }; + DA7D9B4517F7460AA540FF21 /* LiveActivity */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1ED2052A55CE4E65B5B369AF /* Build configuration list for PBXNativeTarget "LiveActivity" */; + buildPhases = ( + B0655D9B3D9B4AC383E042F7 /* Sources */, + 39FE147C4CF348D788956253 /* Frameworks */, + B6435BB459A04ECB9182B634 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LiveActivity; + productName = LiveActivity; + productReference = 174D9F016C3546FEB7D40900 /* LiveActivity.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -179,8 +471,28 @@ attributes = { LastUpgradeCheck = 1130; TargetAttributes = { - 13B07F861A680F5B00A75B9A = { + 0EA489F2BF6143F1BA7B8485 = { + DevelopmentTeam = 8QBDZ766S3; LastSwiftMigration = 1250; + ProvisioningStyle = Automatic; + }; + 1192E8CDD15C43C592005B3F = { + LastSwiftMigration = 1250; + }; + 13B07F861A680F5B00A75B9A = { + DevelopmentTeam = 8QBDZ766S3; + LastSwiftMigration = 1250; + ProvisioningStyle = Automatic; + }; + 1EBBAD76A33B4602849CA169 = { + DevelopmentTeam = 8QBDZ766S3; + LastSwiftMigration = 1250; + ProvisioningStyle = Automatic; + }; + DA7D9B4517F7460AA540FF21 = { + DevelopmentTeam = 8QBDZ766S3; + LastSwiftMigration = 1250; + ProvisioningStyle = Automatic; }; }; }; @@ -198,6 +510,10 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* Nuvio */, + 1EBBAD76A33B4602849CA169 /* LiveActivity */, + DA7D9B4517F7460AA540FF21 /* LiveActivity */, + 0EA489F2BF6143F1BA7B8485 /* LiveActivity */, + 1192E8CDD15C43C592005B3F /* LiveActivity */, ); }; /* End PBXProject section */ @@ -214,6 +530,38 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 1E668E0B92C34E73AECDBE1A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 730F1CE72F24B27100EF7E51 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 26E6801BEE58441EA991EE4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9354055245994CB4B766CACB /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6435BB459A04ECB9182B634 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 730F1CDD2F24B27100EF7E51 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C2800F4B95084F8AA80AC611 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 730F1CF22F24B3B900EF7E51 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -234,29 +582,7 @@ shellPath = /bin/sh; shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; }; - 0274FB5ED7475B1802656938 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 2F7776C5CF342CF6C593386C /* [CP] Check Pods Manifest.lock */ = { + 13C7A3175A582B3D4E9F198E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -278,7 +604,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 8A1BEDBBF6815E406699791F /* [CP] Copy Pods Resources */ = { + 7F1DFB9D902E2DBC35F3FB84 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -428,6 +754,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; }; + E043D7E00F2210228303FC0B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -436,24 +784,136 @@ buildActionMask = 2147483647; files = ( F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, - 9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */, - 9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */, - 9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */, - 9FBA88F72E86ECD700892850 /* KSPlayerView.swift in Sources */, + 9FBA88F42E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerViewManager.swift in Sources */, + 9FBA88F52E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerModule.swift in Sources */, + 9FBA88F62E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerManager.m in Sources */, + 9FBA88F72E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerView.swift in Sources */, 2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 6E9A0429F8E74948A82DEFF5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 730F1CDE2F24B27100EF7E51 /* Color+hex.swift in Sources */, + 730F1CDF2F24B27100EF7E51 /* Date+toTimerInterval.swift in Sources */, + 730F1CE02F24B27100EF7E51 /* Image+dynamic.swift in Sources */, + 730F1CE12F24B27100EF7E51 /* LiveActivityView.swift in Sources */, + 730F1CE22F24B27100EF7E51 /* LiveActivityWidget.swift in Sources */, + 730F1CE32F24B27100EF7E51 /* LiveActivityWidgetBundle.swift in Sources */, + 730F1CE42F24B27100EF7E51 /* View+applyIfPresent.swift in Sources */, + 730F1CE52F24B27100EF7E51 /* View+applyWidgetURL.swift in Sources */, + 730F1CE62F24B27100EF7E51 /* ViewHelpers.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7207870795924249B8C6B3F0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0E96D7F769C7466B98E90CCF /* Color+hex.swift in Sources */, + 3ED3EBF92E41439593E50917 /* Date+toTimerInterval.swift in Sources */, + 1AAD147C01BE4F3095CBE18E /* Image+dynamic.swift in Sources */, + 9FE5BDAA6F674625BECBE154 /* LiveActivityView.swift in Sources */, + C020F93B2978487F8ADD648C /* LiveActivityWidget.swift in Sources */, + FAF84635E95E474983F04A85 /* LiveActivityWidgetBundle.swift in Sources */, + 6B5330DC8F6D4F54B623833E /* View+applyIfPresent.swift in Sources */, + EC582C023B8B431C8F174188 /* View+applyWidgetURL.swift in Sources */, + 25E2CD29119A42F8B4B10C94 /* ViewHelpers.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81117DEE4F2545729BDF03F7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 730F1CE92F24B3B900EF7E51 /* Color+hex.swift in Sources */, + 730F1CEA2F24B3B900EF7E51 /* Date+toTimerInterval.swift in Sources */, + 730F1CEB2F24B3B900EF7E51 /* Image+dynamic.swift in Sources */, + 730F1CEC2F24B3B900EF7E51 /* LiveActivityView.swift in Sources */, + 730F1CED2F24B3B900EF7E51 /* LiveActivityWidget.swift in Sources */, + 730F1CEE2F24B3B900EF7E51 /* LiveActivityWidgetBundle.swift in Sources */, + 730F1CEF2F24B3B900EF7E51 /* View+applyIfPresent.swift in Sources */, + 730F1CF02F24B3B900EF7E51 /* View+applyWidgetURL.swift in Sources */, + 730F1CF12F24B3B900EF7E51 /* ViewHelpers.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B0655D9B3D9B4AC383E042F7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 730F1CD42F24B27100EF7E51 /* Color+hex.swift in Sources */, + 730F1CD52F24B27100EF7E51 /* Date+toTimerInterval.swift in Sources */, + 730F1CD62F24B27100EF7E51 /* Image+dynamic.swift in Sources */, + 730F1CD72F24B27100EF7E51 /* LiveActivityView.swift in Sources */, + 730F1CD82F24B27100EF7E51 /* LiveActivityWidget.swift in Sources */, + 730F1CD92F24B27100EF7E51 /* LiveActivityWidgetBundle.swift in Sources */, + 730F1CDA2F24B27100EF7E51 /* View+applyIfPresent.swift in Sources */, + 730F1CDB2F24B27100EF7E51 /* View+applyWidgetURL.swift in Sources */, + 730F1CDC2F24B27100EF7E51 /* ViewHelpers.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 60E6A74A9C404C8FBD39B48B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DA7D9B4517F7460AA540FF21 /* LiveActivity */; + targetProxy = 3B0AE48F9E2E42D497EE475D /* PBXContainerItemProxy */; + }; + 8410CAE82E604DD1A187EDA2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0EA489F2BF6143F1BA7B8485 /* LiveActivity */; + targetProxy = 55A0DD628D7F4F4F88B4A001 /* PBXContainerItemProxy */; + }; + 978589C2E9184DC9AD20A587 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1EBBAD76A33B4602849CA169 /* LiveActivity */; + targetProxy = 42A51162762645A3AE54FE44 /* PBXContainerItemProxy */; + }; + D012EFA90D7F4BD5B0693671 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1192E8CDD15C43C592005B3F /* LiveActivity */; + targetProxy = 7E1DE2E1924249A899602ABA /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + 04F118641DEC4E9C918EBAAD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LiveActivity/LiveActivity.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 34; + DEVELOPMENT_TEAM = 8QBDZ766S3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LiveActivity/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.3.6; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app.LiveActivity; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 872F6D9F073913A5EBC6DDAC /* Pods-Nuvio.debug.xcconfig */; + baseConfigurationReference = DAD634845937EAF8D64F20FC /* Pods-Nuvio.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Nuvio/Nuvio.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8QBDZ766S3; ENABLE_BITCODE = NO; @@ -474,7 +934,7 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub; PRODUCT_NAME = Nuvio; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; @@ -487,11 +947,13 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6E529CB0ACBADCCC9E9F1C34 /* Pods-Nuvio.release.xcconfig */; + baseConfigurationReference = 0DFF64A670930CED5EA4DF3A /* Pods-Nuvio.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Nuvio/NuvioRelease.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8QBDZ766S3; INFOPLIST_FILE = Nuvio/Info.plist; @@ -507,7 +969,7 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; PRODUCT_NAME = Nuvio; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; @@ -517,6 +979,93 @@ }; name = Release; }; + 38FB0C6278F44A388F0B7111 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LiveActivity/LiveActivity.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 34; + DEVELOPMENT_TEAM = 8QBDZ766S3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LiveActivity/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.3.6; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app.LiveActivity; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3DCEA1FBF99E46F58A7150CC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LiveActivity/LiveActivity.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 34; + DEVELOPMENT_TEAM = 8QBDZ766S3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LiveActivity/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.3.6; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub.LiveActivity; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5F8D0112D2E24FF0A9992E15 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LiveActivity/LiveActivity.entitlements; + CURRENT_PROJECT_VERSION = 34; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LiveActivity/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.3.6; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app.LiveActivity; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 649842CAF51F469AAEDFB4DE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LiveActivity/LiveActivity.entitlements; + CURRENT_PROJECT_VERSION = 34; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LiveActivity/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.3.6; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app.LiveActivity; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -635,6 +1184,76 @@ }; name = Release; }; + B38CBD036A874B33A66DF5E7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LiveActivity/LiveActivityDebug.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 34; + DEVELOPMENT_TEAM = 8QBDZ766S3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LiveActivity/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.3.6; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub.LiveActivity; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C054C2620F32443A8467C53C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LiveActivity/LiveActivity.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 34; + DEVELOPMENT_TEAM = 8QBDZ766S3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LiveActivity/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.3.6; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app.LiveActivity; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E4108F64486C48E192EAA45D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = LiveActivity/LiveActivity.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 34; + DEVELOPMENT_TEAM = 8QBDZ766S3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LiveActivity/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.3.6; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub.LiveActivity; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -647,6 +1266,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 1ED2052A55CE4E65B5B369AF /* Build configuration list for PBXNativeTarget "LiveActivity" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C054C2620F32443A8467C53C /* Debug */, + 04F118641DEC4E9C918EBAAD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2FD0D2A66BEF47A0B8CEE6D9 /* Build configuration list for PBXNativeTarget "LiveActivity" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B38CBD036A874B33A66DF5E7 /* Debug */, + 38FB0C6278F44A388F0B7111 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Nuvio" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -656,6 +1293,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + C95083D445BA485B82D2FFBC /* Build configuration list for PBXNativeTarget "LiveActivity" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3DCEA1FBF99E46F58A7150CC /* Debug */, + E4108F64486C48E192EAA45D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CEA87ACBA91F40F7AA726F19 /* Build configuration list for PBXNativeTarget "LiveActivity" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5F8D0112D2E24FF0A9992E15 /* Debug */, + 649842CAF51F469AAEDFB4DE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; diff --git a/ios/Nuvio/AppDelegate.swift b/ios/Nuvio/AppDelegate.swift index 4364d816..be0e7f70 100644 --- a/ios/Nuvio/AppDelegate.swift +++ b/ios/Nuvio/AppDelegate.swift @@ -84,4 +84,13 @@ class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { return Bundle.main.url(forResource: "main", withExtension: "jsbundle") #endif } + + func application( + _ application: UIApplication, + handleEventsForBackgroundURLSession identifier: String, + completionHandler: @escaping () -> Void + ) { + RNBackgroundDownloader.setCompletionHandlerWithIdentifier(identifier, completionHandler: completionHandler) + } + } diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index f19c0554..6f6a9f72 100644 --- a/ios/Nuvio/Info.plist +++ b/ios/Nuvio/Info.plist @@ -58,9 +58,13 @@ _CC1AD845._googlecast._tcp NSLocalNetworkUsageDescription - Allow $(PRODUCT_NAME) to access your local network + Nuvio uses the local network to discover Cast-enabled devices on your WiFi network and to connect to local media servers. NSMicrophoneUsageDescription This app does not require microphone access. + NSSupportsLiveActivities + + NSSupportsLiveActivitiesFrequentUpdates + RCTNewArchEnabled RCTRootViewBackgroundColor diff --git a/ios/Nuvio/Nuvio-Bridging-Header.h b/ios/Nuvio/Nuvio-Bridging-Header.h index 8361941a..52fbe8b4 100644 --- a/ios/Nuvio/Nuvio-Bridging-Header.h +++ b/ios/Nuvio/Nuvio-Bridging-Header.h @@ -1,3 +1,4 @@ // // Use this file to import your target's public headers that you would like to expose to Swift. // +#import diff --git a/ios/Nuvio/Supporting/Expo.plist b/ios/Nuvio/Supporting/Expo.plist index 9b1d70a1..ac3efa6f 100644 --- a/ios/Nuvio/Supporting/Expo.plist +++ b/ios/Nuvio/Supporting/Expo.plist @@ -9,7 +9,7 @@ EXUpdatesLaunchWaitMs 30000 EXUpdatesRuntimeVersion - 1.2.11 + 1.3.6 EXUpdatesURL https://ota.nuvioapp.space/api/manifest diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9372bfb7..af7d24b3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -235,6 +235,8 @@ PODS: - ExpoModulesCore - ExpoLinking (8.0.10): - ExpoModulesCore + - ExpoLiveActivity (0.4.2): + - ExpoModulesCore - ExpoLocalization (17.0.8): - ExpoModulesCore - ExpoModulesCore (3.0.29): @@ -2811,6 +2813,7 @@ DEPENDENCIES: - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`) - ExpoLinking (from `../node_modules/expo-linking/ios`) + - ExpoLiveActivity (from `../node_modules/expo-live-activity/ios`) - ExpoLocalization (from `../node_modules/expo-localization/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoRandom (from `../node_modules/expo-random/ios`) @@ -2988,6 +2991,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-linear-gradient/ios" ExpoLinking: :path: "../node_modules/expo-linking/ios" + ExpoLiveActivity: + :path: "../node_modules/expo-live-activity/ios" ExpoLocalization: :path: "../node_modules/expo-localization/ios" ExpoModulesCore: @@ -3235,6 +3240,7 @@ SPEC CHECKSUMS: ExpoKeepAwake: 55f75eca6499bb9e4231ebad6f3e9cb8f99c0296 ExpoLinearGradient: 809102bdb979f590083af49f7fa4805cd931bd58 ExpoLinking: f4c4a351523da72a6bfa7e1f4ca92aee1043a3ca + ExpoLiveActivity: d0dd0e8e1460b6b26555b611c4826cdb1036eea2 ExpoLocalization: d9168d5300a5b03e5e78b986124d11fb6ec3ebbd ExpoModulesCore: f3da4f1ab5a8375d0beafab763739dbee8446583 ExpoRandom: d1444df65007bdd4070009efd5dab18e20bf0f00 diff --git a/ios/Podfile.properties.json b/ios/Podfile.properties.json index 42ffb6cb..1119e5b5 100644 --- a/ios/Podfile.properties.json +++ b/ios/Podfile.properties.json @@ -3,4 +3,4 @@ "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", "newArchEnabled": "true", "ios.deploymentTarget": "16.0" -} \ No newline at end of file +} diff --git a/live.md b/live.md new file mode 100644 index 00000000..a166d96b --- /dev/null +++ b/live.md @@ -0,0 +1,379 @@ +![expo-live-activity by Software Mansion](https://github.com/user-attachments/assets/9f9be263-84ee-4034-a3ca-39c72c189544) + +> [!WARNING] +> This library is in early development stage; breaking changes can be introduced in minor version upgrades. + +# expo-live-activity + +`expo-live-activity` is a React Native module designed for use with Expo to manage and display Live Activities on iOS devices exclusively. This module leverages the Live Activities feature introduced in iOS 16, allowing developers to deliver timely updates right on the lock screen. + +## Features + +- Start, update, and stop Live Activities directly from your React Native application. +- Easy integration with a comprehensive API. +- Custom image support within Live Activities with a pre-configured path. +- Listen and handle changes in push notification tokens associated with a Live Activity. + +## Platform compatibility + +**Note:** This module is intended for use on **iOS devices only**. The minimal iOS version that supports Live Activities is 16.2. When methods are invoked on platforms other than iOS or on older iOS versions, they will log an error, ensuring that they are used in the correct context. + +## Installation + +> [!NOTE] +> The library isn't supported in Expo Go; to set it up correctly you need to use [Expo DevClient](https://docs.expo.dev/versions/latest/sdk/dev-client/) . +> To begin using `expo-live-activity`, follow the installation and configuration steps outlined below: + +### Step 1: Installation + +Run the following command to add the expo-live-activity module to your project: + +```sh +npm install expo-live-activity +``` + +### Step 2: Config Plugin Setup + +The module comes with a built-in config plugin that creates a target in iOS with all the necessary files. The images used in Live Activities should be added to a pre-defined folder in your assets directory: + +1. **Add the config plugin to your app.json or app.config.js:** + ```json + { + "expo": { + "plugins": ["expo-live-activity"] + } + } + ``` + If you want to update Live Activity with push notifications you can add option `"enablePushNotifications": true`: + ```json + { + "expo": { + "plugins": [ + [ + "expo-live-activity", + { + "enablePushNotifications": true + } + ] + ] + } + } + ``` +2. **Assets configuration:** + Place images intended for Live Activities in the `assets/liveActivity` folder. The plugin manages these assets automatically. + + Then prebuild your app with: + + ```sh + npx expo prebuild --clean + ``` + +> [!NOTE] +> Because of iOS limitations, the assets can't be bigger than 4KB ([native Live Activity documentation](https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities#Understand-constraints)) + +### Step 3: Usage in Your React Native App + +Import the functionalities provided by the `expo-live-activity` module in your JavaScript or TypeScript files: + +```javascript +import * as LiveActivity from 'expo-live-activity' +``` + +## API + +`expo-live-activity` module exports three primary functions to manage Live Activities: + +### Managing Live Activities + +- **`startActivity(state: LiveActivityState, config?: LiveActivityConfig): string | undefined`**: + Start a new Live Activity. Takes a `state` configuration object for initial activity state and an optional `config` object to customize appearance or behavior. It returns the `ID` of the created Live Activity, which should be stored for future reference. If the Live Activity can't be created (eg. on android or iOS lower than 16.2), it will return `undefined`. + +- **`updateActivity(id: string, state: LiveActivityState)`**: + Update an existing Live Activity. The `state` object should contain updated information. The `activityId` indicates which activity should be updated. + +- **`stopActivity(id: string, state: LiveActivityState)`**: + Terminate an ongoing Live Activity. The `state` object should contain the final state of the activity. The `activityId` indicates which activity should be stopped. + +### Handling Push Notification Tokens + +- **`addActivityPushToStartTokenListener(listener: (event: ActivityPushToStartTokenReceivedEvent) => void): EventSubscription | undefined`**: + Subscribe to changes in the push to start token for starting live acitivities with push notifications. +- **`addActivityTokenListener(listener: (event: ActivityTokenReceivedEvent) => void): EventSubscription | undefined`**: + Subscribe to changes in the push notification token associated with Live Activities. + +### Deep linking + +When starting a new Live Activity, it's possible to pass `deepLinkUrl` field in `config` object. This usually should be a path to one of your screens. If you are using @react-navigation in your project, it's easiest to enable auto linking: + +```typescript +const prefix = Linking.createURL('') + +export default function App() { + const url = Linking.useLinkingURL() + const linking = { + enabled: 'auto' as const, + prefixes: [prefix], + } +} + +// Then start the activity with: +LiveActivity.startActivity(state, { + deepLinkUrl: '/order', +}) +``` + +URL scheme will be taken automatically from `scheme` field in `app.json` or fall back to `ios.bundleIdentifier`. + +### State Object Structure + +The `state` object should include: + +```typescript +{ + title: string; + subtitle?: string; + progressBar: { // Only one property (date, progress, or elapsedTimer) is available at a time + date?: number; // Set as epoch time in milliseconds. This is used as an end date in a countdown timer. + progress?: number; // Set amount of progress in the progress bar (0-1) + elapsedTimer?: { // Count up timer (elapsed time from start) + startDate: number; // Epoch time in milliseconds when the timer started + }; + }; + imageName?: string; // Matches the name of the image in 'assets/liveActivity' + dynamicIslandImageName?: string; // Matches the name of the image in 'assets/liveActivity' +}; +``` + +### Config Object Structure + +The `config` object should include: + +```typescript +{ + backgroundColor?: string; + titleColor?: string; + subtitleColor?: string; + progressViewTint?: string; + progressViewLabelColor?: string; + deepLinkUrl?: string; + timerType?: DynamicIslandTimerType; // "circular" | "digital" - defines timer appearance on the dynamic island + padding?: Padding // number | {top?: number bottom?: number ...} + imagePosition?: ImagePosition; // 'left' | 'right'; + imageAlign?: ImageAlign; // 'top' | 'center' | 'bottom' + imageSize?: ImageSize // { width?: number|`${number}%`, height?: number|`${number}%` } | undefined (defaults to 64pt) + contentFit?: ImageContentFit; // 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' +}; +``` + +### Activity updates + +`LiveActivity.addActivityUpdatesListener` API allows to subscribe to changes in Live Activity state. This is useful for example when you want to update the Live Activity with new information. Handler will receive an `ActivityUpdateEvent` object which contains information about new state under `activityState` property which is of `ActivityState` type, so the possible values are: `'active'`, `'dismissed'`, `'pending'`, `'stale'` or `'ended'`. Apart from this property, the event also contains `activityId` and `activityName` which can be used to identify the Live Activity. + +## Example Usage + +Managing a Live Activity: + +```typescript +const state: LiveActivity.LiveActivityState = { + title: 'Title', + subtitle: 'This is a subtitle', + progressBar: { + date: new Date(Date.now() + 60 * 1000 * 5).getTime(), + }, + imageName: 'live_activity_image', + dynamicIslandImageName: 'dynamic_island_image', +} + +const config: LiveActivity.LiveActivityConfig = { + backgroundColor: '#FFFFFF', + titleColor: '#000000', + subtitleColor: '#333333', + progressViewTint: '#4CAF50', + progressViewLabelColor: '#FFFFFF', + deepLinkUrl: '/dashboard', + timerType: 'circular', + padding: { horizontal: 20, top: 16, bottom: 16 }, + imagePosition: 'right', + imageAlign: 'center', + imageSize: { height: '50%', width: '50%' }, // number (pt) or percentage of the image container, if empty by default is 64pt. + contentFit: 'cover', +} + +const activityId = LiveActivity.startActivity(state, config) +// Store activityId for future reference +``` + +This will initiate a Live Activity with the specified title, subtitle, image from your configured assets folder and a time to which there will be a countdown in a progress view. + +Using an elapsed timer: + +```typescript +const elapsedTimerState: LiveActivity.LiveActivityState = { + title: 'Walk in Progress', + subtitle: 'With Max the Dog', + progressBar: { + elapsedTimer: { + startDate: Date.now() - 5 * 60 * 1000, // Started 5 minutes ago + }, + }, + imageName: 'dog_walking', + dynamicIslandImageName: 'dog_icon', +} + +const activityId = LiveActivity.startActivity(elapsedTimerState, config) +``` + +The elapsed timer will automatically update every second based on the `startDate` you provide. + +Subscribing to push token changes: + +```typescript +useEffect(() => { + const updateTokenSubscription = LiveActivity.addActivityTokenListener( + ({ activityID: newActivityID, activityName: newName, activityPushToken: newToken }) => { + // Send token to a remote server to update Live Activity with push notifications + } + ) + const startTokenSubscription = LiveActivity.addActivityPushToStartTokenListener( + ({ activityPushToStartToken: newActivityPushToStartToken }) => { + // Send token to a remote server to start Live Activity with push notifications + } + ) + + return () => { + updateTokenSubscription?.remove() + startTokenSubscription?.remove() + } +}, []) +``` + +> [!NOTE] +> Receiving push token may not work on simulators. Make sure to use physical device when testing this functionality. + +## Push notifications + +By default, starting and updating Live Activity is possible only via API. If you want to have possibility to start or update Live Activity using push notifications, you can enable that feature by adding `"enablePushNotifications": true` in the plugin config in your `app.json` or `app.config.ts` file. + +> [!NOTE] +> PushToStart works only for iOS 17.2 and higher. + +Example payload for starting Live Activity: + +```json +{ + "aps": { + "event": "start", + "content-state": { + "title": "Live Activity title!", + "subtitle": "Live Activity subtitle.", + "timerEndDateInMilliseconds": 1754410997000, + "progress": 0.5, + "imageName": "live_activity_image", + "dynamicIslandImageName": "dynamic_island_image", + "elapsedTimerStartDateInMilliseconds": null + }, + "timestamp": 1754491435000, // timestamp of when the push notification was sent + "attributes-type": "LiveActivityAttributes", + "attributes": { + "name": "Test", + "backgroundColor": "001A72", + "titleColor": "EBEBF0", + "subtitleColor": "FFFFFF75", + "progressViewTint": "38ACDD", + "progressViewLabelColor": "FFFFFF", + "deepLinkUrl": "/dashboard", + "timerType": "digital", + "padding": 24, // or use object to control each side: { "horizontal": 20, "top": 16, "bottom": 16 } + "imagePosition": "right", + "imageSize": "default" + }, + "alert": { + "title": "", + "body": "", + "sound": "default" + } + } +} +``` + +Example payload for updating Live Activity: + +```json +{ + "aps": { + "event": "update", + "content-state": { + "title": "Hello", + "subtitle": "World", + "timerEndDateInMilliseconds": 1754064245000, + "imageName": "live_activity_image", + "dynamicIslandImageName": "dynamic_island_image" + }, + "timestamp": 1754063621319 // timestamp of when the push notification was sent + } +} +``` + +Where `timerEndDateInMilliseconds` value is a timestamp in milliseconds corresponding to the target point of the countdown displayed in Live Activity view. + +Example payload for starting Live Activity with elapsed timer: + +```json +{ + "aps": { + "event": "start", + "content-state": { + "title": "Walk in Progress", + "subtitle": "With Max", + "timerEndDateInMilliseconds": null, + "progress": null, + "imageName": "dog_walking", + "dynamicIslandImageName": "dog_icon", + "elapsedTimerStartDateInMilliseconds": 1754410997000 + }, + "timestamp": 1754491435000, + "attributes-type": "LiveActivityAttributes", + "attributes": { + "name": "WalkActivity", + "backgroundColor": "001A72", + "titleColor": "EBEBF0", + "progressViewLabelColor": "FFFFFF" + } + } +} +``` + +Where `elapsedTimerStartDateInMilliseconds` is the timestamp (in milliseconds) when the elapsed timer started counting up. + +## Image support + +Live Activity view also supports image display. There are two dedicated fields in the `state` object for that: + +- `imageName` +- `dynamicIslandImageName` + +The value of each field can be: + +- a string which maps to an asset name +- a URL to remote image - currently, it's possible to use this option only via API, but we plan on to add that feature to push notifications as well. It also requires adding "App Groups" capability to both "main app" and "Live Activity" targets. + +## expo-live-activity is created by Software Mansion + +[![swm](https://logo.swmansion.com/logo?color=white&variant=desktop&width=150&tag=typegpu-github 'Software Mansion')](https://swmansion.com) + +Since 2012 [Software Mansion](https://swmansion.com) is a software agency with +experience in building web and mobile apps. We are Core React Native +Contributors and experts in dealing with all kinds of React Native issues. We +can help you build your next dream product – +[Hire us](https://swmansion.com/contact/projects?utm_source=typegpu&utm_medium=readme). + + + +Made by [@software-mansion](https://github.com/software-mansion) and +[community](https://github.com/software-mansion-labs/expo-live-activity/graphs/contributors) 💛 +

+ + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ab0b6e4e..4a2e3040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "expo-intent-launcher": "~13.0.7", "expo-keep-awake": "~15.0.8", "expo-linear-gradient": "~15.0.7", + "expo-live-activity": "^0.4.2", "expo-localization": "~17.0.7", "expo-navigation-bar": "~5.0.10", "expo-notifications": "~0.32.12", @@ -6594,6 +6595,17 @@ "react-native": "*" } }, + "node_modules/expo-live-activity": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/expo-live-activity/-/expo-live-activity-0.4.2.tgz", + "integrity": "sha512-b3QdsXAg8dPr6p8w4U4eBYdndArSprCPOJC9U8wovAsOOrCA3eSv4vwfn41XNDmaPTc6gweCABaIIxPaTg2oZQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-localization": { "version": "17.0.8", "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.8.tgz", diff --git a/package.json b/package.json index b4646066..1333166a 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "expo-intent-launcher": "~13.0.7", "expo-keep-awake": "~15.0.8", "expo-linear-gradient": "~15.0.7", + "expo-live-activity": "^0.4.2", "expo-localization": "~17.0.7", "expo-navigation-bar": "~5.0.10", "expo-notifications": "~0.32.12", diff --git a/src/components/StreamCard.tsx b/src/components/StreamCard.tsx index 74308d06..1849c9e0 100644 --- a/src/components/StreamCard.tsx +++ b/src/components/StreamCard.tsx @@ -29,6 +29,7 @@ interface StreamCardProps { showAlert: (title: string, message: string) => void; parentTitle?: string; parentType?: 'movie' | 'series'; + parentYear?: number; parentSeason?: number; parentEpisode?: number; parentEpisodeTitle?: string; @@ -50,6 +51,7 @@ const StreamCard = memo(({ showAlert, parentTitle, parentType, + parentYear, parentSeason, parentEpisode, parentEpisodeTitle, @@ -139,6 +141,9 @@ const StreamCard = memo(({ const parent: any = stream as any; const inferredTitle = parentTitle || stream.name || stream.title || parent.metaName || 'Content'; const inferredType: 'movie' | 'series' = parentType || (parent.kind === 'series' || parent.type === 'series' ? 'series' : 'movie'); + const year = typeof parentYear === 'number' + ? parentYear + : (typeof parent.year === 'number' ? parent.year : undefined); const season = typeof parentSeason === 'number' ? parentSeason : (parent.season || parent.season_number); const episode = typeof parentEpisode === 'number' ? parentEpisode : (parent.episode || parent.episode_number); const episodeTitle = parentEpisodeTitle || parent.episodeTitle || parent.episode_name; @@ -160,6 +165,7 @@ const StreamCard = memo(({ id: String(idForContent), type: inferredType, title: String(inferredTitle), + year: inferredType === 'movie' ? year : undefined, providerName: String(provider), season: inferredType === 'series' ? (season ? Number(season) : undefined) : undefined, episode: inferredType === 'series' ? (episode ? Number(episode) : undefined) : undefined, diff --git a/src/components/TabletStreamsLayout.tsx b/src/components/TabletStreamsLayout.tsx index 85c21a3c..6eb273c3 100644 --- a/src/components/TabletStreamsLayout.tsx +++ b/src/components/TabletStreamsLayout.tsx @@ -371,6 +371,7 @@ const TabletStreamsLayout: React.FC = ({ showAlert={(t: string, m: string) => openAlert(t, m)} parentTitle={metadata?.name} parentType={type as 'movie' | 'series'} + parentYear={metadata?.year} parentSeason={(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined} parentEpisode={(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined} parentEpisodeTitle={(type === 'series' || type === 'other') ? currentEpisode?.name : undefined} diff --git a/src/contexts/DownloadsContext.tsx b/src/contexts/DownloadsContext.tsx index 527da64b..4f25e2a7 100644 --- a/src/contexts/DownloadsContext.tsx +++ b/src/contexts/DownloadsContext.tsx @@ -9,6 +9,7 @@ import { } from '@kesha-antonov/react-native-background-downloader'; import { mmkvStorage } from '../services/mmkvStorage'; import { notificationService } from '../services/notificationService'; +import { startOrUpdateDownloadLiveActivity, stopDownloadLiveActivity } from '../services/liveActivityService'; export type DownloadStatus = 'downloading' | 'completed' | 'paused' | 'error' | 'queued'; @@ -17,6 +18,7 @@ export interface DownloadItem { contentId: string; // base id (e.g., tt0903747 for series, tt0499549 for movies) type: 'movie' | 'series'; title: string; // movie title or show name + year?: number; providerName?: string; season?: number; episode?: number; @@ -46,6 +48,7 @@ type StartDownloadInput = { id: string; // Base content ID (e.g., tt0903747) type: 'movie' | 'series'; title: string; + year?: number; providerName?: string; season?: number; episode?: number; @@ -119,6 +122,31 @@ function isHttpUrl(url: string): boolean { } } +function formatSeasonEpisode(season?: number, episode?: number): string | null { + if (typeof season !== 'number' || typeof episode !== 'number') return null; + return `S${season}E${episode}`; +} + +function formatMovieTitleWithYear(title: string, year?: number): string { + if (!year || !Number.isFinite(year)) return title; + if (/\(\d{4}\)\s*$/.test(title)) return title; + return `${title} (${year})`; +} + +function getLiveActivityText(d: DownloadItem): { title: string; subtitle: string } { + const title = d.type === 'movie' ? formatMovieTitleWithYear(d.title, d.year) : d.title; + const parts: string[] = []; + + if (d.type === 'series') { + const se = formatSeasonEpisode(d.season, d.episode); + if (se) parts.push(se); + if (d.episodeTitle) parts.push(String(d.episodeTitle)); + } + + parts.push(`${d.progress}%`); + return { title, subtitle: parts.join(' • ') }; +} + async function getContentLength(url: string, headers?: Record): Promise { if (!isHttpUrl(url)) return null; try { @@ -269,6 +297,10 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi // Cache last notified progress to reduce spam const lastNotifyRef = useRef>(new Map()); + // iOS-only Live Activities for background download progress + const liveActivityIdsRef = useRef>(new Map()); + const lastLiveProgressRef = useRef>(new Map()); + const maybeNotifyProgress = useCallback(async (d: DownloadItem) => { try { if (appStateRef.current === 'active') return; @@ -287,6 +319,86 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi } catch { } }, []); + const stopLiveActivityForDownload = useCallback(async (downloadId: string, opts?: { title?: string; subtitle?: string; progressPercent?: number }) => { + const activityId = liveActivityIdsRef.current.get(downloadId); + if (!activityId) return; + + liveActivityIdsRef.current.delete(downloadId); + lastLiveProgressRef.current.delete(downloadId); + + const title = opts?.title || downloadsRef.current.find(d => d.id === downloadId)?.title || 'Download'; + await stopDownloadLiveActivity({ + activityId, + title, + subtitle: opts?.subtitle, + progressPercent: opts?.progressPercent, + }); + }, []); + + const stopAllLiveActivities = useCallback(async () => { + const entries = Array.from(liveActivityIdsRef.current.entries()); + liveActivityIdsRef.current.clear(); + lastLiveProgressRef.current.clear(); + + await Promise.all( + entries.map(async ([downloadId, activityId]) => { + const title = downloadsRef.current.find(d => d.id === downloadId)?.title || 'Download'; + await stopDownloadLiveActivity({ activityId, title }); + }) + ); + }, []); + + const maybeUpdateLiveActivity = useCallback(async (d: DownloadItem) => { + try { + if (d.status !== 'downloading') return; + + // Create the Live Activity as soon as possible (even in foreground) so it exists + // when the user backgrounds / swipes away. Only keep updating progress while backgrounded. + const existingActivityId = liveActivityIdsRef.current.get(d.id); + const isBackground = appStateRef.current !== 'active'; + if (!isBackground && existingActivityId) return; + + const prev = lastLiveProgressRef.current.get(d.id) ?? -1; + if (isBackground && (d.progress <= prev || d.progress - prev < 2)) return; // update every 2% + lastLiveProgressRef.current.set(d.id, d.progress); + + const { title, subtitle } = getLiveActivityText(d); + + const activityId = await startOrUpdateDownloadLiveActivity({ + activityId: existingActivityId, + title, + subtitle, + progressPercent: d.progress, + deepLinkUrl: '/downloads', + }); + + if (activityId && activityId !== existingActivityId) { + liveActivityIdsRef.current.set(d.id, activityId); + } + } catch { + // ignore + } + }, []); + + const syncLiveActivitiesForBackground = useCallback(async () => { + if (appStateRef.current === 'active') return; + + const activeIds = new Set(downloadsRef.current.filter(d => d.status === 'downloading').map(d => d.id)); + await Promise.all( + downloadsRef.current + .filter(d => d.status === 'downloading') + .map(d => maybeUpdateLiveActivity(d)) + ); + + // Stop activities for downloads that are no longer downloading. + const existing = Array.from(liveActivityIdsRef.current.keys()); + await Promise.all( + existing + .filter(id => !activeIds.has(id)) + .map(id => stopLiveActivityForDownload(id)) + ); + }, [maybeUpdateLiveActivity, stopLiveActivityForDownload]); + useEffect(() => { mmkvStorage.setItem(STORAGE_KEY, JSON.stringify(downloads)).catch(() => { }); }, [downloads]); @@ -307,6 +419,11 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi status: 'downloading', updatedAt: Date.now(), })); + + const current = downloadsRef.current.find(x => x.id === taskId); + if (current) { + maybeUpdateLiveActivity({ ...current, status: 'downloading' }); + } }) .progress(({ bytesDownloaded, bytesTotal }: any) => { const now = Date.now(); @@ -338,7 +455,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi if (current && typeof bytesDownloaded === 'number') { const totalBytes = typeof bytesTotal === 'number' && bytesTotal > 0 ? bytesTotal : current.totalBytes; const progress = totalBytes > 0 ? Math.floor((bytesDownloaded / totalBytes) * 100) : current.progress; - maybeNotifyProgress({ ...current, downloadedBytes: bytesDownloaded, totalBytes, progress }); + const next = { ...current, downloadedBytes: bytesDownloaded, totalBytes, progress }; + maybeNotifyProgress(next); + maybeUpdateLiveActivity({ ...next, status: 'downloading' }); } }) .done(({ location, bytesDownloaded, bytesTotal }: any) => { @@ -357,7 +476,12 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi })); const doneItem = downloadsRef.current.find(x => x.id === taskId); - if (doneItem) notifyCompleted({ ...doneItem, status: 'completed', progress: 100, fileUri: finalUri || doneItem.fileUri } as DownloadItem); + if (doneItem) { + notifyCompleted({ ...doneItem, status: 'completed', progress: 100, fileUri: finalUri || doneItem.fileUri } as DownloadItem); + stopLiveActivityForDownload(taskId, { title: doneItem.title, subtitle: 'Completed', progressPercent: 100 }); + } else { + stopLiveActivityForDownload(taskId, { subtitle: 'Completed', progressPercent: 100 }); + } try { completeHandler(taskId); @@ -373,9 +497,12 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi updatedAt: Date.now(), })); + const current = downloadsRef.current.find(x => x.id === taskId); + stopLiveActivityForDownload(taskId, { title: current?.title, subtitle: 'Error', progressPercent: current?.progress }); + console.log(`[DownloadsContext] Background download error: ${taskId}`, error); }); - }, [maybeNotifyProgress, notifyCompleted, updateDownload]); + }, [maybeNotifyProgress, maybeUpdateLiveActivity, notifyCompleted, stopLiveActivityForDownload, updateDownload]); useEffect(() => { (async () => { @@ -396,6 +523,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi contentId: String(meta.contentId ?? taskId), type: (meta.type as 'movie' | 'series') ?? 'movie', title: String(meta.title ?? 'Content'), + year: typeof meta.year === 'number' ? meta.year : undefined, providerName: meta.providerName, season: typeof meta.season === 'number' ? meta.season : undefined, episode: typeof meta.episode === 'number' ? meta.episode : undefined, @@ -466,7 +594,12 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi if (looksComplete) { const done = downloadsRef.current.find(x => x.id === d.id); - if (done) notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: d.fileUri } as DownloadItem); + if (done) { + notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: d.fileUri } as DownloadItem); + stopLiveActivityForDownload(d.id, { title: done.title, subtitle: 'Completed', progressPercent: 100 }); + } else { + stopLiveActivityForDownload(d.id, { subtitle: 'Completed', progressPercent: 100 }); + } tasksRef.current.delete(d.id); lastBytesRef.current.delete(d.id); } @@ -478,17 +611,20 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi } finally { refreshInProgressRef.current = false; } - }, [updateDownload, notifyCompleted]); + }, [updateDownload, notifyCompleted, stopLiveActivityForDownload]); useEffect(() => { const sub = AppState.addEventListener('change', (s) => { appStateRef.current = s; if (s === 'active') { + stopAllLiveActivities(); refreshAllDownloadsFromDisk(); + } else { + syncLiveActivitiesForBackground(); } }); return () => sub.remove(); - }, [refreshAllDownloadsFromDisk]); + }, [refreshAllDownloadsFromDisk, stopAllLiveActivities, syncLiveActivitiesForBackground]); const resumeDownload = useCallback(async (id: string) => { const item = downloadsRef.current.find(d => d.id === id); @@ -516,11 +652,14 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi try { await task.resume(); + + // If app is backgrounded, kick Live Activity updates. + maybeUpdateLiveActivity({ ...item, status: 'downloading' }); } catch (e) { console.log(`[DownloadsContext] Resume failed: ${id}`, e); updateDownload(id, (d) => ({ ...d, status: 'error', updatedAt: Date.now() })); } - }, [attachDownloadTask, updateDownload]); + }, [attachDownloadTask, maybeUpdateLiveActivity, updateDownload]); const startDownload = useCallback(async (input: StartDownloadInput) => { if (!isHttpUrl(input.url)) { @@ -581,6 +720,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi contentId, type: input.type, title: input.title, + year: typeof input.year === 'number' ? input.year : undefined, providerName: input.providerName, season: input.season, episode: input.episode, @@ -608,6 +748,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi setDownloads(prev => [newItem, ...prev]); + // If somehow started while app is backgrounded, show Live Activity. + maybeUpdateLiveActivity(newItem); + const task = createDownloadTask({ id: compoundId, url: input.url, @@ -617,6 +760,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi contentId, type: input.type, title: input.title, + year: typeof input.year === 'number' ? input.year : undefined, providerName: input.providerName, season: input.season, episode: input.episode, @@ -643,7 +787,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi updateDownload(compoundId, (d) => ({ ...d, status: 'error', updatedAt: Date.now() })); throw e; } - }, [attachDownloadTask, resumeDownload, updateDownload]); + }, [attachDownloadTask, maybeUpdateLiveActivity, resumeDownload, updateDownload]); const pauseDownload = useCallback(async (id: string) => { console.log(`[DownloadsContext] Pausing download: ${id}`); @@ -652,6 +796,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi // This will cause any ongoing download/resume operations to check status and exit gracefully updateDownload(id, (d) => ({ ...d, status: 'paused', updatedAt: Date.now() })); + const current = downloadsRef.current.find(d => d.id === id); + stopLiveActivityForDownload(id, { title: current?.title, subtitle: 'Paused', progressPercent: current?.progress }); + const task = tasksRef.current.get(id); if (!task) return; @@ -660,9 +807,11 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi } catch (e) { console.log(`[DownloadsContext] Pause failed: ${id}`, e); } - }, [updateDownload]); + }, [stopLiveActivityForDownload, updateDownload]); const cancelDownload = useCallback(async (id: string) => { + const current = downloadsRef.current.find(d => d.id === id); + await stopLiveActivityForDownload(id, { title: current?.title, subtitle: 'Canceled', progressPercent: current?.progress }); try { const task = tasksRef.current.get(id); if (task) { @@ -678,15 +827,16 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi await FileSystem.deleteAsync(item.fileUri, { idempotent: true }).catch(() => { }); } setDownloads(prev => prev.filter(d => d.id !== id)); - }, []); + }, [stopLiveActivityForDownload]); const removeDownload = useCallback(async (id: string) => { const item = downloadsRef.current.find(d => d.id === id); + await stopLiveActivityForDownload(id, { title: item?.title, subtitle: 'Removed', progressPercent: item?.progress }); if (item?.fileUri && item.status === 'completed') { await FileSystem.deleteAsync(item.fileUri, { idempotent: true }).catch(() => { }); } setDownloads(prev => prev.filter(d => d.id !== id)); - }, []); + }, [stopLiveActivityForDownload]); const value = useMemo(() => ({ downloads, diff --git a/src/screens/streams/components/StreamsList.tsx b/src/screens/streams/components/StreamsList.tsx index 7164c5f8..639f53e5 100644 --- a/src/screens/streams/components/StreamsList.tsx +++ b/src/screens/streams/components/StreamsList.tsx @@ -119,6 +119,7 @@ const StreamsList = memo( showAlert={(t: string, m: string) => openAlert(t, m)} parentTitle={metadata?.name} parentType={type as 'movie' | 'series'} + parentYear={metadata?.year} parentSeason={ (type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined } diff --git a/src/services/liveActivityService.ts b/src/services/liveActivityService.ts new file mode 100644 index 00000000..828177f3 --- /dev/null +++ b/src/services/liveActivityService.ts @@ -0,0 +1,86 @@ +import { Platform } from 'react-native'; + +type LiveActivityModule = { + startActivity: (state: any, config?: any) => string | undefined; + updateActivity: (id: string, state: any) => void; + stopActivity: (id: string, state: any) => void; +}; + +function getLiveActivityModule(): LiveActivityModule | null { + if (Platform.OS !== 'ios') return null; + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('expo-live-activity') as LiveActivityModule; + } catch { + return null; + } +} + +function clamp01(n: number): number { + if (!Number.isFinite(n)) return 0; + return Math.min(1, Math.max(0, n)); +} + +export async function startOrUpdateDownloadLiveActivity(params: { + activityId?: string; + title: string; + progressPercent: number; + subtitle?: string; + deepLinkUrl?: string; +}): Promise { + const mod = getLiveActivityModule(); + if (!mod) return undefined; + + const progress01 = clamp01(params.progressPercent / 100); + const state = { + title: params.title, + subtitle: params.subtitle, + progressBar: { + progress: progress01, + }, + }; + + const config = params.deepLinkUrl ? { deepLinkUrl: params.deepLinkUrl } : undefined; + + try { + if (params.activityId) { + mod.updateActivity(params.activityId, state); + return params.activityId; + } + + const id = mod.startActivity(state, config); + if (!id) { + console.warn( + '[LiveActivity] startActivity returned undefined. Live Activities require a physical iOS device on iOS 16.2+ and a clean prebuild after enabling the expo-live-activity plugin.' + ); + } + return id; + } catch { + return undefined; + } +} + +export async function stopDownloadLiveActivity(params: { + activityId: string; + title: string; + progressPercent?: number; + subtitle?: string; +}): Promise { + const mod = getLiveActivityModule(); + if (!mod) return; + + const progress01 = clamp01((params.progressPercent ?? 0) / 100); + const state = { + title: params.title, + subtitle: params.subtitle, + progressBar: { + progress: progress01, + }, + }; + + try { + mod.stopActivity(params.activityId, state); + } catch { + // ignore + } +} diff --git a/src/utils/version.ts b/src/utils/version.ts index d681c860..06f1acea 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -1,7 +1,7 @@ // Single source of truth for the app version displayed in Settings // Update this when bumping app version -export const APP_VERSION = '1.3.6'; +export const APP_VERSION = '1.3.7'; export function getDisplayedAppVersion(): string { return APP_VERSION;