@neoskola/auto-play 0.3.14 → 0.3.15
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,17 +27,16 @@ 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
|
|
38
|
+
// Set the config ID on the shared singleton so TemplateStore can find it
|
|
39
|
+
initTemplate(template: CPNowPlayingTemplate.shared, id: config.id)
|
|
46
40
|
|
|
47
41
|
DispatchQueue.main.async { [weak self] in
|
|
48
42
|
guard let self = self else { return }
|
|
@@ -57,102 +51,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
57
51
|
}
|
|
58
52
|
}
|
|
59
53
|
|
|
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
|
|
107
|
-
|
|
108
|
-
@MainActor
|
|
109
|
-
func injectCustomView() throws {
|
|
110
|
-
guard let scene = SceneStore.getRootScene(),
|
|
111
|
-
let window = scene.window else {
|
|
112
|
-
throw AutoPlayError.noUiWindow("NowPlaying: window nil")
|
|
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 }
|
|
134
|
-
|
|
135
|
-
if let previousRootVC = previousRootVC {
|
|
136
|
-
window.rootViewController = previousRootVC
|
|
137
|
-
window.makeKeyAndVisible()
|
|
138
|
-
}
|
|
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
|
-
}
|
|
155
|
-
|
|
156
54
|
// MARK: - Native Audio Playback
|
|
157
55
|
|
|
158
56
|
@MainActor
|
|
@@ -170,7 +68,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
170
68
|
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
171
69
|
config.isPlaying = true
|
|
172
70
|
updateNowPlayingInfo()
|
|
173
|
-
updatePlayerUI()
|
|
174
71
|
MPNowPlayingInfoCenter.default().playbackState = .playing
|
|
175
72
|
|
|
176
73
|
print("[NowPlayingTemplate] Downloading audio: \(url)")
|
|
@@ -229,7 +126,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
229
126
|
if !duration.isNaN && !duration.isInfinite && duration > 0 {
|
|
230
127
|
self.currentDuration = duration
|
|
231
128
|
self.updateNowPlayingInfo()
|
|
232
|
-
self.updatePlayerUI()
|
|
233
129
|
print("[NowPlayingTemplate] Duration resolved via KVO: \(duration)s")
|
|
234
130
|
}
|
|
235
131
|
}
|
|
@@ -299,8 +195,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
299
195
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
300
196
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
301
197
|
|
|
302
|
-
updatePlayerUI()
|
|
303
|
-
|
|
304
198
|
// 95% completion check
|
|
305
199
|
if !completionFired && currentDuration > 0 && currentTime / currentDuration >= 0.95 {
|
|
306
200
|
completionFired = true
|
|
@@ -318,7 +212,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
318
212
|
private func handlePlaybackFinished() {
|
|
319
213
|
config.isPlaying = false
|
|
320
214
|
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
321
|
-
updatePlayerUI()
|
|
322
215
|
if !completionFired {
|
|
323
216
|
completionFired = true
|
|
324
217
|
config.onComplete?()
|
|
@@ -331,7 +224,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
331
224
|
player?.pause()
|
|
332
225
|
config.isPlaying = false
|
|
333
226
|
updatePlaybackState(isPlaying: false)
|
|
334
|
-
updatePlayerUI()
|
|
335
227
|
reportProgress()
|
|
336
228
|
}
|
|
337
229
|
|
|
@@ -341,7 +233,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
341
233
|
player?.play()
|
|
342
234
|
config.isPlaying = true
|
|
343
235
|
updatePlaybackState(isPlaying: true)
|
|
344
|
-
updatePlayerUI()
|
|
345
236
|
}
|
|
346
237
|
|
|
347
238
|
@MainActor
|
|
@@ -366,7 +257,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
366
257
|
cleanupPlayer()
|
|
367
258
|
config.isPlaying = false
|
|
368
259
|
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
369
|
-
updatePlayerUI()
|
|
370
260
|
}
|
|
371
261
|
|
|
372
262
|
private func cleanupPlayer() {
|
|
@@ -483,7 +373,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
483
373
|
self.player?.seek(to: time)
|
|
484
374
|
self.currentElapsedTime = positionEvent.positionTime
|
|
485
375
|
self.updateNowPlayingInfo()
|
|
486
|
-
self.updatePlayerUI()
|
|
487
376
|
return .success
|
|
488
377
|
}
|
|
489
378
|
}
|
|
@@ -508,7 +397,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
508
397
|
|
|
509
398
|
@MainActor
|
|
510
399
|
func invalidate() {
|
|
511
|
-
updatePlayerUI()
|
|
512
400
|
updateNowPlayingInfo()
|
|
513
401
|
|
|
514
402
|
if loadedImage == nil, let image = config.image {
|
|
@@ -535,7 +423,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
535
423
|
func onPopped() {
|
|
536
424
|
config.onPopped?()
|
|
537
425
|
cleanupPlayer()
|
|
538
|
-
restoreOriginalView()
|
|
539
426
|
|
|
540
427
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
541
428
|
commandCenter.playCommand.removeTarget(nil)
|
|
@@ -556,7 +443,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
556
443
|
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
557
444
|
|
|
558
445
|
if !isSetupComplete {
|
|
559
|
-
updatePlayerUI()
|
|
560
446
|
updateNowPlayingInfo()
|
|
561
447
|
isSetupComplete = true
|
|
562
448
|
}
|
|
@@ -583,7 +469,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
583
469
|
config.title = AutoText(text: title, distance: nil, duration: nil)
|
|
584
470
|
config.subtitle = AutoText(text: subtitle, distance: nil, duration: nil)
|
|
585
471
|
updateNowPlayingInfo()
|
|
586
|
-
updatePlayerUI()
|
|
587
472
|
}
|
|
588
473
|
|
|
589
474
|
@MainActor
|
|
@@ -605,11 +490,5 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
605
490
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
|
|
606
491
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
607
492
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
608
|
-
|
|
609
|
-
updatePlayerUI()
|
|
610
493
|
}
|
|
611
|
-
|
|
612
|
-
// MARK: - CPMapTemplateDelegate
|
|
613
|
-
|
|
614
|
-
func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {}
|
|
615
494
|
}
|