@neoskola/auto-play 0.3.13 → 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,97 +51,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
57
51
|
}
|
|
58
52
|
}
|
|
59
53
|
|
|
60
|
-
// MARK: - CPMapButton Controls (transparent touch handlers)
|
|
61
|
-
|
|
62
|
-
private static func buildMapButtons(isPlaying: Bool, owner: NowPlayingTemplate) -> [CPMapButton] {
|
|
63
|
-
// 1x1 transparent image for invisible buttons
|
|
64
|
-
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 1, height: 1))
|
|
65
|
-
let clearImage = renderer.image { ctx in
|
|
66
|
-
UIColor.clear.setFill()
|
|
67
|
-
ctx.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Previous track button
|
|
71
|
-
let prevButton = CPMapButton(image: clearImage) { [weak owner] _ in
|
|
72
|
-
owner?.config.onPreviousTrack?()
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Play/Pause button
|
|
76
|
-
let playPauseButton = CPMapButton(image: clearImage) { [weak owner] _ in
|
|
77
|
-
guard let owner = owner else { return }
|
|
78
|
-
DispatchQueue.main.async {
|
|
79
|
-
if owner.config.isPlaying {
|
|
80
|
-
owner.pauseAudio()
|
|
81
|
-
owner.config.onPause?()
|
|
82
|
-
} else {
|
|
83
|
-
owner.resumeAudio()
|
|
84
|
-
owner.config.onPlay?()
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Next track button
|
|
90
|
-
let nextButton = CPMapButton(image: clearImage) { [weak owner] _ in
|
|
91
|
-
owner?.config.onNextTrack?()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return [prevButton, playPauseButton, nextButton]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private func updateMapButtons() {
|
|
98
|
-
template.mapButtons = Self.buildMapButtons(isPlaying: config.isPlaying, owner: self)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// MARK: - Custom View Injection
|
|
102
|
-
|
|
103
|
-
@MainActor
|
|
104
|
-
func injectCustomView() throws {
|
|
105
|
-
guard let scene = SceneStore.getRootScene(),
|
|
106
|
-
let window = scene.window else {
|
|
107
|
-
throw AutoPlayError.noUiWindow("NowPlaying: window nil")
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
previousRootVC = window.rootViewController
|
|
111
|
-
|
|
112
|
-
let customVC = NeoSkolaNowPlayingViewController()
|
|
113
|
-
|
|
114
|
-
let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
|
|
115
|
-
let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
|
|
116
|
-
customVC.updateInfo(courseName: subtitleText, lessonName: titleText)
|
|
117
|
-
customVC.updatePlaybackState(isPlaying: config.isPlaying)
|
|
118
|
-
|
|
119
|
-
window.rootViewController = customVC
|
|
120
|
-
window.makeKeyAndVisible()
|
|
121
|
-
|
|
122
|
-
self.customViewController = customVC
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
@MainActor
|
|
126
|
-
private func restoreOriginalView() {
|
|
127
|
-
guard let scene = SceneStore.getRootScene(),
|
|
128
|
-
let window = scene.window else { return }
|
|
129
|
-
|
|
130
|
-
if let previousRootVC = previousRootVC {
|
|
131
|
-
window.rootViewController = previousRootVC
|
|
132
|
-
window.makeKeyAndVisible()
|
|
133
|
-
}
|
|
134
|
-
self.previousRootVC = nil
|
|
135
|
-
self.customViewController = nil
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// MARK: - Player UI
|
|
139
|
-
|
|
140
|
-
private func updatePlayerUI() {
|
|
141
|
-
guard let customVC = customViewController else { return }
|
|
142
|
-
|
|
143
|
-
let titleText = Parser.parseText(text: config.title) ?? ""
|
|
144
|
-
let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
|
|
145
|
-
customVC.updateInfo(courseName: subtitleText, lessonName: titleText)
|
|
146
|
-
customVC.updatePlaybackState(isPlaying: config.isPlaying)
|
|
147
|
-
customVC.updateTime(elapsed: currentElapsedTime, duration: currentDuration)
|
|
148
|
-
updateMapButtons()
|
|
149
|
-
}
|
|
150
|
-
|
|
151
54
|
// MARK: - Native Audio Playback
|
|
152
55
|
|
|
153
56
|
@MainActor
|
|
@@ -165,7 +68,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
165
68
|
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
166
69
|
config.isPlaying = true
|
|
167
70
|
updateNowPlayingInfo()
|
|
168
|
-
updatePlayerUI()
|
|
169
71
|
MPNowPlayingInfoCenter.default().playbackState = .playing
|
|
170
72
|
|
|
171
73
|
print("[NowPlayingTemplate] Downloading audio: \(url)")
|
|
@@ -224,7 +126,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
224
126
|
if !duration.isNaN && !duration.isInfinite && duration > 0 {
|
|
225
127
|
self.currentDuration = duration
|
|
226
128
|
self.updateNowPlayingInfo()
|
|
227
|
-
self.updatePlayerUI()
|
|
228
129
|
print("[NowPlayingTemplate] Duration resolved via KVO: \(duration)s")
|
|
229
130
|
}
|
|
230
131
|
}
|
|
@@ -294,8 +195,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
294
195
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
295
196
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
296
197
|
|
|
297
|
-
updatePlayerUI()
|
|
298
|
-
|
|
299
198
|
// 95% completion check
|
|
300
199
|
if !completionFired && currentDuration > 0 && currentTime / currentDuration >= 0.95 {
|
|
301
200
|
completionFired = true
|
|
@@ -313,7 +212,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
313
212
|
private func handlePlaybackFinished() {
|
|
314
213
|
config.isPlaying = false
|
|
315
214
|
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
316
|
-
updatePlayerUI()
|
|
317
215
|
if !completionFired {
|
|
318
216
|
completionFired = true
|
|
319
217
|
config.onComplete?()
|
|
@@ -326,7 +224,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
326
224
|
player?.pause()
|
|
327
225
|
config.isPlaying = false
|
|
328
226
|
updatePlaybackState(isPlaying: false)
|
|
329
|
-
updatePlayerUI()
|
|
330
227
|
reportProgress()
|
|
331
228
|
}
|
|
332
229
|
|
|
@@ -336,7 +233,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
336
233
|
player?.play()
|
|
337
234
|
config.isPlaying = true
|
|
338
235
|
updatePlaybackState(isPlaying: true)
|
|
339
|
-
updatePlayerUI()
|
|
340
236
|
}
|
|
341
237
|
|
|
342
238
|
@MainActor
|
|
@@ -361,7 +257,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
361
257
|
cleanupPlayer()
|
|
362
258
|
config.isPlaying = false
|
|
363
259
|
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
364
|
-
updatePlayerUI()
|
|
365
260
|
}
|
|
366
261
|
|
|
367
262
|
private func cleanupPlayer() {
|
|
@@ -478,7 +373,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
478
373
|
self.player?.seek(to: time)
|
|
479
374
|
self.currentElapsedTime = positionEvent.positionTime
|
|
480
375
|
self.updateNowPlayingInfo()
|
|
481
|
-
self.updatePlayerUI()
|
|
482
376
|
return .success
|
|
483
377
|
}
|
|
484
378
|
}
|
|
@@ -503,7 +397,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
503
397
|
|
|
504
398
|
@MainActor
|
|
505
399
|
func invalidate() {
|
|
506
|
-
updatePlayerUI()
|
|
507
400
|
updateNowPlayingInfo()
|
|
508
401
|
|
|
509
402
|
if loadedImage == nil, let image = config.image {
|
|
@@ -530,7 +423,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
530
423
|
func onPopped() {
|
|
531
424
|
config.onPopped?()
|
|
532
425
|
cleanupPlayer()
|
|
533
|
-
restoreOriginalView()
|
|
534
426
|
|
|
535
427
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
536
428
|
commandCenter.playCommand.removeTarget(nil)
|
|
@@ -551,7 +443,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
551
443
|
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
552
444
|
|
|
553
445
|
if !isSetupComplete {
|
|
554
|
-
updatePlayerUI()
|
|
555
446
|
updateNowPlayingInfo()
|
|
556
447
|
isSetupComplete = true
|
|
557
448
|
}
|
|
@@ -578,7 +469,6 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
578
469
|
config.title = AutoText(text: title, distance: nil, duration: nil)
|
|
579
470
|
config.subtitle = AutoText(text: subtitle, distance: nil, duration: nil)
|
|
580
471
|
updateNowPlayingInfo()
|
|
581
|
-
updatePlayerUI()
|
|
582
472
|
}
|
|
583
473
|
|
|
584
474
|
@MainActor
|
|
@@ -600,11 +490,5 @@ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate {
|
|
|
600
490
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
|
|
601
491
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
602
492
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
603
|
-
|
|
604
|
-
updatePlayerUI()
|
|
605
493
|
}
|
|
606
|
-
|
|
607
|
-
// MARK: - CPMapTemplateDelegate
|
|
608
|
-
|
|
609
|
-
func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {}
|
|
610
494
|
}
|
|
@@ -12,12 +12,6 @@ class NeoSkolaNowPlayingViewController: UIViewController {
|
|
|
12
12
|
private let durationLabel = UILabel()
|
|
13
13
|
private let timeStack = UIStackView()
|
|
14
14
|
|
|
15
|
-
// MARK: - Button Controls (visual only, touch handled by CPMapButton)
|
|
16
|
-
private let buttonStack = UIStackView()
|
|
17
|
-
private let previousButton = UIButton(type: .custom)
|
|
18
|
-
private let playPauseButton = UIButton(type: .custom)
|
|
19
|
-
private let nextButton = UIButton(type: .custom)
|
|
20
|
-
|
|
21
15
|
// MARK: - Lifecycle
|
|
22
16
|
|
|
23
17
|
override func viewDidLoad() {
|
|
@@ -72,45 +66,6 @@ class NeoSkolaNowPlayingViewController: UIViewController {
|
|
|
72
66
|
timeStack.addArrangedSubview(currentTimeLabel)
|
|
73
67
|
timeStack.addArrangedSubview(durationLabel)
|
|
74
68
|
|
|
75
|
-
// Previous Button
|
|
76
|
-
previousButton.setImage(
|
|
77
|
-
UIImage(systemName: "backward.end.fill",
|
|
78
|
-
withConfiguration: UIImage.SymbolConfiguration(pointSize: 28, weight: .medium))?
|
|
79
|
-
.withTintColor(.white, renderingMode: .alwaysOriginal),
|
|
80
|
-
for: .normal
|
|
81
|
-
)
|
|
82
|
-
previousButton.backgroundColor = .clear
|
|
83
|
-
previousButton.isUserInteractionEnabled = false
|
|
84
|
-
|
|
85
|
-
// Play/Pause Button
|
|
86
|
-
playPauseButton.setImage(
|
|
87
|
-
UIImage(systemName: "play.circle.fill",
|
|
88
|
-
withConfiguration: UIImage.SymbolConfiguration(pointSize: 44, weight: .medium))?
|
|
89
|
-
.withTintColor(.white, renderingMode: .alwaysOriginal),
|
|
90
|
-
for: .normal
|
|
91
|
-
)
|
|
92
|
-
playPauseButton.backgroundColor = .clear
|
|
93
|
-
playPauseButton.isUserInteractionEnabled = false
|
|
94
|
-
|
|
95
|
-
// Next Button
|
|
96
|
-
nextButton.setImage(
|
|
97
|
-
UIImage(systemName: "forward.end.fill",
|
|
98
|
-
withConfiguration: UIImage.SymbolConfiguration(pointSize: 28, weight: .medium))?
|
|
99
|
-
.withTintColor(.white, renderingMode: .alwaysOriginal),
|
|
100
|
-
for: .normal
|
|
101
|
-
)
|
|
102
|
-
nextButton.backgroundColor = .clear
|
|
103
|
-
nextButton.isUserInteractionEnabled = false
|
|
104
|
-
|
|
105
|
-
// Button Stack (horizontal)
|
|
106
|
-
buttonStack.axis = .horizontal
|
|
107
|
-
buttonStack.alignment = .center
|
|
108
|
-
buttonStack.distribution = .equalSpacing
|
|
109
|
-
buttonStack.spacing = 40
|
|
110
|
-
buttonStack.addArrangedSubview(previousButton)
|
|
111
|
-
buttonStack.addArrangedSubview(playPauseButton)
|
|
112
|
-
buttonStack.addArrangedSubview(nextButton)
|
|
113
|
-
|
|
114
69
|
// Main container stack (vertical)
|
|
115
70
|
containerStack.axis = .vertical
|
|
116
71
|
containerStack.alignment = .fill
|
|
@@ -137,14 +92,6 @@ class NeoSkolaNowPlayingViewController: UIViewController {
|
|
|
137
92
|
containerStack.addArrangedSubview(progressView)
|
|
138
93
|
containerStack.addArrangedSubview(timeStack)
|
|
139
94
|
|
|
140
|
-
// Spacer between time and buttons
|
|
141
|
-
let spacer3 = UIView()
|
|
142
|
-
spacer3.translatesAutoresizingMaskIntoConstraints = false
|
|
143
|
-
spacer3.heightAnchor.constraint(equalToConstant: 16).isActive = true
|
|
144
|
-
containerStack.addArrangedSubview(spacer3)
|
|
145
|
-
|
|
146
|
-
containerStack.addArrangedSubview(buttonStack)
|
|
147
|
-
|
|
148
95
|
view.addSubview(containerStack)
|
|
149
96
|
}
|
|
150
97
|
|
|
@@ -170,14 +117,6 @@ class NeoSkolaNowPlayingViewController: UIViewController {
|
|
|
170
117
|
func updatePlaybackState(isPlaying: Bool) {
|
|
171
118
|
statusLabel.text = isPlaying ? "OYNATILIYOR" : "DURAKLATILDI"
|
|
172
119
|
statusLabel.textColor = isPlaying ? UIColor.systemGreen : UIColor.systemGray
|
|
173
|
-
|
|
174
|
-
let iconName = isPlaying ? "pause.circle.fill" : "play.circle.fill"
|
|
175
|
-
playPauseButton.setImage(
|
|
176
|
-
UIImage(systemName: iconName,
|
|
177
|
-
withConfiguration: UIImage.SymbolConfiguration(pointSize: 44, weight: .medium))?
|
|
178
|
-
.withTintColor(.white, renderingMode: .alwaysOriginal),
|
|
179
|
-
for: .normal
|
|
180
|
-
)
|
|
181
120
|
}
|
|
182
121
|
|
|
183
122
|
func updateTime(elapsed: Double, duration: Double) {
|