added liveactivity for ios downloads

This commit is contained in:
tapframe 2026-01-24 13:41:36 +05:30
parent 671ed871e3
commit fd7372a2e9
33 changed files with 2038 additions and 71 deletions

View file

@ -67,6 +67,7 @@
},
"owner": "nayifleo",
"plugins": [
"expo-live-activity",
[
"@sentry/react-native/expo",
{

View file

@ -0,0 +1,11 @@
{
"colors": [
{
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}

View file

@ -0,0 +1,11 @@
{
"colors": [
{
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View file

@ -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
)
}
}
}

View file

@ -0,0 +1,7 @@
import SwiftUI
extension Date {
static func toTimerInterval(miliseconds: Double) -> ClosedRange<Self> {
now ... max(now, Date(timeIntervalSince1970: miliseconds / 1000))
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -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

View file

@ -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)
}
}

View file

@ -0,0 +1,9 @@
import SwiftUI
import WidgetKit
@main
struct LiveActivityWidgetBundle: WidgetBundle {
var body: some Widget {
LiveActivityWidget()
}
}

View file

@ -0,0 +1,12 @@
import SwiftUI
extension View {
@ViewBuilder
func applyIfPresent<T>(_ value: T?, transform: (Self, T) -> some View) -> some View {
if let value {
transform(self, value)
} else {
self
}
}
}

View file

@ -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))
}
}
}
}

View file

@ -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)
}
}

View file

@ -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 = "<group>"; };
0E13CE4BDE2F4555806AE753 /* Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0F1D0037D1F24E60BDB57628 /* Assets.xcassets */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
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 = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
2DE29A8A87D24662BEFFF849 /* LiveActivity.entitlements */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; path = LiveActivity.entitlements; sourceTree = "<group>"; };
2F448294A36E433E924078C1 /* LiveActivityView.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = LiveActivityView.swift; sourceTree = "<group>"; };
324373F393774A9CA40DE22E /* View+applyIfPresent.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = "View+applyIfPresent.swift"; sourceTree = "<group>"; };
3396D68881EF486E99FD480A /* ViewHelpers.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = ViewHelpers.swift; sourceTree = "<group>"; };
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 = "<group>"; };
3A48D8A298DD48928E8D0A02 /* Date+toTimerInterval.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = "Date+toTimerInterval.swift"; sourceTree = "<group>"; };
49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
730F1CE82F24B29C00EF7E51 /* LiveActivityDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LiveActivityDebug.entitlements; sourceTree = "<group>"; };
73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; sourceTree = "<group>"; };
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 = "<group>"; };
9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ../KSPlayer/RNBridge/KSPlayerManager.m; sourceTree = "<group>"; };
9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerModule.swift; sourceTree = "<group>"; };
9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerView.swift; sourceTree = "<group>"; };
9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerViewManager.swift; sourceTree = "<group>"; };
8034143A77A946B5A793F967 /* Color+hex.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = "Color+hex.swift"; sourceTree = "<group>"; };
9FBA88F02E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ../KSPlayer/RNBridge/KSPlayerManager.m; sourceTree = "<group>"; };
9FBA88F12E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerModule.swift; sourceTree = "<group>"; };
9FBA88F22E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerView.swift; sourceTree = "<group>"; };
9FBA88F32E86ECD700892850 /* ../KSPlayer/RNBridge/KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerViewManager.swift; sourceTree = "<group>"; };
A83D742B36224176A0AB3B25 /* LiveActivityWidgetBundle.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = LiveActivityWidgetBundle.swift; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Nuvio/SplashScreen.storyboard; sourceTree = "<group>"; };
AD48662BB71E4C9C9E340289 /* LiveActivityWidget.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; path = LiveActivityWidget.swift; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
F11748442D0722820044C1D9 /* Nuvio-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Nuvio-Bridging-Header.h"; path = "Nuvio/Nuvio-Bridging-Header.h"; sourceTree = "<group>"; };
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 = "<group>";
@ -89,6 +256,13 @@
name = ExpoModulesProviders;
sourceTree = "<group>";
};
62B088ADB2A740DAB9E343F9 /* LiveActivity */ = {
isa = PBXGroup;
children = (
);
path = LiveActivity;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
};
B9F3EB198DED443D980ADFB3 /* LiveActivity */ = {
isa = PBXGroup;
children = (
);
path = LiveActivity;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup;
children = (
@ -128,15 +317,42 @@
path = Nuvio/Supporting;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
E8C72B3DF7DB40A8896F56C9 /* LiveActivity */ = {
isa = PBXGroup;
children = (
730F1CE82F24B29C00EF7E51 /* LiveActivityDebug.entitlements */,
);
path = LiveActivity;
sourceTree = "<group>";
};
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 */;

View file

@ -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)
}
}

View file

@ -58,9 +58,13 @@
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your local network</string>
<string>Nuvio uses the local network to discover Cast-enabled devices on your WiFi network and to connect to local media servers.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not require microphone access.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<false/>
<key>RCTNewArchEnabled</key>
<true/>
<key>RCTRootViewBackgroundColor</key>

View file

@ -1,3 +1,4 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <RNBackgroundDownloader.h>

View file

@ -9,7 +9,7 @@
<key>EXUpdatesLaunchWaitMs</key>
<integer>30000</integer>
<key>EXUpdatesRuntimeVersion</key>
<string>1.2.11</string>
<string>1.3.6</string>
<key>EXUpdatesURL</key>
<string>https://ota.nuvioapp.space/api/manifest</string>
</dict>

View file

@ -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

View file

@ -3,4 +3,4 @@
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
"newArchEnabled": "true",
"ios.deploymentTarget": "16.0"
}
}

379
live.md Normal file
View file

@ -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).
<!-- automd:contributors author="software-mansion" -->
Made by [@software-mansion](https://github.com/software-mansion) and
[community](https://github.com/software-mansion-labs/expo-live-activity/graphs/contributors) 💛
<br><br>
<a href="https://github.com/software-mansion-labs/expo-live-activity/graphs/contributors">
<img src="https://contrib.rocks/image?repo=software-mansion-labs/expo-live-activity" />
</a>
<!-- /automd -->

12
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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,

View file

@ -371,6 +371,7 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
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}

View file

@ -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<string, string>): Promise<number | null> {
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<Map<string, number>>(new Map());
// iOS-only Live Activities for background download progress
const liveActivityIdsRef = useRef<Map<string, string>>(new Map());
const lastLiveProgressRef = useRef<Map<string, number>>(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<DownloadsContextValue>(() => ({
downloads,

View file

@ -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
}

View file

@ -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<string | undefined> {
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<void> {
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
}
}

View file

@ -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;