diff --git a/App/Continuity/Views/DownloadQueueButton.swift b/App/Continuity/Views/DownloadQueueButton.swift new file mode 100644 index 0000000..ac13be4 --- /dev/null +++ b/App/Continuity/Views/DownloadQueueButton.swift @@ -0,0 +1,42 @@ +import SwiftUI +import Ingest + +/// Live ingest affordance: arrow + remaining-job count in one glass capsule. +/// +/// Isolated so `ingestJobs` updates (byte progress included) only invalidate this control, +/// not the library grid or Now Playing chrome. The count is a single token (`19`, not +/// `4/4 19`) with `lineLimit(1)` + `fixedSize` so a toolbar or island-adjacent slot +/// cannot wrap it onto two lines. +struct DownloadQueueButton: View { + @Environment(PreparationQueue.self) private var prepQueue + @Binding var showingDownloads: Bool + /// Library toolbar stays visible so Downloads is always reachable. Now Playing hides + /// the control when the queue is empty so it doesn't sit under the Dynamic Island. + var showsWhenEmpty: Bool = true + + var body: some View { + let count = prepQueue.ingestJobs.count + if showsWhenEmpty || count > 0 { + Button { + showingDownloads = true + } label: { + HStack(spacing: 6) { + Image(systemName: count == 0 ? "arrow.down.circle" : "arrow.down.circle.fill") + if count > 0 { + Text("\(count)") + .font(.caption.weight(.bold).monospacedDigit()) + .lineLimit(1) + .minimumScaleFactor(1) + } + } + .padding(.horizontal, count > 0 ? 10 : 8) + .padding(.vertical, 6) + .continuityGlassCapsule(interactive: true) + // Grow with the digits; never compress into a square that wraps `419` as `4/4`/`19`. + .fixedSize(horizontal: true, vertical: true) + } + .buttonStyle(.plain) + .accessibilityLabel(count == 0 ? "Downloads" : "\(count) downloads in progress") + } + } +} diff --git a/App/Continuity/Views/LibrarySheetView.swift b/App/Continuity/Views/LibrarySheetView.swift index 6ed28b8..40e631f 100644 --- a/App/Continuity/Views/LibrarySheetView.swift +++ b/App/Continuity/Views/LibrarySheetView.swift @@ -24,7 +24,7 @@ struct LibrarySheetView: View { .navigationTitle("Continuity") .toolbar { ToolbarItem(placement: .topBarLeading) { - DownloadsToolbarButton(showingDownloads: $showingDownloads) + DownloadQueueButton(showingDownloads: $showingDownloads) } // Every action is a primaryAction so nothing collapses into a dead "…" // overflow menu (secondaryAction items did, and looked broken). @@ -119,30 +119,3 @@ private struct AddBadgeIcon: View { } } -/// Isolated so byte-level download progress only invalidates this control, not the library grid. -private struct DownloadsToolbarButton: View { - @Environment(PreparationQueue.self) private var prepQueue - @Binding var showingDownloads: Bool - - var body: some View { - let count = prepQueue.ingestJobs.count - return Button { - showingDownloads = true - } label: { - Image(systemName: count == 0 ? "arrow.down.circle" : "arrow.down.circle.fill") - } - .accessibilityLabel("Downloads") - .overlay(alignment: .topTrailing) { - if count > 0 { - Text("\(count)") - .font(.system(size: 9, weight: .bold)) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background(.tint, in: Capsule()) - .foregroundStyle(.white) - .offset(x: 8, y: -8) - .accessibilityHidden(true) - } - } - } -} diff --git a/App/Continuity/Views/NowPlayingView.swift b/App/Continuity/Views/NowPlayingView.swift index b158080..8dc52ca 100644 --- a/App/Continuity/Views/NowPlayingView.swift +++ b/App/Continuity/Views/NowPlayingView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Ingest import Playback import Domain @@ -12,6 +13,7 @@ struct NowPlayingView: View { @Environment(MainPagerState.self) private var pagerState @State private var showingTransitionSettings = false + @State private var showingDownloads = false var body: some View { // No backdrop here: MainPagerView supplies AlbumBackdrop as the page BACKGROUND @@ -58,6 +60,16 @@ struct NowPlayingView: View { } .padding(.top, 8) } + // Same control as the library toolbar, hidden when idle so it doesn't sit under + // the Dynamic Island. Isolated: job-count ticks don't rebuild this page. + .overlay(alignment: .topLeading) { + DownloadQueueButton(showingDownloads: $showingDownloads, showsWhenEmpty: false) + .padding(.leading, 16) + .padding(.top, 8) + } + .sheet(isPresented: $showingDownloads) { + DownloadsView() + } .overlay(alignment: .bottom) { pageChevron( system: "chevron.compact.down",