@neoskola/auto-play 0.3.7 → 0.3.9

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,6 +188,13 @@ 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
+
191
198
  if let autoDismissMs = TemplateStore.getTemplate(
192
199
  templateId: templateId
193
200
  )?.autoDismissMs {
@@ -2,14 +2,18 @@ import CarPlay
2
2
  import MediaPlayer
3
3
  import AVFoundation
4
4
 
5
- class NowPlayingTemplate: AutoPlayTemplate {
6
- var template: CPListTemplate
5
+ class NowPlayingTemplate: NSObject, AutoPlayTemplate, CPMapTemplateDelegate, NowPlayingViewDelegate {
6
+ var template: CPMapTemplate
7
7
  var config: NowPlayingTemplateConfig
8
8
  private var loadedImage: UIImage?
9
9
  private var isSetupComplete = false
10
10
  private var currentElapsedTime: Double = 0
11
11
  private var currentDuration: Double = 0
12
12
 
13
+ // Custom UIKit view
14
+ private var customViewController: NeoSkolaNowPlayingViewController?
15
+ private var previousRootVC: UIViewController?
16
+
13
17
  // Native audio player
14
18
  private var player: AVPlayer?
15
19
  private var playerItem: AVPlayerItem?
@@ -33,75 +37,12 @@ class NowPlayingTemplate: AutoPlayTemplate {
33
37
 
34
38
  init(config: NowPlayingTemplateConfig) {
35
39
  self.config = config
40
+ template = CPMapTemplate(id: config.id)
36
41
 
37
- let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
38
- let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
42
+ super.init()
39
43
 
40
- // Section 1: Bilgi — ders ve kurs adi
41
- let infoItem = CPListItem(
42
- text: titleText,
43
- detailText: subtitleText,
44
- image: UIImage(systemName: "music.note"),
45
- accessoryImage: nil,
46
- accessoryType: .none
47
- )
48
- let infoSection = CPListSection(
49
- items: [infoItem],
50
- header: "Şimdi Oynatılıyor",
51
- sectionIndexTitle: nil
52
- )
53
-
54
- // Section 2: Sure bilgisi
55
- let timeItem = CPListItem(
56
- text: "Yükleniyor...",
57
- detailText: nil,
58
- image: UIImage(systemName: "clock"),
59
- accessoryImage: nil,
60
- accessoryType: .none
61
- )
62
- let timeSection = CPListSection(
63
- items: [timeItem],
64
- header: nil,
65
- sectionIndexTitle: nil
66
- )
67
-
68
- // Section 3: Kontroller — onceki, oynat/duraklat, sonraki
69
- let prevItem = CPListItem(
70
- text: "Önceki Bölüm",
71
- detailText: nil,
72
- image: UIImage(systemName: "backward.end.fill"),
73
- accessoryImage: nil,
74
- accessoryType: .none
75
- )
76
- let playPauseItem = CPListItem(
77
- text: "Oynat",
78
- detailText: nil,
79
- image: UIImage(systemName: "play.circle.fill"),
80
- accessoryImage: nil,
81
- accessoryType: .none
82
- )
83
- let nextItem = CPListItem(
84
- text: "Sonraki Bölüm",
85
- detailText: nil,
86
- image: UIImage(systemName: "forward.end.fill"),
87
- accessoryImage: nil,
88
- accessoryType: .none
89
- )
90
- let controlSection = CPListSection(
91
- items: [prevItem, playPauseItem, nextItem],
92
- header: nil,
93
- sectionIndexTitle: nil
94
- )
95
-
96
- template = CPListTemplate(
97
- title: "Now Playing",
98
- sections: [infoSection, timeSection, controlSection],
99
- assistantCellConfiguration: nil,
100
- id: config.id
101
- )
102
-
103
- // Handler'lari ayarla
104
- setupListItemHandlers(prevItem: prevItem, playPauseItem: playPauseItem, nextItem: nextItem)
44
+ template.mapDelegate = self
45
+ template.mapButtons = []
105
46
 
106
47
  DispatchQueue.main.async { [weak self] in
107
48
  guard let self = self else { return }
@@ -116,122 +57,81 @@ class NowPlayingTemplate: AutoPlayTemplate {
116
57
  }
117
58
  }
118
59
 
119
- // MARK: - List Item Handlers
60
+ // MARK: - Custom View Injection
120
61
 
121
- private func setupListItemHandlers(prevItem: CPListItem, playPauseItem: CPListItem, nextItem: CPListItem) {
122
- prevItem.handler = { [weak self] _, completion in
123
- self?.config.onPreviousTrack?()
124
- completion()
125
- }
126
-
127
- playPauseItem.handler = { [weak self] _, completion in
128
- DispatchQueue.main.async {
129
- guard let self = self else { completion(); return }
130
- if self.config.isPlaying {
131
- self.pauseAudio()
132
- self.config.onPause?()
133
- } else {
134
- self.resumeAudio()
135
- self.config.onPlay?()
136
- }
137
- completion()
138
- }
62
+ @MainActor
63
+ func injectCustomView() throws {
64
+ guard let scene = SceneStore.getRootScene(),
65
+ let window = scene.window else {
66
+ throw AutoPlayError.noUiWindow("NowPlaying: window nil")
139
67
  }
140
68
 
141
- nextItem.handler = { [weak self] _, completion in
142
- self?.config.onNextTrack?()
143
- completion()
69
+ previousRootVC = window.rootViewController
70
+
71
+ let customVC = NeoSkolaNowPlayingViewController()
72
+ customVC.delegate = self
73
+
74
+ let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
75
+ let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
76
+ customVC.updateInfo(courseName: subtitleText, lessonName: titleText)
77
+ customVC.updatePlaybackState(isPlaying: config.isPlaying)
78
+
79
+ if let loadedImage = loadedImage {
80
+ customVC.updateArtwork(image: loadedImage)
144
81
  }
82
+
83
+ window.rootViewController = customVC
84
+ window.makeKeyAndVisible()
85
+
86
+ self.customViewController = customVC
87
+ }
88
+
89
+ @MainActor
90
+ private func restoreOriginalView() {
91
+ guard let scene = SceneStore.getRootScene(),
92
+ let window = scene.window else { return }
93
+
94
+ if let previousRootVC = previousRootVC {
95
+ window.rootViewController = previousRootVC
96
+ window.makeKeyAndVisible()
97
+ }
98
+ self.previousRootVC = nil
99
+ self.customViewController = nil
100
+ }
101
+
102
+ // MARK: - NowPlayingViewDelegate
103
+
104
+ func didTapPlayPause() {
105
+ DispatchQueue.main.async { [weak self] in
106
+ guard let self = self else { return }
107
+ if self.config.isPlaying {
108
+ self.pauseAudio()
109
+ self.config.onPause?()
110
+ } else {
111
+ self.resumeAudio()
112
+ self.config.onPlay?()
113
+ }
114
+ }
115
+ }
116
+
117
+ func didTapPrevious() {
118
+ config.onPreviousTrack?()
119
+ }
120
+
121
+ func didTapNext() {
122
+ config.onNextTrack?()
145
123
  }
146
124
 
147
125
  // MARK: - Player UI
148
126
 
149
127
  private func updatePlayerUI() {
150
- let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
151
- let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
128
+ guard let customVC = customViewController else { return }
152
129
 
153
- // Section 1: Bilgi
154
- let infoItem = CPListItem(
155
- text: titleText,
156
- detailText: subtitleText,
157
- image: loadedImage ?? UIImage(systemName: "music.note"),
158
- accessoryImage: nil,
159
- accessoryType: .none
160
- )
161
- let infoSection = CPListSection(
162
- items: [infoItem],
163
- header: "Şimdi Oynatılıyor",
164
- sectionIndexTitle: nil
165
- )
166
-
167
- // Section 2: Sure
168
- let elapsed = formatTime(currentElapsedTime)
169
- let total = currentDuration > 0 ? formatTime(currentDuration) : "--:--"
170
- let timeText: String
171
- let timeIcon: String
172
- if config.isPlaying {
173
- timeText = "\(elapsed) / \(total)"
174
- timeIcon = "waveform"
175
- } else {
176
- timeText = "Duraklatıldı \(elapsed) / \(total)"
177
- timeIcon = "pause.circle"
178
- }
179
- let timeItem = CPListItem(
180
- text: timeText,
181
- detailText: nil,
182
- image: UIImage(systemName: timeIcon),
183
- accessoryImage: nil,
184
- accessoryType: .none
185
- )
186
- let timeSection = CPListSection(
187
- items: [timeItem],
188
- header: nil,
189
- sectionIndexTitle: nil
190
- )
191
-
192
- // Section 3: Kontroller
193
- let prevItem = CPListItem(
194
- text: "Önceki Bölüm",
195
- detailText: nil,
196
- image: UIImage(systemName: "backward.end.fill"),
197
- accessoryImage: nil,
198
- accessoryType: .none
199
- )
200
-
201
- let playPauseText = config.isPlaying ? "Duraklat" : "Oynat"
202
- let playPauseIcon = config.isPlaying ? "pause.circle.fill" : "play.circle.fill"
203
- let playPauseItem = CPListItem(
204
- text: playPauseText,
205
- detailText: nil,
206
- image: UIImage(systemName: playPauseIcon),
207
- accessoryImage: nil,
208
- accessoryType: .none
209
- )
210
-
211
- let nextItem = CPListItem(
212
- text: "Sonraki Bölüm",
213
- detailText: nil,
214
- image: UIImage(systemName: "forward.end.fill"),
215
- accessoryImage: nil,
216
- accessoryType: .none
217
- )
218
-
219
- setupListItemHandlers(prevItem: prevItem, playPauseItem: playPauseItem, nextItem: nextItem)
220
-
221
- let controlSection = CPListSection(
222
- items: [prevItem, playPauseItem, nextItem],
223
- header: nil,
224
- sectionIndexTitle: nil
225
- )
226
-
227
- template.updateSections([infoSection, timeSection, controlSection])
228
- }
229
-
230
- private func formatTime(_ seconds: Double) -> String {
231
- guard !seconds.isNaN && !seconds.isInfinite && seconds >= 0 else { return "0:00" }
232
- let mins = Int(seconds) / 60
233
- let secs = Int(seconds) % 60
234
- return String(format: "%d:%02d", mins, secs)
130
+ let titleText = Parser.parseText(text: config.title) ?? ""
131
+ let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
132
+ customVC.updateInfo(courseName: subtitleText, lessonName: titleText)
133
+ customVC.updatePlaybackState(isPlaying: config.isPlaying)
134
+ customVC.updateTime(elapsed: currentElapsedTime, duration: currentDuration)
235
135
  }
236
136
 
237
137
  // MARK: - Native Audio Playback
@@ -380,7 +280,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
380
280
  nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
381
281
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
382
282
 
383
- // Update custom player UI
384
283
  updatePlayerUI()
385
284
 
386
285
  // 95% completion check
@@ -540,7 +439,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
540
439
  return .success
541
440
  }
542
441
 
543
- // Sonraki bolum
544
442
  commandCenter.nextTrackCommand.isEnabled = true
545
443
  commandCenter.nextTrackCommand.removeTarget(nil)
546
444
  commandCenter.nextTrackCommand.addTarget { [weak self] _ in
@@ -548,7 +446,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
548
446
  return .success
549
447
  }
550
448
 
551
- // Onceki bolum
552
449
  commandCenter.previousTrackCommand.isEnabled = true
553
450
  commandCenter.previousTrackCommand.removeTarget(nil)
554
451
  commandCenter.previousTrackCommand.addTarget { [weak self] _ in
@@ -583,7 +480,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
583
480
  DispatchQueue.main.async {
584
481
  self.loadedImage = uiImage
585
482
  self.updateNowPlayingInfo()
586
- self.updatePlayerUI()
483
+ self.customViewController?.updateArtwork(image: uiImage)
587
484
  }
588
485
  }.resume()
589
486
  }
@@ -620,6 +517,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
620
517
  func onPopped() {
621
518
  config.onPopped?()
622
519
  cleanupPlayer()
520
+ restoreOriginalView()
623
521
 
624
522
  let commandCenter = MPRemoteCommandCenter.shared()
625
523
  commandCenter.playCommand.removeTarget(nil)
@@ -692,4 +590,8 @@ class NowPlayingTemplate: AutoPlayTemplate {
692
590
 
693
591
  updatePlayerUI()
694
592
  }
593
+
594
+ // MARK: - CPMapTemplateDelegate
595
+
596
+ func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {}
695
597
  }
@@ -0,0 +1,275 @@
1
+ import UIKit
2
+
3
+ protocol NowPlayingViewDelegate: AnyObject {
4
+ func didTapPlayPause()
5
+ func didTapPrevious()
6
+ func didTapNext()
7
+ }
8
+
9
+ class NeoSkolaNowPlayingViewController: UIViewController {
10
+ weak var delegate: NowPlayingViewDelegate?
11
+
12
+ // MARK: - UI Elements
13
+ private let containerView = UIView()
14
+ private let courseNameLabel = UILabel()
15
+ private let lessonNameLabel = UILabel()
16
+ private let artworkImageView = UIImageView()
17
+ private let artworkContainer = UIView()
18
+ private let statusLabel = UILabel()
19
+ private let controlsStack = UIStackView()
20
+ private let prevButton = UIButton(type: .system)
21
+ private let playPauseButton = UIButton(type: .system)
22
+ private let nextButton = UIButton(type: .system)
23
+ private let progressView = UIProgressView(progressViewStyle: .default)
24
+ private let currentTimeLabel = UILabel()
25
+ private let durationLabel = UILabel()
26
+ private let timeStack = UIStackView()
27
+
28
+ private var isPlaying = false
29
+
30
+ // MARK: - Lifecycle
31
+
32
+ override func viewDidLoad() {
33
+ super.viewDidLoad()
34
+ view.backgroundColor = UIColor(red: 0.06, green: 0.06, blue: 0.10, alpha: 1.0)
35
+ setupUI()
36
+ setupConstraints()
37
+ }
38
+
39
+ // MARK: - Setup
40
+
41
+ private func setupUI() {
42
+ containerView.translatesAutoresizingMaskIntoConstraints = false
43
+ view.addSubview(containerView)
44
+
45
+ // Course Name (kucuk, gri)
46
+ courseNameLabel.font = .systemFont(ofSize: 13, weight: .medium)
47
+ courseNameLabel.textColor = UIColor(white: 0.55, alpha: 1.0)
48
+ courseNameLabel.textAlignment = .center
49
+ courseNameLabel.numberOfLines = 1
50
+ courseNameLabel.translatesAutoresizingMaskIntoConstraints = false
51
+ containerView.addSubview(courseNameLabel)
52
+
53
+ // Lesson Name (buyuk, beyaz, bold)
54
+ lessonNameLabel.font = .systemFont(ofSize: 19, weight: .bold)
55
+ lessonNameLabel.textColor = .white
56
+ lessonNameLabel.textAlignment = .center
57
+ lessonNameLabel.numberOfLines = 2
58
+ lessonNameLabel.adjustsFontSizeToFitWidth = true
59
+ lessonNameLabel.minimumScaleFactor = 0.7
60
+ lessonNameLabel.translatesAutoresizingMaskIntoConstraints = false
61
+ containerView.addSubview(lessonNameLabel)
62
+
63
+ // Artwork Container (golge ve yuvarlak koseler)
64
+ artworkContainer.translatesAutoresizingMaskIntoConstraints = false
65
+ artworkContainer.layer.cornerRadius = 14
66
+ artworkContainer.layer.shadowColor = UIColor.black.cgColor
67
+ artworkContainer.layer.shadowOpacity = 0.5
68
+ artworkContainer.layer.shadowOffset = CGSize(width: 0, height: 4)
69
+ artworkContainer.layer.shadowRadius = 12
70
+ containerView.addSubview(artworkContainer)
71
+
72
+ // Artwork Image
73
+ artworkImageView.contentMode = .scaleAspectFill
74
+ artworkImageView.clipsToBounds = true
75
+ artworkImageView.layer.cornerRadius = 14
76
+ artworkImageView.backgroundColor = UIColor(white: 0.15, alpha: 1.0)
77
+ artworkImageView.translatesAutoresizingMaskIntoConstraints = false
78
+ artworkContainer.addSubview(artworkImageView)
79
+
80
+ // Placeholder icon
81
+ let placeholderConfig = UIImage.SymbolConfiguration(pointSize: 32, weight: .light)
82
+ artworkImageView.image = UIImage(systemName: "music.note", withConfiguration: placeholderConfig)
83
+ artworkImageView.tintColor = UIColor(white: 0.3, alpha: 1.0)
84
+
85
+ // Status Label
86
+ statusLabel.font = .systemFont(ofSize: 11, weight: .semibold)
87
+ statusLabel.textColor = UIColor.systemGreen
88
+ statusLabel.textAlignment = .center
89
+ statusLabel.text = "OYNATILIYOR"
90
+ statusLabel.translatesAutoresizingMaskIntoConstraints = false
91
+ containerView.addSubview(statusLabel)
92
+
93
+ // Controls
94
+ setupControls()
95
+
96
+ // Progress Bar
97
+ progressView.progressTintColor = UIColor.systemGreen
98
+ progressView.trackTintColor = UIColor(white: 0.2, alpha: 1.0)
99
+ progressView.progress = 0
100
+ progressView.translatesAutoresizingMaskIntoConstraints = false
101
+ containerView.addSubview(progressView)
102
+
103
+ // Time Labels
104
+ currentTimeLabel.font = .monospacedDigitSystemFont(ofSize: 11, weight: .medium)
105
+ currentTimeLabel.textColor = UIColor(white: 0.45, alpha: 1.0)
106
+ currentTimeLabel.text = "0:00"
107
+ currentTimeLabel.translatesAutoresizingMaskIntoConstraints = false
108
+
109
+ durationLabel.font = .monospacedDigitSystemFont(ofSize: 11, weight: .medium)
110
+ durationLabel.textColor = UIColor(white: 0.45, alpha: 1.0)
111
+ durationLabel.text = "--:--"
112
+ durationLabel.textAlignment = .right
113
+ durationLabel.translatesAutoresizingMaskIntoConstraints = false
114
+
115
+ timeStack.axis = .horizontal
116
+ timeStack.distribution = .equalSpacing
117
+ timeStack.addArrangedSubview(currentTimeLabel)
118
+ timeStack.addArrangedSubview(durationLabel)
119
+ timeStack.translatesAutoresizingMaskIntoConstraints = false
120
+ containerView.addSubview(timeStack)
121
+ }
122
+
123
+ private func setupControls() {
124
+ // Previous Button
125
+ let prevConfig = UIImage.SymbolConfiguration(pointSize: 24, weight: .medium)
126
+ prevButton.setImage(UIImage(systemName: "backward.end.fill", withConfiguration: prevConfig), for: .normal)
127
+ prevButton.tintColor = .white
128
+ prevButton.addTarget(self, action: #selector(prevTapped), for: .touchUpInside)
129
+ prevButton.translatesAutoresizingMaskIntoConstraints = false
130
+
131
+ // Play/Pause Button (buyuk)
132
+ let playConfig = UIImage.SymbolConfiguration(pointSize: 42, weight: .medium)
133
+ playPauseButton.setImage(UIImage(systemName: "play.circle.fill", withConfiguration: playConfig), for: .normal)
134
+ playPauseButton.tintColor = UIColor.systemGreen
135
+ playPauseButton.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
136
+ playPauseButton.translatesAutoresizingMaskIntoConstraints = false
137
+
138
+ // Next Button
139
+ let nextConfig = UIImage.SymbolConfiguration(pointSize: 24, weight: .medium)
140
+ nextButton.setImage(UIImage(systemName: "forward.end.fill", withConfiguration: nextConfig), for: .normal)
141
+ nextButton.tintColor = .white
142
+ nextButton.addTarget(self, action: #selector(nextTapped), for: .touchUpInside)
143
+ nextButton.translatesAutoresizingMaskIntoConstraints = false
144
+
145
+ // Stack
146
+ controlsStack.axis = .horizontal
147
+ controlsStack.alignment = .center
148
+ controlsStack.distribution = .equalCentering
149
+ controlsStack.spacing = 40
150
+ controlsStack.addArrangedSubview(prevButton)
151
+ controlsStack.addArrangedSubview(playPauseButton)
152
+ controlsStack.addArrangedSubview(nextButton)
153
+ controlsStack.translatesAutoresizingMaskIntoConstraints = false
154
+ containerView.addSubview(controlsStack)
155
+ }
156
+
157
+ private func setupConstraints() {
158
+ let safeArea = view.safeAreaLayoutGuide
159
+
160
+ NSLayoutConstraint.activate([
161
+ // Container — tam ekran, safe area icinde
162
+ containerView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 8),
163
+ containerView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 20),
164
+ containerView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -20),
165
+ containerView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -8),
166
+
167
+ // Course Name — top
168
+ courseNameLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 4),
169
+ courseNameLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
170
+ courseNameLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
171
+
172
+ // Lesson Name — course name altinda
173
+ lessonNameLabel.topAnchor.constraint(equalTo: courseNameLabel.bottomAnchor, constant: 4),
174
+ lessonNameLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
175
+ lessonNameLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
176
+
177
+ // Artwork Container — merkez
178
+ artworkContainer.topAnchor.constraint(equalTo: lessonNameLabel.bottomAnchor, constant: 12),
179
+ artworkContainer.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
180
+ artworkContainer.widthAnchor.constraint(equalToConstant: 110),
181
+ artworkContainer.heightAnchor.constraint(equalToConstant: 110),
182
+
183
+ // Artwork Image — container'i doldur
184
+ artworkImageView.topAnchor.constraint(equalTo: artworkContainer.topAnchor),
185
+ artworkImageView.leadingAnchor.constraint(equalTo: artworkContainer.leadingAnchor),
186
+ artworkImageView.trailingAnchor.constraint(equalTo: artworkContainer.trailingAnchor),
187
+ artworkImageView.bottomAnchor.constraint(equalTo: artworkContainer.bottomAnchor),
188
+
189
+ // Status Label
190
+ statusLabel.topAnchor.constraint(equalTo: artworkContainer.bottomAnchor, constant: 10),
191
+ statusLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
192
+
193
+ // Controls Stack — status altinda
194
+ controlsStack.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 10),
195
+ controlsStack.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
196
+ controlsStack.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor, multiplier: 0.7),
197
+
198
+ // Button min sizes
199
+ prevButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
200
+ prevButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
201
+ playPauseButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 54),
202
+ playPauseButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 54),
203
+ nextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
204
+ nextButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
205
+
206
+ // Progress Bar
207
+ progressView.topAnchor.constraint(equalTo: controlsStack.bottomAnchor, constant: 14),
208
+ progressView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
209
+ progressView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
210
+ progressView.heightAnchor.constraint(equalToConstant: 3),
211
+
212
+ // Time Stack
213
+ timeStack.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 4),
214
+ timeStack.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
215
+ timeStack.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
216
+ ])
217
+ }
218
+
219
+ // MARK: - Public Update Methods
220
+
221
+ func updateInfo(courseName: String, lessonName: String) {
222
+ courseNameLabel.text = courseName
223
+ lessonNameLabel.text = lessonName
224
+ }
225
+
226
+ func updatePlaybackState(isPlaying: Bool) {
227
+ self.isPlaying = isPlaying
228
+
229
+ let config = UIImage.SymbolConfiguration(pointSize: 42, weight: .medium)
230
+ let iconName = isPlaying ? "pause.circle.fill" : "play.circle.fill"
231
+ playPauseButton.setImage(UIImage(systemName: iconName, withConfiguration: config), for: .normal)
232
+ playPauseButton.tintColor = isPlaying ? UIColor.systemOrange : UIColor.systemGreen
233
+
234
+ statusLabel.text = isPlaying ? "OYNATILIYOR" : "DURAKLATILDI"
235
+ statusLabel.textColor = isPlaying ? UIColor.systemGreen : UIColor.systemGray
236
+ }
237
+
238
+ func updateTime(elapsed: Double, duration: Double) {
239
+ currentTimeLabel.text = formatTime(elapsed)
240
+ durationLabel.text = duration > 0 ? formatTime(duration) : "--:--"
241
+
242
+ if duration > 0 {
243
+ let progress = Float(min(elapsed / duration, 1.0))
244
+ progressView.setProgress(progress, animated: true)
245
+ }
246
+ }
247
+
248
+ func updateArtwork(image: UIImage) {
249
+ artworkImageView.image = image
250
+ artworkImageView.contentMode = .scaleAspectFill
251
+ }
252
+
253
+ // MARK: - Helpers
254
+
255
+ private func formatTime(_ seconds: Double) -> String {
256
+ guard !seconds.isNaN && !seconds.isInfinite && seconds >= 0 else { return "0:00" }
257
+ let mins = Int(seconds) / 60
258
+ let secs = Int(seconds) % 60
259
+ return String(format: "%d:%02d", mins, secs)
260
+ }
261
+
262
+ // MARK: - Actions
263
+
264
+ @objc private func prevTapped() {
265
+ delegate?.didTapPrevious()
266
+ }
267
+
268
+ @objc private func playPauseTapped() {
269
+ delegate?.didTapPlayPause()
270
+ }
271
+
272
+ @objc private func nextTapped() {
273
+ delegate?.didTapNext()
274
+ }
275
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neoskola/auto-play",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Android Auto and Apple CarPlay for react-native",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",