mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 09:35:42 +00:00
added liveactivity for ios downloads
This commit is contained in:
parent
671ed871e3
commit
fd7372a2e9
33 changed files with 2038 additions and 71 deletions
1
app.json
1
app.json
|
|
@ -67,6 +67,7 @@
|
|||
},
|
||||
"owner": "nayifleo",
|
||||
"plugins": [
|
||||
"expo-live-activity",
|
||||
[
|
||||
"@sentry/react-native/expo",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors": [
|
||||
{
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
6
ios/LiveActivity/Assets.xcassets/Contents.json
Normal file
6
ios/LiveActivity/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors": [
|
||||
{
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
37
ios/LiveActivity/Color+hex.swift
Normal file
37
ios/LiveActivity/Color+hex.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
7
ios/LiveActivity/Date+toTimerInterval.swift
Normal file
7
ios/LiveActivity/Date+toTimerInterval.swift
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import SwiftUI
|
||||
|
||||
extension Date {
|
||||
static func toTimerInterval(miliseconds: Double) -> ClosedRange<Self> {
|
||||
now ... max(now, Date(timeIntervalSince1970: miliseconds / 1000))
|
||||
}
|
||||
}
|
||||
33
ios/LiveActivity/Image+dynamic.swift
Normal file
33
ios/LiveActivity/Image+dynamic.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
11
ios/LiveActivity/Info.plist
Normal file
11
ios/LiveActivity/Info.plist
Normal 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>
|
||||
5
ios/LiveActivity/LiveActivity.entitlements
Normal file
5
ios/LiveActivity/LiveActivity.entitlements
Normal 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>
|
||||
5
ios/LiveActivity/LiveActivityDebug.entitlements
Normal file
5
ios/LiveActivity/LiveActivityDebug.entitlements
Normal 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>
|
||||
247
ios/LiveActivity/LiveActivityView.swift
Normal file
247
ios/LiveActivity/LiveActivityView.swift
Normal 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
|
||||
169
ios/LiveActivity/LiveActivityWidget.swift
Normal file
169
ios/LiveActivity/LiveActivityWidget.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
9
ios/LiveActivity/LiveActivityWidgetBundle.swift
Normal file
9
ios/LiveActivity/LiveActivityWidgetBundle.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@main
|
||||
struct LiveActivityWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
LiveActivityWidget()
|
||||
}
|
||||
}
|
||||
12
ios/LiveActivity/View+applyIfPresent.swift
Normal file
12
ios/LiveActivity/View+applyIfPresent.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
24
ios/LiveActivity/View+applyWidgetURL.swift
Normal file
24
ios/LiveActivity/View+applyWidgetURL.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
ios/LiveActivity/ViewHelpers.swift
Normal file
33
ios/LiveActivity/ViewHelpers.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
379
live.md
Normal file
379
live.md
Normal file
|
|
@ -0,0 +1,379 @@
|
|||

|
||||
|
||||
> [!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
|
||||
|
||||
[](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
12
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
86
src/services/liveActivityService.ts
Normal file
86
src/services/liveActivityService.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue