Skip to content
Open
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
104 changes: 104 additions & 0 deletions Ruddarr/Dependencies/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,115 @@ class Router {
var seriesPath: NavigationPath = .init()
var calendarPath: NavigationPath = .init()
var settingsPath: NavigationPath = .init()
var mediaSheetRoute: MediaSheetRoute?
var mediaSheetPath: NavigationPath = .init()

func reset() {
moviesPath = .init()
seriesPath = .init()
calendarPath = .init()
mediaSheetRoute = nil
mediaSheetPath = .init()
}

func presentMovie(_ movie: Movie) {
mediaSheetPath = .init()
mediaSheetRoute = .movie(movie)
}

func presentSeries(_ series: Series) {
mediaSheetPath = .init()
mediaSheetRoute = .series(series)
}

func presentEpisode(_ episode: Episode, grouped: Bool) {
let route = MediaSheetRoute.episode(episode, grouped: grouped)
mediaSheetPath = route.initialPath
mediaSheetRoute = route
}

func dismissMediaSheet() {
mediaSheetRoute = nil
mediaSheetPath = .init()
}
}

struct MediaSheetRoute: Identifiable {
enum Kind {
case movie
case series
}

let id: String
let kind: Kind
let movieId: Movie.ID?
let seriesId: Series.ID?
let seasonId: Season.ID?
let episodeId: Episode.ID?
let instanceId: Instance.ID?
let movie: Movie?
let series: Series?
let episode: Episode?

static func movie(_ movie: Movie) -> Self {
.init(
id: "movie-\(movie.id)-\(movie.instanceId?.uuidString ?? "unknown")",
kind: .movie,
movieId: movie.id,
seriesId: nil,
seasonId: nil,
episodeId: nil,
instanceId: movie.instanceId,
movie: movie,
series: nil,
episode: nil
)
}

static func series(_ series: Series) -> Self {
.init(
id: "series-\(series.id)-\(series.instanceId?.uuidString ?? "unknown")",
kind: .series,
movieId: nil,
seriesId: series.id,
seasonId: nil,
episodeId: nil,
instanceId: series.instanceId,
movie: nil,
series: series,
episode: nil
)
}

static func episode(_ episode: Episode, grouped: Bool) -> Self {
.init(
id: "series-\(episode.seriesId)-\(episode.id)-\(episode.instanceId?.uuidString ?? "unknown")",
kind: .series,
movieId: nil,
seriesId: episode.seriesId,
seasonId: episode.seasonNumber,
episodeId: grouped ? nil : episode.id,
instanceId: episode.instanceId,
movie: nil,
series: nil,
episode: episode
)
}

var initialPath: NavigationPath {
var path = NavigationPath()

guard kind == .series, let seriesId, let seasonId else {
return path
}

path.append(SeriesPath.season(seriesId, seasonId, nil))

if let episodeId {
path.append(SeriesPath.episode(seriesId, episodeId))
}

return path
}
}

Expand Down
74 changes: 74 additions & 0 deletions Ruddarr/Models/Calendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,80 @@ class MediaCalendar {
calendar.startOfDay(for: Date.now).timeIntervalSince1970
}

func applyMonitoringChange(_ change: CalendarMonitoringChange) {
switch change.kind {
case .movie:
updateMovie(change)
case .series:
updateSeries(change)
case .episode:
updateEpisode(change)
}
}

private func updateMovie(_ change: CalendarMonitoringChange) {
for day in Array(movies.keys) {
guard var items = movies[day] else { continue }
var changed = false

for index in items.indices {
guard items[index].id == change.mediaId else { continue }
guard matches(items[index].instanceId, change.instanceId) else { continue }

items[index].monitored = change.monitored
changed = true
}

if changed {
movies[day] = items
}
}
}

private func updateSeries(_ change: CalendarMonitoringChange) {
for day in Array(episodes.keys) {
guard var items = episodes[day] else { continue }
var changed = false

for index in items.indices {
guard items[index].seriesId == change.mediaId else { continue }
guard matches(items[index].instanceId, change.instanceId) else { continue }
guard var series = items[index].series else { continue }

series.monitored = change.monitored
items[index].series = series
changed = true
}

if changed {
episodes[day] = items
}
}
}

private func updateEpisode(_ change: CalendarMonitoringChange) {
for day in Array(episodes.keys) {
guard var items = episodes[day] else { continue }
var changed = false

for index in items.indices {
guard items[index].id == change.mediaId else { continue }
guard matches(items[index].instanceId, change.instanceId) else { continue }

items[index].monitored = change.monitored
changed = true
}

if changed {
episodes[day] = items
}
}
}

private func matches(_ mediaInstanceId: Instance.ID?, _ changeInstanceId: Instance.ID?) -> Bool {
changeInstanceId == nil || mediaInstanceId == nil || mediaInstanceId == changeInstanceId
}

