@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:
|
|
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
|
-
|
|
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
|
-
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: -
|
|
60
|
+
// MARK: - Custom View Injection
|
|
120
61
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
151
|
-
let subtitleText = config.subtitle.flatMap { Parser.parseText(text: $0) } ?? ""
|
|
128
|
+
guard let customVC = customViewController else { return }
|
|
152
129
|
|
|
153
|
-
|
|
154
|
-
let
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
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
|
+
}
|