@neoskola/auto-play 0.3.14 → 0.3.16
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.
|
@@ -188,13 +188,6 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
188
188
|
)
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// NowPlayingTemplate: inject custom UIKit view into CPWindow
|
|
192
|
-
if let nowPlaying = template as? NowPlayingTemplate {
|
|
193
|
-
try await MainActor.run {
|
|
194
|
-
try nowPlaying.injectCustomView()
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
191
|
if let autoDismissMs = TemplateStore.getTemplate(
|
|
199
192
|
templateId: templateId
|
|
200
193
|
)?.autoDismissMs {
|
|
@@ -2,18 +2,13 @@ import CarPlay
|
|
|
2
2
|
import MediaPlayer
|
|
3
3
|
import AVFoundation
|
|
4
4
|
|
|
5
|
-
class NowPlayingTemplate: NSObject, AutoPlayTemplate
|
|
6
|
-
var template: CPMapTemplate
|
|
5
|
+
class NowPlayingTemplate: NSObject, AutoPlayTemplate {
|
|
7
6
|
var config: NowPlayingTemplateConfig
|
|
8
7
|
private var loadedImage: UIImage?
|
|
9
8
|
private var isSetupComplete = false
|
|
10
9
|
private var currentElapsedTime: Double = 0
|
|
11
10
|
private var currentDuration: Double = 0
|
|
12
11
|
|
|
13
|
-
// Custom UIKit view
|
|
14
|
-
private var customViewController: NeoSkolaNowPlayingViewController?
|
|
15
|
-
private var previousRootVC: UIViewController?
|
|
16
|
-
|
|
17
12
|
// Native audio player
|
|
18
13
|
private var player: AVPlayer?
|
|
19
14
|
private var playerItem: AVPlayerItem?
|
|
@@ -32,125 +27,27 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
func getTemplate() -> CPTemplate {
|
|
35
|
-
return
|
|
30
|
+
return CPNowPlayingTemplate.shared
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
init(config: NowPlayingTemplateConfig) {
|
|
39
34
|
self.config = config
|
|
40
|
-
template = CPMapTemplate(id: config.id)
|
|
41
35
|
|
|
42
36
|
super.init()
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
template
|
|
46
|
-
|
|
47
|
-
DispatchQueue.main.async { [weak self] in
|
|
48
|
-
guard let self = self else { return }
|
|
49
|
-
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
50
|
-
self.setupRemoteCommandCenter()
|
|
51
|
-
self.updateNowPlayingInfo()
|
|
52
|
-
self.isSetupComplete = true
|
|
53
|
-
|
|
54
|
-
if let image = config.image {
|
|
55
|
-
self.loadImageAsync(image: image)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// MARK: - CPMapButton Controls
|
|
61
|
-
|
|
62
|
-
private static func buildMapButtons(isPlaying: Bool, owner: NowPlayingTemplate) -> [CPMapButton] {
|
|
63
|
-
let buttonSize = CPButtonMaximumImageSize
|
|
64
|
-
|
|
65
|
-
// Previous track button
|
|
66
|
-
let prevImage = UIImage(systemName: "backward.end.fill")?
|
|
67
|
-
.withTintColor(.white, renderingMode: .alwaysOriginal)
|
|
68
|
-
.resized(to: buttonSize)
|
|
69
|
-
let prevButton = CPMapButton(image: prevImage ?? UIImage()) { [weak owner] _ in
|
|
70
|
-
owner?.config.onPreviousTrack?()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Play/Pause button
|
|
74
|
-
let playPauseIconName = isPlaying ? "pause.circle.fill" : "play.circle.fill"
|
|
75
|
-
let playPauseImage = UIImage(systemName: playPauseIconName)?
|
|
76
|
-
.withTintColor(.white, renderingMode: .alwaysOriginal)
|
|
77
|
-
.resized(to: buttonSize)
|
|
78
|
-
let playPauseButton = CPMapButton(image: playPauseImage ?? UIImage()) { [weak owner] _ in
|
|
79
|
-
guard let owner = owner else { return }
|
|
80
|
-
DispatchQueue.main.async {
|
|
81
|
-
if owner.config.isPlaying {
|
|
82
|
-
owner.pauseAudio()
|
|
83
|
-
owner.config.onPause?()
|
|
84
|
-
} else {
|
|
85
|
-
owner.resumeAudio()
|
|
86
|
-
owner.config.onPlay?()
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Next track button
|
|
92
|
-
let nextImage = UIImage(systemName: "forward.end.fill")?
|
|
93
|
-
.withTintColor(.white, renderingMode: .alwaysOriginal)
|
|
94
|
-
.resized(to: buttonSize)
|
|
95
|
-
let nextButton = CPMapButton(image: nextImage ?? UIImage()) { [weak owner] _ in
|
|
96
|
-
owner?.config.onNextTrack?()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return [prevButton, playPauseButton, nextButton]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private func updateMapButtons() {
|
|
103
|
-
template.mapButtons = Self.buildMapButtons(isPlaying: config.isPlaying, owner: self)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// MARK: - Custom View Injection
|
|
38
|
+
// Set the config ID on the shared singleton so TemplateStore can find it
|
|
39
|
+
initTemplate(template: CPNowPlayingTemplate.shared, id: config.id)
|
|
107
40
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
previousRootVC = window.rootViewController
|
|
116
|
-
|
|
117
|
-
let customVC = NeoSkolaNowPlayingViewController()
|
|
118
|
-
|
|
119
|
-
let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
|
|
120
|
-
let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
|
|
121
|
-
customVC.updateInfo(courseName: subtitleText, lessonName: titleText)
|
|
122
|
-
customVC.updatePlaybackState(isPlaying: config.isPlaying)
|
|
123
|
-
|
|
124
|
-
window.rootViewController = customVC
|
|
125
|
-
window.makeKeyAndVisible()
|
|
126
|
-
|
|
127
|
-
self.customViewController = customVC
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
@MainActor
|
|
131
|
-
private func restoreOriginalView() {
|
|
132
|
-
guard let scene = SceneStore.getRootScene(),
|
|
133
|
-
let window = scene.window else { return }
|
|
41
|
+
// Setup remote commands and now playing info synchronously
|
|
42
|
+
// so they're ready before CPNowPlayingTemplate.shared is pushed
|
|
43
|
+
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
44
|
+
setupRemoteCommandCenter()
|
|
45
|
+
updateNowPlayingInfo()
|
|
46
|
+
isSetupComplete = true
|
|
134
47
|
|
|
135
|
-
if let
|
|
136
|
-
|
|
137
|
-
window.makeKeyAndVisible()
|
|
48
|
+
if let image = config.image {
|
|
49
|
+
loadImageAsync(image: image)
|
|
138
50
|
}
|
|
139
|
-
self.previousRootVC = nil
|
|
140
|
-
self.customViewController = nil
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// MARK: - Player UI
|
|
144
|
-
|
|
145
|
-
private func updatePlayerUI() {
|
|
146
|
-
guard let customVC = customViewController else { return }
|
|
147
|
-
|
|
148
|
-
let titleText = Parser.parseText(text: config.title) ?? ""
|
|
149
|
-
let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
|
|
150
|
-
customVC.updateInfo(courseName: subtitleText, lessonName: titleText)
|
|
151
|
-
customVC.updatePlaybackState(isPlaying: config.isPlaying)
|
|
152
|
-
customVC.updateTime(elapsed: currentElapsedTime, duration: currentDuration)
|
|
153
|
-
updateMapButtons()
|
|
154
51
|
}
|
|
155
52
|
|
|
156
53
|
// MARK: - Native Audio Playback
|
|
@@ -170,7 +67,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
170
67
|
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
171
68
|
config.isPlaying = true
|
|
172
69
|
updateNowPlayingInfo()
|
|
173
|
-
updatePlayerUI()
|
|
174
70
|
MPNowPlayingInfoCenter.default().playbackState = .playing
|
|
175
71
|
|
|
176
72
|
print("[NowPlayingTemplate] Downloading audio: \(url)")
|
|
@@ -229,7 +125,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
229
125
|
if !duration.isNaN && !duration.isInfinite && duration > 0 {
|
|
230
126
|
self.currentDuration = duration
|
|
231
127
|
self.updateNowPlayingInfo()
|
|
232
|
-
self.updatePlayerUI()
|
|
233
128
|
print("[NowPlayingTemplate] Duration resolved via KVO: \(duration)s")
|
|
234
129
|
}
|
|
235
130
|
}
|
|
@@ -299,8 +194,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
299
194
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
300
195
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
301
196
|
|
|
302
|
-
updatePlayerUI()
|
|
303
|
-
|
|
304
197
|
// 95% completion check
|
|
305
198
|
if !completionFired && currentDuration > 0 && currentTime / currentDuration >= 0.95 {
|
|
306
199
|
completionFired = true
|
|
@@ -318,7 +211,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
318
211
|
private func handlePlaybackFinished() {
|
|
319
212
|
config.isPlaying = false
|
|
320
213
|
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
321
|
-
updatePlayerUI()
|
|
322
214
|
if !completionFired {
|
|
323
215
|
completionFired = true
|
|
324
216
|
config.onComplete?()
|
|
@@ -331,7 +223,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
331
223
|
player?.pause()
|
|
332
224
|
config.isPlaying = false
|
|
333
225
|
updatePlaybackState(isPlaying: false)
|
|
334
|
-
updatePlayerUI()
|
|
335
226
|
reportProgress()
|
|
336
227
|
}
|
|
337
228
|
|
|
@@ -341,7 +232,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
341
232
|
player?.play()
|
|
342
233
|
config.isPlaying = true
|
|
343
234
|
updatePlaybackState(isPlaying: true)
|
|
344
|
-
updatePlayerUI()
|
|
345
235
|
}
|
|
346
236
|
|
|
347
237
|
@MainActor
|
|
@@ -366,7 +256,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
366
256
|
cleanupPlayer()
|
|
367
257
|
config.isPlaying = false
|
|
368
258
|
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
369
|
-
updatePlayerUI()
|
|
370
259
|
}
|
|
371
260
|
|
|
372
261
|
private func cleanupPlayer() {
|
|
@@ -483,7 +372,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
483
372
|
self.player?.seek(to: time)
|
|
484
373
|
self.currentElapsedTime = positionEvent.positionTime
|
|
485
374
|
self.updateNowPlayingInfo()
|
|
486
|
-
self.updatePlayerUI()
|
|
487
375
|
return .success
|
|
488
376
|
}
|
|
489
377
|
}
|
|
@@ -508,7 +396,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
508
396
|
|
|
509
397
|
@MainActor
|
|
510
398
|
func invalidate() {
|
|
511
|
-
updatePlayerUI()
|
|
512
399
|
updateNowPlayingInfo()
|
|
513
400
|
|
|
514
401
|
if loadedImage == nil, let image = config.image {
|
|
@@ -535,7 +422,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
535
422
|
func onPopped() {
|
|
536
423
|
config.onPopped?()
|
|
537
424
|
cleanupPlayer()
|
|
538
|
-
restoreOriginalView()
|
|
539
425
|
|
|
540
426
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
541
427
|
commandCenter.playCommand.removeTarget(nil)
|
|
@@ -556,7 +442,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
556
442
|
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
557
443
|
|
|
558
444
|
if !isSetupComplete {
|
|
559
|
-
updatePlayerUI()
|
|
560
445
|
updateNowPlayingInfo()
|
|
561
446
|
isSetupComplete = true
|
|
562
447
|
}
|
|
@@ -583,7 +468,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
583
468
|
config.title = AutoText(text: title, distance: nil, duration: nil)
|
|
584
469
|
config.subtitle = AutoText(text: subtitle, distance: nil, duration: nil)
|
|
585
470
|
updateNowPlayingInfo()
|
|
586
|
-
updatePlayerUI()
|
|
587
471
|
}
|
|
588
472
|
|
|
589
473
|
@MainActor
|
|
@@ -605,11 +489,5 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
605
489
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
|
|
606
490
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
607
491
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
608
|
-
|
|
609
|
-
updatePlayerUI()
|
|
610
492
|
}
|
|
611
|
-
|
|
612
|
-
// MARK: - CPMapTemplateDelegate
|
|
613
|
-
|
|
614
|
-
func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {}
|
|
615
493
|
}
|