@neoskola/auto-play 0.3.8 → 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,82 +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 + progress bar accessory
41
- let infoItem = CPListItem(
42
- text: titleText,
43
- detailText: subtitleText,
44
- image: UIImage(systemName: "music.note")?
45
- .withTintColor(.systemPurple, renderingMode: .alwaysOriginal),
46
- accessoryImage: nil,
47
- accessoryType: .none
48
- )
49
- infoItem.isEnabled = false
50
- let infoSection = CPListSection(
51
- items: [infoItem],
52
- header: "Şimdi Oynatılıyor",
53
- sectionIndexTitle: nil
54
- )
55
-
56
- // Section 2: Sure bilgisi
57
- let timeItem = CPListItem(
58
- text: "Yükleniyor...",
59
- detailText: nil,
60
- image: UIImage(systemName: "clock")?
61
- .withTintColor(.systemGray, renderingMode: .alwaysOriginal),
62
- accessoryImage: nil,
63
- accessoryType: .none
64
- )
65
- timeItem.isEnabled = false
66
- let timeSection = CPListSection(
67
- items: [timeItem],
68
- header: nil,
69
- sectionIndexTitle: nil
70
- )
71
-
72
- // Section 3: Kontroller — renkli ikonlar
73
- let prevItem = CPListItem(
74
- text: "Önceki Bölüm",
75
- detailText: nil,
76
- image: UIImage(systemName: "backward.end.fill")?
77
- .withTintColor(.systemBlue, renderingMode: .alwaysOriginal),
78
- accessoryImage: nil,
79
- accessoryType: .none
80
- )
81
- let playPauseItem = CPListItem(
82
- text: "Oynat",
83
- detailText: nil,
84
- image: UIImage(systemName: "play.circle.fill")?
85
- .withTintColor(.systemGreen, renderingMode: .alwaysOriginal),
86
- accessoryImage: nil,
87
- accessoryType: .none
88
- )
89
- let nextItem = CPListItem(
90
- text: "Sonraki Bölüm",
91
- detailText: nil,
92
- image: UIImage(systemName: "forward.end.fill")?
93
- .withTintColor(.systemBlue, renderingMode: .alwaysOriginal),
94
- accessoryImage: nil,
95
- accessoryType: .none
96
- )
97
- let controlSection = CPListSection(
98
- items: [prevItem, playPauseItem, nextItem],
99
- header: nil,
100
- sectionIndexTitle: nil
101
- )
102
-
103
- template = CPListTemplate(
104
- title: titleText,
105
- sections: [infoSection, timeSection, controlSection],
106
- assistantCellConfiguration: nil,
107
- id: config.id
108
- )
109
-
110
- // Handler'lari ayarla
111
- setupListItemHandlers(prevItem: prevItem, playPauseItem: playPauseItem, nextItem: nextItem)
44
+ template.mapDelegate = self
45
+ template.mapButtons = []
112
46
 
113
47
  DispatchQueue.main.async { [weak self] in
114
48
  guard let self = self else { return }
@@ -123,173 +57,83 @@ class NowPlayingTemplate: AutoPlayTemplate {
123
57
  }
124
58
  }
125
59
 
126
- // MARK: - List Item Handlers
60
+ // MARK: - Custom View Injection
127
61
 
128
- private func setupListItemHandlers(prevItem: CPListItem, playPauseItem: CPListItem, nextItem: CPListItem) {
129
- prevItem.handler = { [weak self] _, completion in
130
- self?.config.onPreviousTrack?()
131
- completion()
132
- }
133
-
134
- playPauseItem.handler = { [weak self] _, completion in
135
- DispatchQueue.main.async {
136
- guard let self = self else { completion(); return }
137
- if self.config.isPlaying {
138
- self.pauseAudio()
139
- self.config.onPause?()
140
- } else {
141
- self.resumeAudio()
142
- self.config.onPlay?()
143
- }
144
- completion()
145
- }
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")
146
67
  }
147
68
 
148
- nextItem.handler = { [weak self] _, completion in
149
- self?.config.onNextTrack?()
150
- completion()
151
- }
152
- }
69
+ previousRootVC = window.rootViewController
153
70
 