func reset() {
instances = []
dates = []
Expand Down
8 changes: 8 additions & 0 deletions Ruddarr/Models/Movies/Movies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ class Movies {

case .update(let movie, let moveFiles):
_ = try await dependencies.api.updateMovie(movie, moveFiles, instance)
NotificationCenter.default.postCalendarMonitoringChange(
.init(
kind: .movie,
mediaId: movie.id,
instanceId: movie.instanceId ?? instance.id,
monitored: movie.monitored
)
)

case .delete(let movie, let addExclusion, let deleteFiles):
_ = try await dependencies.api.deleteMovie(movie, addExclusion, deleteFiles, instance)
Expand Down
2 changes: 1 addition & 1 deletion Ruddarr/Models/Series/Episode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct Episode: Identifiable, Codable, Equatable {
let sceneSeasonNumber: Int?
let unverifiedSceneNumbering: Bool

let series: Series?
var series: Series?

var calendarGroupCount: Int?

Expand Down
10 changes: 10 additions & 0 deletions Ruddarr/Models/Series/SeriesEpisodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ class SeriesEpisodes {

do {
_ = try await dependencies.api.monitorEpisode(episodes, monitored, instance)
for episode in episodes {
NotificationCenter.default.postCalendarMonitoringChange(
.init(
kind: .episode,
mediaId: episode,
instanceId: instance.id,
monitored: monitored
)
)
}
} catch is CancellationError {
// do nothing
} catch let apiError as API.Error {
Expand Down
8 changes: 8 additions & 0 deletions Ruddarr/Models/Series/SeriesModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ class SeriesModel {

case .update(let series, let moveFiles):
_ = try await dependencies.api.updateSeries(series, moveFiles, instance)
NotificationCenter.default.postCalendarMonitoringChange(
.init(
kind: .series,
mediaId: series.id,
instanceId: series.instanceId ?? instance.id,
monitored: series.monitored
)
)

case .delete(let series, let addExclusion, let deleteFiles):
_ = try await dependencies.api.deleteSeries(series, addExclusion, deleteFiles, instance)
Expand Down
59 changes: 59 additions & 0 deletions Ruddarr/Utilities/Events.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,69 @@ import Foundation

extension Notification.Name {
static let scrollToToday = Notification.Name("scrollToTodayInCalendar")
static let calendarMonitoringChanged = Notification.Name("calendarMonitoringChanged")
}

struct CalendarMonitoringChange {
enum Kind: String {
case movie
case series
case episode
}

let kind: Kind
let mediaId: Int
let instanceId: Instance.ID?
let monitored: Bool

init(kind: Kind, mediaId: Int, instanceId: Instance.ID?, monitored: Bool) {
self.kind = kind
self.mediaId = mediaId
self.instanceId = instanceId
self.monitored = monitored
}

init?(_ notification: Notification) {
guard let userInfo = notification.userInfo else { return nil }
guard let rawKind = userInfo[Key.kind] as? String else { return nil }
guard let kind = Kind(rawValue: rawKind) else { return nil }
guard let mediaId = userInfo[Key.mediaId] as? Int else { return nil }
guard let monitored = userInfo[Key.monitored] as? Bool else { return nil }

self.kind = kind
self.mediaId = mediaId
self.instanceId = userInfo[Key.instanceId] as? Instance.ID
self.monitored = monitored
}

var userInfo: [String: Any] {
var userInfo: [String: Any] = [
Key.kind: kind.rawValue,
Key.mediaId: mediaId,
Key.monitored: monitored,
]

if let instanceId {
userInfo[Key.instanceId] = instanceId
}

return userInfo
}

private enum Key {
static let kind = "kind"
static let mediaId = "mediaId"
static let instanceId = "instanceId"
static let monitored = "monitored"
}
}

extension NotificationCenter {
func post(name: Notification.Name) {
post(name: name, object: nil)
}

func postCalendarMonitoringChange(_ change: CalendarMonitoringChange) {
post(name: .calendarMonitoringChanged, object: nil, userInfo: change.userInfo)
}
}
21 changes: 2 additions & 19 deletions Ruddarr/Views/Calendar/CalendarMedia.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,7 @@ struct CalendarMovie: View {
.background(.card.opacity(shouldFade ? 0.6 : 1))
.clipShape(RoundedRectangle(cornerRadius: 14))
.onTapGesture {
let deeplink = String(
format: "ruddarr://movies/open/%d?instance=%@",
movie.id,
movie.instanceId!.uuidString
)

try? QuickActions.Deeplink(url: URL(string: deeplink)!)()
dependencies.router.presentMovie(movie)
}
}

Expand Down Expand Up @@ -113,18 +107,7 @@ struct CalendarEpisode: View {
.background(.card.opacity(shouldFade ? 0.6 : 1))
.clipShape(RoundedRectangle(cornerRadius: 14))
.onTapGesture {
var deeplink = String(
format: "ruddarr://series/open/%d?season=%d&instance=%@",
episode.seriesId,
episode.seasonNumber,
episode.instanceId!.uuidString
)

if !isGrouped {
deeplink.append("&episode=\(episode.episodeNumber)")
}

try? QuickActions.Deeplink(url: URL(string: deeplink)!)()
dependencies.router.presentEpisode(episode, grouped: isGrouped)
}
}

Expand Down
7 changes: 7 additions & 0 deletions Ruddarr/Views/CalendarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ struct CalendarView: View {
scrollTo(calendar.today())
}
}
.onReceive(NotificationCenter.default.publisher(for: .calendarMonitoringChanged)) { notification in
guard let change = CalendarMonitoringChange(notification) else { return }

withAnimation(.easeOut(duration: 0.2)) {
calendar.applyMonitoringChange(change)
}
}
.task {
await load()
}
Expand Down
Loading