Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion Shfl/Domain/ShufflePlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ final class ShufflePlayer: ObservableObject {
rebuildQueueIfPlaying()
}

func addSongs(_ newSongs: [Song]) throws {
let existingIds = Set(songs.map(\.id))
let uniqueNewSongs = newSongs.filter { !existingIds.contains($0.id) }

let availableCapacity = Self.maxSongs - songs.count
guard uniqueNewSongs.count <= availableCapacity else {
throw ShufflePlayerError.capacityReached
}

songs.append(contentsOf: uniqueNewSongs)
// Don't rebuild queue during initial load - not playing yet
}

func removeSong(id: String) {
songs.removeAll { $0.id == id }
rebuildQueueIfPlaying()
Expand Down Expand Up @@ -203,9 +216,13 @@ final class ShufflePlayer: ObservableObject {
lastObservedSongId = nil

if !isQueuePrepared {
// Emit loading state for immediate UI feedback
if let firstSong = songs.first {
playbackState = .loading(firstSong)
}
try await prepareQueue()
}

try await musicService.play()
}

Expand Down
29 changes: 21 additions & 8 deletions Shfl/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class AppViewModel: ObservableObject {

@Published var isAuthorized = false
@Published var isLoading = true
@Published var loadingMessage = "Loading..."
@Published var showingManage = false
@Published var showingPicker = false
@Published var showingPickerDirect = false
Expand Down Expand Up @@ -40,21 +41,33 @@ final class AppViewModel: ObservableObject {
}

func onAppear() async {
isAuthorized = await musicService.isAuthorized
// Check authorization in parallel with song loading
loadingMessage = "Loading your music..."
async let authStatus = musicService.isAuthorized

// Load songs (synchronous SwiftData call, runs on MainActor)
var songs: [Song] = []
do {
let songs = try repository.loadSongs()
for song in songs {
try? player.addSong(song)
}
songs = try repository.loadSongs()
} catch {
print("Failed to load songs: \(error)")
}

isLoading = false
// Batch-add songs (O(n) instead of O(n²))
if !songs.isEmpty {
try? player.addSongs(songs)
}

// Wait for auth check
isAuthorized = await authStatus

// Prepare queue in background for instant playback
Task { try? await player.prepareQueue() }
// Prepare queue before dismissing loading screen
if !player.songs.isEmpty {
loadingMessage = "Preparing playback..."
try? await player.prepareQueue()
}

isLoading = false
}

func requestAuthorization() async {
Expand Down
3 changes: 2 additions & 1 deletion Shfl/Views/Components/PlayPauseButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ struct PlayPauseButton: View {
ZStack {
Circle()
.fill(buttonBackgroundColor)
.frame(width: buttonSize, height: buttonSize)
.colorEffect(
ShaderLibrary.shfl_brushedMetal(
.float2(buttonSize / 2, buttonSize / 2),
.float2(highlightOffset),
.float(theme.brushedMetalIntensity)
)
)
.frame(width: buttonSize, height: buttonSize)
.clipShape(Circle())
.shadow(
color: .black.opacity(0.1),
radius: isPressed ? ClickWheelFeedback.centerPressedShadowRadius : ClickWheelFeedback.centerNormalShadowRadius,
Expand Down
2 changes: 1 addition & 1 deletion Shfl/Views/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ struct MainView: View {
ProgressView()
.scaleEffect(1.5)

Text("Loading your music...")
Text(viewModel.loadingMessage)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Expand Down
Loading