154
- // MARK: - Player UI
71
+ let customVC = NeoSkolaNowPlayingViewController()
72
+ customVC.delegate = self
155
73
 
156
- private func updatePlayerUI() {
157
74
  let titleText = Parser.parseText(text: config.title) ?? "Now Playing"
158
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)
81
+ }
82
+
83
+ window.rootViewController = customVC
84
+ window.makeKeyAndVisible()
159
85
 
160
- // Progress hesapla
161
- let progress = currentDuration > 0 ? currentElapsedTime / currentDuration : 0
162
-
163
- // Section 1: Bilgi + progress bar accessory
164
- let infoItem = CPListItem(
165
- text: titleText,
166
- detailText: subtitleText,
167
- image: loadedImage ?? UIImage(systemName: "music.note")?
168
- .withTintColor(.systemPurple, renderingMode: .alwaysOriginal),
169
- accessoryImage: drawProgressBarImage(progress: progress),
170
- accessoryType: .none
171
- )
172
- infoItem.isEnabled = false
173
- let infoSection = CPListSection(
174
- items: [infoItem],
175
- header: "Şimdi Oynatılıyor",
176
- sectionIndexTitle: nil
177
- )
178
-
179
- // Section 2: Sure + yuzde detay
180
- let elapsed = formatTime(currentElapsedTime)
181
- let total = currentDuration > 0 ? formatTime(currentDuration) : "--:--"
182
- let timeText: String
183
- let timeIcon: UIImage?
184
- if config.isPlaying {
185
- timeText = "\(elapsed) / \(total)"
186
- timeIcon = UIImage(systemName: "waveform")?
187
- .withTintColor(.systemGreen, renderingMode: .alwaysOriginal)
188
- } else {
189
- timeText = "Duraklatıldı \(elapsed) / \(total)"
190
- timeIcon = UIImage(systemName: "pause.circle")?
191
- .withTintColor(.systemGray, renderingMode: .alwaysOriginal)
192
- }
193
-
194
- let progressPercent = currentDuration > 0
195
- ? Int((currentElapsedTime / currentDuration) * 100)
196
- : 0
197
- let timeDetailText: String? = currentDuration > 0 ? "%\(progressPercent) tamamlandı" : nil
198
-
199
- let timeItem = CPListItem(
200
- text: timeText,
201
- detailText: timeDetailText,
202
- image: timeIcon,
203
- accessoryImage: nil,
204
- accessoryType: .none
205
- )
206
- timeItem.isEnabled = false
207
- let timeSection = CPListSection(
208
- items: [timeItem],
209
- header: nil,
210
- sectionIndexTitle: nil
211
- )
212
-
213
- // Section 3: Kontroller — renkli ikonlar
214
- let prevItem = CPListItem(
215
- text: "Önceki Bölüm",
216
- detailText: nil,
217
- image: UIImage(systemName: "backward.end.fill")?
218
- .withTintColor(.systemBlue, renderingMode: .alwaysOriginal),
219
- accessoryImage: nil,
220
- accessoryType: .none
221
- )
222
-
223
- let playPauseText = config.isPlaying ? "Duraklat" : "Oynat"
224
- let playPauseIcon: UIImage? = config.isPlaying
225
- ? UIImage(systemName: "pause.circle.fill")?
226
- .withTintColor(.systemOrange, renderingMode: .alwaysOriginal)
227
- : UIImage(systemName: "play.circle.fill")?
228
- .withTintColor(.systemGreen, renderingMode: .alwaysOriginal)
229
- let playPauseItem = CPListItem(
230
- text: playPauseText,
231
- detailText: nil,
232
- image: playPauseIcon,
233
- accessoryImage: nil,
234
- accessoryType: .none
235
- )
236
-
237
- let nextItem = CPListItem(
238
- text: "Sonraki Bölüm",
239
- detailText: nil,
240
- image: UIImage(systemName: "forward.end.fill")?
241
- .withTintColor(.systemBlue, renderingMode: .alwaysOriginal),
242
- accessoryImage: nil,
243
- accessoryType: .none
244
- )
245
-
246
- setupListItemHandlers(prevItem: prevItem, playPauseItem: playPauseItem, nextItem: nextItem)
247
-
248
- let controlSection = CPListSection(
249
- items: [prevItem, playPauseItem, nextItem],
250
- header: nil,
251
- sectionIndexTitle: nil
252
- )
253
-
254
- template.updateSections([infoSection, timeSection, controlSection])
255
- }
256
-
257
- private func formatTime(_ seconds: Double) -> String {
258
- guard !seconds.isNaN && !seconds.isInfinite && seconds >= 0 else { return "0:00" }
259
- let mins = Int(seconds) / 60
260
- let secs = Int(seconds) % 60
261
- return String(format: "%d:%02d", mins, secs)
262
- }
263
-
264
- private func drawProgressBarImage(progress: Double) -> UIImage? {
265
- let size = CPListItem.maximumImageSize
266
- let renderer = UIGraphicsImageRenderer(size: size)
267
- return renderer.image { _ in
268
- let barHeight: CGFloat = 6
269
- let barY = (size.height - barHeight) / 2
270
- let cornerRadius = barHeight / 2
271
-
272
- // Arka plan (gri track)
273
- let trackPath = UIBezierPath(
274
- roundedRect: CGRect(x: 2, y: barY, width: size.width - 4, height: barHeight),
275
- cornerRadius: cornerRadius
276
- )
277
- UIColor.systemGray4.setFill()
278
- trackPath.fill()
279
-
280
- // Ilerleme (yesil fill)
281
- let progressWidth = max(0, (size.width - 4) * CGFloat(min(progress, 1.0)))
282
- if progressWidth > 0 {
283
- let progressPath = UIBezierPath(
284
- roundedRect: CGRect(x: 2, y: barY, width: progressWidth, height: barHeight),
285
- cornerRadius: cornerRadius
286
- )
287
- UIColor.systemGreen.setFill()
288
- progressPath.fill()
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?()
289
113
  }
290
114
  }
291
115
  }
