diff --git a/Ruddarr/Models/Queue/QueueItem.swift b/Ruddarr/Models/Queue/QueueItem.swift index 44417445..f49fe176 100644 --- a/Ruddarr/Models/Queue/QueueItem.swift +++ b/Ruddarr/Models/Queue/QueueItem.swift @@ -154,7 +154,17 @@ struct QueueItem: Codable, Identifiable, Equatable { var progressLabel: String { guard sizeleft > 0 else { return 100.formatted(.percent) } - return ((size - sizeleft) / size).formatted(.percent.precision(.fractionLength(1))) + return progressFraction.formatted(.percent.precision(.fractionLength(1))) + } + + var progressFraction: Float { + guard size > 0 else { return sizeleft <= 0 ? 1 : 0 } + return min(max((size - sizeleft) / size, 0), 1) + } + + var hasDownloadProgress: Bool { + trackedDownloadState == .downloading || + status == "downloading" } var remainingLabel: String? { diff --git a/Ruddarr/Views/Calendar/CalendarMedia.swift b/Ruddarr/Views/Calendar/CalendarMedia.swift index 986059e2..9422770e 100644 --- a/Ruddarr/Views/Calendar/CalendarMedia.swift +++ b/Ruddarr/Views/Calendar/CalendarMedia.swift @@ -3,6 +3,7 @@ import SwiftUI struct CalendarMovie: View { var date: Date var movie: Movie + var downloadProgress: Float? @EnvironmentObject var settings: AppSettings @@ -17,10 +18,7 @@ struct CalendarMovie: View { Spacer() - statusIcon - .font(.subheadline) - .imageScale(.small) - .foregroundStyle(.secondary) + status } if let type = movie.releaseType(for: date) { @@ -51,6 +49,18 @@ struct CalendarMovie: View { !movie.monitored && !movie.isDownloaded } + @ViewBuilder + var status: some View { + if let downloadProgress { + CalendarDownloadProgress(progress: downloadProgress) + } else { + statusIcon + .font(.subheadline) + .imageScale(.small) + .foregroundStyle(.secondary) + } + } + @ViewBuilder var statusIcon: some View { if movie.isDownloaded { @@ -67,6 +77,7 @@ struct CalendarMovie: View { struct CalendarEpisode: View { var episode: Episode + var downloadProgress: Float? @EnvironmentObject var settings: AppSettings @@ -97,9 +108,7 @@ struct CalendarEpisode: View { Spacer() - statusIcon - .foregroundStyle(.secondary) - .imageScale(.small) + status } .foregroundStyle(.secondary) .font(.subheadline) @@ -154,6 +163,17 @@ struct CalendarEpisode: View { } } + @ViewBuilder + var status: some View { + if let downloadProgress { + CalendarDownloadProgress(progress: downloadProgress) + } else { + statusIcon + .foregroundStyle(.secondary) + .imageScale(.small) + } + } + @ViewBuilder var statusIcon: some View { if episode.isDownloaded { @@ -172,6 +192,27 @@ struct CalendarEpisode: View { } } +private struct CalendarDownloadProgress: View { + var progress: Float + + @EnvironmentObject var settings: AppSettings + + var body: some View { + ZStack { + Circle() + .stroke(.secondary.opacity(0.28), lineWidth: 2) + + Circle() + .trim(from: 0, to: CGFloat(progress)) + .stroke(settings.theme.tint, style: StrokeStyle(lineWidth: 2, lineCap: .round)) + .rotationEffect(.degrees(-90)) + } + .frame(width: 18, height: 18) + .accessibilityLabel("Downloading") + .accessibilityValue(progress.formatted(.percent.precision(.fractionLength(0)))) + } +} + enum CalendarMediaType: CaseIterable { case all case movies diff --git a/Ruddarr/Views/CalendarView.swift b/Ruddarr/Views/CalendarView.swift index 6b14e23a..7d2502c2 100644 --- a/Ruddarr/Views/CalendarView.swift +++ b/Ruddarr/Views/CalendarView.swift @@ -2,6 +2,7 @@ import SwiftUI struct CalendarView: View { @State var calendar = MediaCalendar() + @State var queue = Queue.shared @State private var scrollView: ScrollViewProxy? @State private var initializationError: API.Error? @@ -80,6 +81,8 @@ struct CalendarView: View { todayButton } .onAppear { + queue.instances = settings.instances + if Set(calendar.instances.map(\.id)) != Set(settings.instances.map(\.id)) { calendar.reset() calendar.instances = settings.instances @@ -94,6 +97,10 @@ struct CalendarView: View { .task { await load() } + .task { + queue.instances = settings.instances + await queue.fetchTasks() + } .alert( isPresented: $alertPresented, error: calendar.error @@ -241,13 +248,20 @@ struct CalendarView: View { VStack(spacing: 8) { if displayMovies, let movies = filteredMovies[timestamp] { ForEach(movies) { movie in - CalendarMovie(date: date, movie: movie) + CalendarMovie( + date: date, + movie: movie, + downloadProgress: downloadProgress(for: movie) + ) } } if displaySeries, let episodes = filteredEpisodes[timestamp] { ForEach(episodes) { episode in - CalendarEpisode(episode: episode) + CalendarEpisode( + episode: episode, + downloadProgress: downloadProgress(for: episode) + ) } } @@ -371,6 +385,44 @@ struct CalendarView: View { } } +extension CalendarView { + var activeQueueItems: [QueueItem] { + queue.items + .flatMap { $0.value } + .filter(\.hasDownloadProgress) + } + + func activeDownload(for movie: Movie) -> QueueItem? { + activeQueueItems.first { + $0.instanceId == movie.instanceId && + $0.movieId == movie.id + } + } + + func downloadProgress(for movie: Movie) -> Float? { + activeDownload(for: movie)?.progressFraction + } + + func activeDownload(for episode: Episode) -> QueueItem? { + activeQueueItems.first { + guard $0.instanceId == episode.instanceId else { + return false + } + + if let episodeId = $0.episodeId { + return episodeId == episode.id + } + + return $0.seriesId == episode.seriesId && + $0.seasonNumber == episode.seasonNumber + } + } + + func downloadProgress(for episode: Episode) -> Float? { + activeDownload(for: episode)?.progressFraction + } +} + // swiftlint:disable file_length #Preview { dependencies.router.selectedTab = .calendar