@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:
|
|
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
|
-
|
|
38
|
-
let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
|
|
42
|
+
super.init()
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
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: -
|
|
60
|
+
// MARK: - Custom View Injection
|
|
127
61
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
149
|
-
self?.config.onNextTrack?()
|
|
150
|
-
completion()
|
|
151
|
-
}
|
|
152
|
-
}
|
|
69
|
+
previousRootVC = window.rootViewController
|
|
153
70
|
|
|
154
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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.
|
|
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
|
+
}
|