@neoskola/auto-play 0.3.8 → 0.3.10

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,269 @@
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
+ // Left side: artwork + info
14
+ private let leftColumn = UIView()
15
+ private let artworkImageView = UIImageView()
16
+ private let courseNameLabel = UILabel()
17
+ private let lessonNameLabel = UILabel()
18
+
19
+ // Right side: controls + progress
20
+ private let rightColumn = UIView()
21
+ private let statusLabel = UILabel()
22
+ private let controlsStack = UIStackView()
23
+ private let prevButton = UIButton(type: .system)
24
+ private let playPauseButton = UIButton(type: .system)
25
+ private let nextButton = UIButton(type: .system)
26
+ private let progressView = UIProgressView(progressViewStyle: .default)
27
+ private let currentTimeLabel = UILabel()
28
+ private let durationLabel = UILabel()
29
+ private let timeStack = UIStackView()
30
+
31
+ private var isPlaying = false
32
+
33
+ // MARK: - Lifecycle
34
+
35
+ override func viewDidLoad() {
36
+ super.viewDidLoad()
37
+ view.backgroundColor = UIColor(red: 0.06, green: 0.06, blue: 0.10, alpha: 1.0)
38
+ setupUI()
39
+ setupConstraints()
40
+ }
41
+
42
+ // MARK: - Setup
43
+
44
+ private func setupUI() {
45
+ // Left Column
46
+ leftColumn.translatesAutoresizingMaskIntoConstraints = false
47
+ view.addSubview(leftColumn)
48
+
49
+ // Artwork
50
+ artworkImageView.contentMode = .scaleAspectFill
51
+ artworkImageView.clipsToBounds = true
52
+ artworkImageView.layer.cornerRadius = 10
53
+ artworkImageView.backgroundColor = UIColor(white: 0.15, alpha: 1.0)
54
+ artworkImageView.translatesAutoresizingMaskIntoConstraints = false
55
+ leftColumn.addSubview(artworkImageView)
56
+
57
+ let placeholderConfig = UIImage.SymbolConfiguration(pointSize: 28, weight: .light)
58
+ artworkImageView.image = UIImage(systemName: "music.note", withConfiguration: placeholderConfig)
59
+ artworkImageView.tintColor = UIColor(white: 0.3, alpha: 1.0)
60
+
61
+ // Course Name
62
+ courseNameLabel.font = .systemFont(ofSize: 11, weight: .medium)
63
+ courseNameLabel.textColor = UIColor(white: 0.5, alpha: 1.0)
64
+ courseNameLabel.textAlignment = .center
65
+ courseNameLabel.numberOfLines = 1
66
+ courseNameLabel.adjustsFontSizeToFitWidth = true
67
+ courseNameLabel.minimumScaleFactor = 0.8
68
+ courseNameLabel.translatesAutoresizingMaskIntoConstraints = false
69
+ leftColumn.addSubview(courseNameLabel)
70
+
71
+ // Lesson Name
72
+ lessonNameLabel.font = .systemFont(ofSize: 13, weight: .bold)
73
+ lessonNameLabel.textColor = .white
74
+ lessonNameLabel.textAlignment = .center
75
+ lessonNameLabel.numberOfLines = 2
76
+ lessonNameLabel.adjustsFontSizeToFitWidth = true
77
+ lessonNameLabel.minimumScaleFactor = 0.7
78
+ lessonNameLabel.translatesAutoresizingMaskIntoConstraints = false
79
+ leftColumn.addSubview(lessonNameLabel)
80
+
81
+ // Right Column
82
+ rightColumn.translatesAutoresizingMaskIntoConstraints = false
83
+ view.addSubview(rightColumn)
84
+
85
+ // Status Label
86
+ statusLabel.font = .systemFont(ofSize: 10, weight: .semibold)
87
+ statusLabel.textColor = UIColor.systemGreen
88
+ statusLabel.textAlignment = .center
89
+ statusLabel.text = "OYNATILIYOR"
90
+ statusLabel.translatesAutoresizingMaskIntoConstraints = false
91
+ rightColumn.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
+ rightColumn.addSubview(progressView)
102
+
103
+ // Time Labels
104
+ currentTimeLabel.font = .monospacedDigitSystemFont(ofSize: 10, 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: 10, 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
+ rightColumn.addSubview(timeStack)
121
+ }
122
+
123
+ private func setupControls() {
124
+ let prevConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)
125
+ prevButton.setImage(UIImage(systemName: "backward.end.fill", withConfiguration: prevConfig), for: .normal)
126
+ prevButton.tintColor = .white
127
+ prevButton.addTarget(self, action: #selector(prevTapped), for: .touchUpInside)
128
+ prevButton.translatesAutoresizingMaskIntoConstraints = false
129
+
130
+ let playConfig = UIImage.SymbolConfiguration(pointSize: 36, weight: .medium)
131
+ playPauseButton.setImage(UIImage(systemName: "play.circle.fill", withConfiguration: playConfig), for: .normal)
132
+ playPauseButton.tintColor = UIColor.systemGreen
133
+ playPauseButton.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
134
+ playPauseButton.translatesAutoresizingMaskIntoConstraints = false
135
+
136
+ let nextConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)
137
+ nextButton.setImage(UIImage(systemName: "forward.end.fill", withConfiguration: nextConfig), for: .normal)
138
+ nextButton.tintColor = .white
139
+ nextButton.addTarget(self, action: #selector(nextTapped), for: .touchUpInside)
140
+ nextButton.translatesAutoresizingMaskIntoConstraints = false
141
+
142
+ controlsStack.axis = .horizontal
143
+ controlsStack.alignment = .center
144
+ controlsStack.distribution = .equalCentering
145
+ controlsStack.spacing = 28
146
+ controlsStack.addArrangedSubview(prevButton)
147
+ controlsStack.addArrangedSubview(playPauseButton)
148
+ controlsStack.addArrangedSubview(nextButton)
149
+ controlsStack.translatesAutoresizingMaskIntoConstraints = false
150
+ rightColumn.addSubview(controlsStack)
151
+ }
152
+
153
+ private func setupConstraints() {
154
+ let safeArea = view.safeAreaLayoutGuide
155
+
156
+ NSLayoutConstraint.activate([
157
+ // Left Column — sol taraf, artwork + text
158
+ leftColumn.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 8),
159
+ leftColumn.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 16),
160
+ leftColumn.bottomAnchor.constraint(lessThanOrEqualTo: safeArea.bottomAnchor, constant: -8),
161
+ leftColumn.widthAnchor.constraint(equalTo: safeArea.widthAnchor, multiplier: 0.35),
162
+
163
+ // Artwork — sol column ustunde, kare
164
+ artworkImageView.topAnchor.constraint(equalTo: leftColumn.topAnchor),
165
+ artworkImageView.centerXAnchor.constraint(equalTo: leftColumn.centerXAnchor),
166
+ artworkImageView.widthAnchor.constraint(equalTo: leftColumn.widthAnchor, constant: -8),
167
+ artworkImageView.heightAnchor.constraint(equalTo: artworkImageView.widthAnchor),
168
+
169
+ // Course Name — artwork altinda
170
+ courseNameLabel.topAnchor.constraint(equalTo: artworkImageView.bottomAnchor, constant: 6),
171
+ courseNameLabel.leadingAnchor.constraint(equalTo: leftColumn.leadingAnchor),
172
+ courseNameLabel.trailingAnchor.constraint(equalTo: leftColumn.trailingAnchor),
173
+
174
+ // Lesson Name — course altinda
175
+ lessonNameLabel.topAnchor.constraint(equalTo: courseNameLabel.bottomAnchor, constant: 2),
176
+ lessonNameLabel.leadingAnchor.constraint(equalTo: leftColumn.leadingAnchor),
177
+ lessonNameLabel.trailingAnchor.constraint(equalTo: leftColumn.trailingAnchor),
178
+
179
+ // Right Column — sag taraf, kontroller + progress
180
+ rightColumn.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 8),
181
+ rightColumn.leadingAnchor.constraint(equalTo: leftColumn.trailingAnchor, constant: 12),
182
+ rightColumn.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -16),
183
+ rightColumn.bottomAnchor.constraint(lessThanOrEqualTo: safeArea.bottomAnchor, constant: -8),
184
+
185
+ // Status — sag column ust
186
+ statusLabel.topAnchor.constraint(equalTo: rightColumn.topAnchor, constant: 8),
187
+ statusLabel.centerXAnchor.constraint(equalTo: rightColumn.centerXAnchor),
188
+
189
+ // Controls — dikey merkez
190
+ controlsStack.centerYAnchor.constraint(equalTo: rightColumn.centerYAnchor, constant: -4),
191
+ controlsStack.centerXAnchor.constraint(equalTo: rightColumn.centerXAnchor),
192
+
193
+ prevButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 40),
194
+ prevButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
195
+ playPauseButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 48),
196
+ playPauseButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 48),
197
+ nextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 40),
198
+ nextButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
199
+
200
+ // Progress Bar — controls altinda
201
+ progressView.topAnchor.constraint(equalTo: controlsStack.bottomAnchor, constant: 10),
202
+ progressView.leadingAnchor.constraint(equalTo: rightColumn.leadingAnchor),
203
+ progressView.trailingAnchor.constraint(equalTo: rightColumn.trailingAnchor),
204
+ progressView.heightAnchor.constraint(equalToConstant: 3),
205
+
206
+ // Time Stack — progress altinda
207
+ timeStack.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 3),
208
+ timeStack.leadingAnchor.constraint(equalTo: rightColumn.leadingAnchor),
209
+ timeStack.trailingAnchor.constraint(equalTo: rightColumn.trailingAnchor),
210
+ ])
211
+ }
212
+
213
+ // MARK: - Public Update Methods
214
+
215
+ func updateInfo(courseName: String, lessonName: String) {
216
+ courseNameLabel.text = courseName
217
+ lessonNameLabel.text = lessonName
218
+ }
219
+
220
+ func updatePlaybackState(isPlaying: Bool) {
221
+ self.isPlaying = isPlaying
222
+
223
+ let config = UIImage.SymbolConfiguration(pointSize: 36, weight: .medium)
224
+ let iconName = isPlaying ? "pause.circle.fill" : "play.circle.fill"
225
+ playPauseButton.setImage(UIImage(systemName: iconName, withConfiguration: config), for: .normal)
226
+ playPauseButton.tintColor = isPlaying ? UIColor.systemOrange : UIColor.systemGreen
227
+
228
+ statusLabel.text = isPlaying ? "OYNATILIYOR" : "DURAKLATILDI"
229
+ statusLabel.textColor = isPlaying ? UIColor.systemGreen : UIColor.systemGray
230
+ }
231
+
232
+ func updateTime(elapsed: Double, duration: Double) {
233
+ currentTimeLabel.text = formatTime(elapsed)
234
+ durationLabel.text = duration > 0 ? formatTime(duration) : "--:--"
235
+
236
+ if duration > 0 {
237
+ let progress = Float(min(elapsed / duration, 1.0))
238
+ progressView.setProgress(progress, animated: true)
239
+ }
240
+ }
241
+
242
+ func updateArtwork(image: UIImage) {
243
+ artworkImageView.image = image
244
+ artworkImageView.contentMode = .scaleAspectFill
245
+ }
246
+
247
+ // MARK: - Helpers
248
+
249
+ private func formatTime(_ seconds: Double) -> String {
250
+ guard !seconds.isNaN && !seconds.isInfinite && seconds >= 0 else { return "0:00" }
251
+ let mins = Int(seconds) / 60
252
+ let secs = Int(seconds) % 60
253
+ return String(format: "%d:%02d", mins, secs)
254
+ }
255
+
256
+ // MARK: - Actions
257
+
258
+ @objc private func prevTapped() {
259
+ delegate?.didTapPrevious()
260
+ }
261
+
262
+ @objc private func playPauseTapped() {
263
+ delegate?.didTapPlayPause()
264
+ }
265
+
266
+ @objc private func nextTapped() {
267
+ delegate?.didTapNext()
268
+ }
269
+ }
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.10",
4
4
  "description": "Android Auto and Apple CarPlay for react-native",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",