292
116
 
117
+ func didTapPrevious() {
118
+ config.onPreviousTrack?()
119
+ }
120
+
121
+ func didTapNext() {
122
+ config.onNextTrack?()
123
+ }
124
+
125
+ // MARK: - Player UI
126
+
127
+ private func updatePlayerUI() {
128
+ guard let customVC = customViewController else { return }
129
+
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)
135
+ }
136
+
293
137
  // MARK: - Native Audio Playback
294
138
 
295
139
  @MainActor
@@ -436,7 +280,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
436
280
  nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
437
281
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
438
282
 
439
- // Update custom player UI
440
283
  updatePlayerUI()
441
284
 
442
285
  // 95% completion check
@@ -596,7 +439,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
596
439
  return .success
597
440
  }
598
441
 
599
- // Sonraki bolum
600
442
  commandCenter.nextTrackCommand.isEnabled = true
601
443
  commandCenter.nextTrackCommand.removeTarget(nil)
602
444
  commandCenter.nextTrackCommand.addTarget { [weak self] _ in
@@ -604,7 +446,6 @@ class NowPlayingTemplate: AutoPlayTemplate {
604
446
  return .success
605
447
  }
606
448
 
607
- // Onceki bolum
608
449
  commandCenter.previousTrackCommand.isEnabled = true
609
450
  commandCenter.previousTrackCommand.removeTarget(nil)
610
451
  commandCenter.previousTrackCommand.addTarget { [weak self] _ in
@@ -639,7 +480,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
639
480
  DispatchQueue.main.async {
640
481
  self.loadedImage = uiImage
641
482
  self.updateNowPlayingInfo()
642
- self.updatePlayerUI()
483
+ self.customViewController?.updateArtwork(image: uiImage)
643
484
  }
644
485
  }.resume()
645
486
  }
@@ -676,6 +517,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
676
517
  func onPopped() {
677
518
  config.onPopped?()
678
519
  cleanupPlayer()
520
+ restoreOriginalView()
679
521
 
680
522
  let commandCenter = MPRemoteCommandCenter.shared()
681
523
  commandCenter.playCommand.removeTarget(nil)
@@ -748,4 +590,8 @@ class NowPlayingTemplate: AutoPlayTemplate {
748
590
 
749
591
  updatePlayerUI()
750
592
  }
593
+
594
+ // MARK: - CPMapTemplateDelegate
595
+
596
+ func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {}
751
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.8",
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",