@neoskola/auto-play 0.3.18 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -2,7 +2,8 @@ import CarPlay
|
|
|
2
2
|
import MediaPlayer
|
|
3
3
|
import AVFoundation
|
|
4
4
|
|
|
5
|
-
class NowPlayingTemplate:
|
|
5
|
+
class NowPlayingTemplate: AutoPlayTemplate {
|
|
6
|
+
var template: CPListTemplate
|
|
6
7
|
var config: NowPlayingTemplateConfig
|
|
7
8
|
private var loadedImage: UIImage?
|
|
8
9
|
private var isSetupComplete = false
|
|
@@ -27,33 +28,106 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
func getTemplate() -> CPTemplate {
|
|
30
|
-
return
|
|
31
|
+
return template
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
init(config: NowPlayingTemplateConfig) {
|
|
34
35
|
self.config = config
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
// Custom player screen using CPListTemplate instead of CPNowPlayingTemplate.shared
|
|
38
|
+
let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
|
|
39
|
+
let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
|
|
40
|
+
|
|
41
|
+
let infoItem = CPListItem(
|
|
42
|
+
text: titleText,
|
|
43
|
+
detailText: subtitleText,
|
|
44
|
+
image: UIImage(systemName: "music.note"),
|
|
45
|
+
accessoryImage: nil,
|
|
46
|
+
accessoryType: .none
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
let statusItem = CPListItem(
|
|
50
|
+
text: "Yükleniyor...",
|
|
51
|
+
detailText: nil,
|
|
52
|
+
image: UIImage(systemName: "arrow.down.circle"),
|
|
53
|
+
accessoryImage: nil,
|
|
54
|
+
accessoryType: .none
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
let section = CPListSection(
|
|
58
|
+
items: [infoItem, statusItem],
|
|
59
|
+
header: nil,
|
|
60
|
+
sectionIndexTitle: nil
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
template = CPListTemplate(
|
|
64
|
+
title: "Now Playing",
|
|
65
|
+
sections: [section],
|
|
66
|
+
assistantCellConfiguration: nil,
|
|
67
|
+
id: config.id
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
DispatchQueue.main.async { [weak self] in
|
|
71
|
+
guard let self = self else { return }
|
|
72
|
+
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
73
|
+
self.setupRemoteCommandCenter()
|
|
74
|
+
self.updateNowPlayingInfo()
|
|
75
|
+
self.isSetupComplete = true
|
|
37
76
|
|
|
38
|
-
|
|
39
|
-
|
|
77
|
+
if let image = config.image {
|
|
78
|
+
self.loadImageAsync(image: image)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
40
82
|
|
|
41
|
-
|
|
42
|
-
// so they're ready before CPNowPlayingTemplate.shared is pushed
|
|
43
|
-
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
44
|
-
setupRemoteCommandCenter()
|
|
45
|
-
updateNowPlayingInfo()
|
|
83
|
+
// MARK: - Player UI
|
|
46
84
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
85
|
+
private func updatePlayerUI() {
|
|
86
|
+
let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
|
|
87
|
+
let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
|
|
51
88
|
|
|
52
|
-
|
|
89
|
+
let infoItem = CPListItem(
|
|
90
|
+
text: titleText,
|
|
91
|
+
detailText: subtitleText,
|
|
92
|
+
image: loadedImage ?? UIImage(systemName: "music.note"),
|
|
93
|
+
accessoryImage: nil,
|
|
94
|
+
accessoryType: .none
|
|
95
|
+
)
|
|
53
96
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
97
|
+
let statusText: String
|
|
98
|
+
let statusIcon: String
|
|
99
|
+
if config.isPlaying {
|
|
100
|
+
let elapsed = formatTime(currentElapsedTime)
|
|
101
|
+
let total = currentDuration > 0 ? formatTime(currentDuration) : "--:--"
|
|
102
|
+
statusText = "Playing \(elapsed) / \(total)"
|
|
103
|
+
statusIcon = "play.circle.fill"
|
|
104
|
+
} else {
|
|
105
|
+
statusText = "Paused"
|
|
106
|
+
statusIcon = "pause.circle.fill"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let statusItem = CPListItem(
|
|
110
|
+
text: statusText,
|
|
111
|
+
detailText: nil,
|
|
112
|
+
image: UIImage(systemName: statusIcon),
|
|
113
|
+
accessoryImage: nil,
|
|
114
|
+
accessoryType: .none
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
let section = CPListSection(
|
|
118
|
+
items: [infoItem, statusItem],
|
|
119
|
+
header: nil,
|
|
120
|
+
sectionIndexTitle: nil
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
template.updateSections([section])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func formatTime(_ seconds: Double) -> String {
|
|
127
|
+
guard !seconds.isNaN && !seconds.isInfinite && seconds >= 0 else { return "0:00" }
|
|
128
|
+
let mins = Int(seconds) / 60
|
|
129
|
+
let secs = Int(seconds) % 60
|
|
130
|
+
return String(format: "%d:%02d", mins, secs)
|
|
57
131
|
}
|
|
58
132
|
|
|
59
133
|
// MARK: - Native Audio Playback
|
|
@@ -73,6 +147,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
73
147
|
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
74
148
|
config.isPlaying = true
|
|
75
149
|
updateNowPlayingInfo()
|
|
150
|
+
updatePlayerUI()
|
|
76
151
|
MPNowPlayingInfoCenter.default().playbackState = .playing
|
|
77
152
|
|
|
78
153
|
print("[NowPlayingTemplate] Downloading audio: \(url)")
|
|
@@ -131,6 +206,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
131
206
|
if !duration.isNaN && !duration.isInfinite && duration > 0 {
|
|
132
207
|
self.currentDuration = duration
|
|
133
208
|
self.updateNowPlayingInfo()
|
|
209
|
+
self.updatePlayerUI()
|
|
134
210
|
print("[NowPlayingTemplate] Duration resolved via KVO: \(duration)s")
|
|
135
211
|
}
|
|
136
212
|
}
|
|
@@ -200,6 +276,9 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
200
276
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
201
277
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
202
278
|
|
|
279
|
+
// Update custom player UI
|
|
280
|
+
updatePlayerUI()
|
|
281
|
+
|
|
203
282
|
// 95% completion check
|
|
204
283
|
if !completionFired && currentDuration > 0 && currentTime / currentDuration >= 0.95 {
|
|
205
284
|
completionFired = true
|
|
@@ -217,6 +296,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
217
296
|
private func handlePlaybackFinished() {
|
|
218
297
|
config.isPlaying = false
|
|
219
298
|
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
299
|
+
updatePlayerUI()
|
|
220
300
|
if !completionFired {
|
|
221
301
|
completionFired = true
|
|
222
302
|
config.onComplete?()
|
|
@@ -229,6 +309,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
229
309
|
player?.pause()
|
|
230
310
|
config.isPlaying = false
|
|
231
311
|
updatePlaybackState(isPlaying: false)
|
|
312
|
+
updatePlayerUI()
|
|
232
313
|
reportProgress()
|
|
233
314
|
}
|
|
234
315
|
|
|
@@ -238,6 +319,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
238
319
|
player?.play()
|
|
239
320
|
config.isPlaying = true
|
|
240
321
|
updatePlaybackState(isPlaying: true)
|
|
322
|
+
updatePlayerUI()
|
|
241
323
|
}
|
|
242
324
|
|
|
243
325
|
@MainActor
|
|
@@ -262,6 +344,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
262
344
|
cleanupPlayer()
|
|
263
345
|
config.isPlaying = false
|
|
264
346
|
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
347
|
+
updatePlayerUI()
|
|
265
348
|
}
|
|
266
349
|
|
|
267
350
|
private func cleanupPlayer() {
|
|
@@ -307,8 +390,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
307
390
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
|
308
391
|
}
|
|
309
392
|
|
|
310
|
-
// Keep playback metadata populated even before duration is known.
|
|
311
|
-
// This helps CarPlay recognize an active now-playing session.
|
|
312
393
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
|
|
313
394
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
314
395
|
|
|
@@ -381,6 +462,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
381
462
|
self.player?.seek(to: time)
|
|
382
463
|
self.currentElapsedTime = positionEvent.positionTime
|
|
383
464
|
self.updateNowPlayingInfo()
|
|
465
|
+
self.updatePlayerUI()
|
|
384
466
|
return .success
|
|
385
467
|
}
|
|
386
468
|
}
|
|
@@ -396,6 +478,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
396
478
|
DispatchQueue.main.async {
|
|
397
479
|
self.loadedImage = uiImage
|
|
398
480
|
self.updateNowPlayingInfo()
|
|
481
|
+
self.updatePlayerUI()
|
|
399
482
|
}
|
|
400
483
|
}.resume()
|
|
401
484
|
}
|
|
@@ -405,6 +488,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
405
488
|
|
|
406
489
|
@MainActor
|
|
407
490
|
func invalidate() {
|
|
491
|
+
updatePlayerUI()
|
|
408
492
|
updateNowPlayingInfo()
|
|
409
493
|
|
|
410
494
|
if loadedImage == nil, let image = config.image {
|
|
@@ -451,6 +535,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
451
535
|
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
452
536
|
|
|
453
537
|
if !isSetupComplete {
|
|
538
|
+
updatePlayerUI()
|
|
454
539
|
updateNowPlayingInfo()
|
|
455
540
|
isSetupComplete = true
|
|
456
541
|
}
|
|
@@ -477,6 +562,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
477
562
|
config.title = AutoText(text: title, distance: nil, duration: nil)
|
|
478
563
|
config.subtitle = AutoText(text: subtitle, distance: nil, duration: nil)
|
|
479
564
|
updateNowPlayingInfo()
|
|
565
|
+
updatePlayerUI()
|
|
480
566
|
}
|
|
481
567
|
|
|
482
568
|
@MainActor
|
|
@@ -498,5 +584,7 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
|
498
584
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
|
|
499
585
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
500
586
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
587
|
+
|
|
588
|
+
updatePlayerUI()
|
|
501
589
|
}
|
|
502
590
|
}
|