@@ -37,6 +37,18 @@ struct AudioPlayerControls: View {
3737 }
3838 . buttonStyle ( . plain)
3939 . padding ( . leading, 8 )
40+ Button ( action: {
41+ model. cycleLoopMode ( )
42+ } ) {
43+ Image ( systemName: model. iconForLoopMode)
44+ . symbolVariant ( model. isLoopActive ? . circle. fill : . none)
45+ . font ( . title)
46+ . imageScale ( . small)
47+ . foregroundStyle ( model. isLoopActive ? . mint : . gray)
48+ }
49+ . buttonStyle ( . plain)
50+ . padding ( . leading, 8 )
51+ . help ( model. loopModeDescription)
4052 Spacer ( )
4153 HStack {
4254 Slider ( value: $model. volume)
@@ -101,6 +113,31 @@ struct AudioPlayerControls: View {
101113 }
102114 . padding ( . bottom, 8 )
103115 . padding ( . horizontal, 16 )
116+
117+ if model. isLoopActive {
118+ Divider ( )
119+ VStack ( alignment: . leading) {
120+ Text ( " Loop Times: \( model. loopTimes == 0 ? " ∞ " : " \( Int ( model. loopTimes) ) " ) " )
121+ . font ( . subheadline)
122+ . fontWeight ( . medium)
123+ . foregroundStyle ( . black)
124+ HStack {
125+ Text ( " 1 " )
126+ . font ( . caption)
127+ Slider ( value: $model. loopTimes, in: 0 ... 10 , step: 1 )
128+ . onChange ( of: model. loopTimes) { _, new in
129+ model. updateLoopTimes ( new)
130+ }
131+ Text ( " ∞ " )
132+ . font ( . caption)
133+ }
134+ Text ( model. loopTimes == 0 ? " Loop infinitely " : " Loop \( Int ( model. loopTimes) ) time \( Int ( model. loopTimes) == 1 ? " " : " s " ) " )
135+ . font ( . caption2)
136+ . foregroundStyle ( . secondary)
137+ }
138+ . padding ( . bottom, 8 )
139+ . padding ( . horizontal, 16 )
140+ }
104141 }
105142 . onChange ( of: currentTrack) { oldValue, newValue in
106143 if let track = newValue {
@@ -132,6 +169,9 @@ extension AudioPlayerControls {
132169
133170 var isPlaying : Bool = false
134171 var isMuted : Bool = false
172+
173+ var loopMode : AudioPlayerLoopMode = . off
174+ var loopTimes : Double = 0 // 0 means infinite
135175
136176 var volume : Float = 0.5
137177
@@ -159,6 +199,52 @@ extension AudioPlayerControls {
159199 return " speaker.wave.3 "
160200 }
161201 }
202+
203+ var iconForLoopMode : String {
204+ switch loopMode {
205+ case . off:
206+ return " repeat "
207+ case . single:
208+ return " repeat.1 "
209+ case . all:
210+ return " repeat "
211+ }
212+ }
213+
214+ var loopModeDescription : String {
215+ switch loopMode {
216+ case . off:
217+ return " Loop: Off "
218+ case . single( let times) :
219+ if let times = times, times > 0 {
220+ return " Loop: Single ( \( times) x) "
221+ }
222+ return " Loop: Single (∞) "
223+ case . all( let times) :
224+ if let times = times, times > 0 {
225+ return " Loop: All ( \( times) x) "
226+ }
227+ return " Loop: All (∞) "
228+ }
229+ }
230+
231+ var isLoopActive : Bool {
232+ if case . off = loopMode {
233+ return false
234+ }
235+ return true
236+ }
237+
238+ var currentLoopTimes : Int ? {
239+ switch loopMode {
240+ case . off:
241+ return nil
242+ case . single( let times) :
243+ return times
244+ case . all( let times) :
245+ return times
246+ }
247+ }
162248
163249 init ( audioPlayerService: AudioPlayerService ) {
164250 self . audioPlayerService = audioPlayerService
@@ -220,6 +306,19 @@ extension AudioPlayerControls {
220306 isMuted. toggle ( )
221307 audioPlayerService. toggleMute ( )
222308 }
309+
310+ func cycleLoopMode( ) {
311+ audioPlayerService. cycleLoopMode ( )
312+ loopMode = audioPlayerService. loopMode
313+ // Update loopTimes to match the current mode's times
314+ loopTimes = Double ( currentLoopTimes ?? 0 )
315+ }
316+
317+ func updateLoopTimes( _ times: Double ) {
318+ let timesValue = times == 0 ? nil : Int ( times)
319+ audioPlayerService. setLoopTimes ( timesValue)
320+ loopMode = audioPlayerService. loopMode
321+ }
223322
224323 func playPause( ) {
225324 if audioPlayerService. state == . playing {
0 commit comments