From 2daf4104cb0f975a7095c84fdb84b06af7bd6a3f Mon Sep 17 00:00:00 2001 From: Brecht Bakker Date: Sat, 2 Aug 2025 22:15:19 +0200 Subject: [PATCH] Add "Update Library" and "Search All Missing" options to the MovieView and SeriesView. Closes #141 --- Ruddarr/Dependencies/Toast.swift | 9 ++++ Ruddarr/Localizable.xcstrings | 50 +++++++++++++++++++ .../Models/Instances/InstanceCommand.swift | 12 +++++ Ruddarr/Views/Movies/MoviesView+Toolbar.swift | 40 +++++++++++++-- Ruddarr/Views/MoviesView.swift | 18 ++++++- Ruddarr/Views/Series/SeriesView+Toolbar.swift | 40 +++++++++++++-- Ruddarr/Views/SeriesView.swift | 18 ++++++- 7 files changed, 179 insertions(+), 8 deletions(-) diff --git a/Ruddarr/Dependencies/Toast.swift b/Ruddarr/Dependencies/Toast.swift index 99d0be82..51af1826 100644 --- a/Ruddarr/Dependencies/Toast.swift +++ b/Ruddarr/Dependencies/Toast.swift @@ -74,6 +74,9 @@ extension Toast { case seasonSearchQueued case episodeSearchQueued case monitoredSearchQueued + case missingMoviesSearchQueued + case missingEpisodesSearchQueued + case libraryRefreshQueued case movieDeleted case seriesDeleted case fileDeleted @@ -103,6 +106,12 @@ extension Toast { custom(text: String(localized: "Episode Search Queued"), icon: "checkmark.circle.fill") case .monitoredSearchQueued: custom(text: String(localized: "Monitored Search Queued"), icon: "checkmark.circle.fill") + case .missingMoviesSearchQueued: + custom(text: String(localized: "Missing Movies Search Queued"), icon: "checkmark.circle.fill") + case .missingEpisodesSearchQueued: + custom(text: String(localized: "Missing Episodes Search Queued"), icon: "checkmark.circle.fill") + case .libraryRefreshQueued: + custom(text: String(localized: "Library Refresh Queued"), icon: "checkmark.circle.fill") case .movieDeleted: custom(text: String(localized: "Movie Deleted"), icon: "checkmark.circle.fill") case .seriesDeleted: diff --git a/Ruddarr/Localizable.xcstrings b/Ruddarr/Localizable.xcstrings index 9deae379..bb19ded6 100644 --- a/Ruddarr/Localizable.xcstrings +++ b/Ruddarr/Localizable.xcstrings @@ -2350,6 +2350,16 @@ } } }, + "Library Refresh Queued" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Library Refresh Queued" + } + } + } + }, "Light" : { "comment" : "Light appearance/mode", "localizations" : { @@ -2547,6 +2557,26 @@ } } }, + "Missing Episodes Search Queued" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Missing Episodes Search Queued" + } + } + } + }, + "Missing Movies Search Queued" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Missing Movies Search Queued" + } + } + } + }, "Monitor" : { "comment" : "Label of picker of what to monitor (movie, collection, etc.)", "localizations" : { @@ -4307,6 +4337,16 @@ } } }, + "Search All Missing" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Search All Missing" + } + } + } + }, "Search for Movie" : { "localizations" : { "en" : { @@ -5199,6 +5239,16 @@ } } }, + "Update Library" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Library" + } + } + } + }, "URL" : { "localizations" : { "en" : { diff --git a/Ruddarr/Models/Instances/InstanceCommand.swift b/Ruddarr/Models/Instances/InstanceCommand.swift index 60a04de1..409111d0 100644 --- a/Ruddarr/Models/Instances/InstanceCommand.swift +++ b/Ruddarr/Models/Instances/InstanceCommand.swift @@ -1,9 +1,13 @@ import SwiftUI enum InstanceCommand { + case refreshMoviesLibrary + case missingMoviesSearch case refreshMovie(_ ids: [Movie.ID]) case search(_ ids: [Movie.ID]) + case refreshSeriesLibrary + case missingEpisodeSearch case refreshSeries(_ series: Series.ID) case seriesSearch(_ series: Series.ID) case seasonSearch(_ series: Series.ID, season: Season.ID) @@ -15,10 +19,18 @@ enum InstanceCommand { var payload: any Payload { switch self { + case .refreshMoviesLibrary: + RadarrPayload(name: "RefreshMovie") + case .missingMoviesSearch: + RadarrPayload(name: "MissingMoviesSearch") case .refreshMovie(let ids): RadarrPayload(name: "RefreshMovie", movieIds: ids) case .search(let ids): RadarrPayload(name: "MoviesSearch", movieIds: ids) + case .refreshSeriesLibrary: + SonarrPayload(name: "RefreshSeries") + case .missingEpisodeSearch: + SonarrPayload(name: "MissingEpisodeSearch") case .refreshSeries(let series): SonarrPayload(name: "RefreshSeries", seriesId: series) case .seriesSearch(let series): diff --git a/Ruddarr/Views/Movies/MoviesView+Toolbar.swift b/Ruddarr/Views/Movies/MoviesView+Toolbar.swift index e8f07cb1..70c38694 100644 --- a/Ruddarr/Views/Movies/MoviesView+Toolbar.swift +++ b/Ruddarr/Views/Movies/MoviesView+Toolbar.swift @@ -2,16 +2,50 @@ import SwiftUI extension MoviesView { @ToolbarContentBuilder - var toolbarSearchButton: some ToolbarContent { + var toolbarLibraryOptions: some ToolbarContent { if !instance.isVoid { ToolbarItem(placement: .primaryAction) { - NavigationLink(value: MoviesPath.search()) { - Image(systemName: "plus") + HStack { + toolbarContextMenu + toolbarSearchButton } } } } + var toolbarSearchButton: some View { + NavigationLink(value: MoviesPath.search()) { + Image(systemName: "plus") + } + } + + var toolbarContextMenu: some View { + Menu { + Section { + refreshLibraryButton + searchAllMissingButton + } + } label: { + ToolbarActionButton() + } + } + + var refreshLibraryButton: some View { + Button("Update Library", systemImage: "arrow.clockwise") { + Task { + await refreshLibrary() + } + } + } + + var searchAllMissingButton: some View { + Button("Search All Missing", systemImage: "magnifyingglass") { + Task { + await searchAllMissing() + } + } + } + @ToolbarContentBuilder var toolbarViewOptions: some ToolbarContent { ToolbarItem(placement: .navigation) { diff --git a/Ruddarr/Views/MoviesView.swift b/Ruddarr/Views/MoviesView.swift index 3adcf48b..a798d1c6 100644 --- a/Ruddarr/Views/MoviesView.swift +++ b/Ruddarr/Views/MoviesView.swift @@ -80,7 +80,7 @@ struct MoviesView: View { toolbarInstancePicker } - toolbarSearchButton + toolbarLibraryOptions } .scrollDismissesKeyboard(.immediately) .searchable( @@ -320,6 +320,22 @@ struct MoviesView: View { scheduleNextRun(time: DispatchTime.now(), id: id) } + + func refreshLibrary() async { + guard await instance.movies.command(.refreshMoviesLibrary) else { + return + } + + dependencies.toast.show(.libraryRefreshQueued) + } + + func searchAllMissing() async { + guard await instance.movies.command(.missingMoviesSearch) else { + return + } + + dependencies.toast.show(.missingMoviesSearchQueued) + } } #Preview("Offline") { diff --git a/Ruddarr/Views/Series/SeriesView+Toolbar.swift b/Ruddarr/Views/Series/SeriesView+Toolbar.swift index ebbc322f..a0f9dc60 100644 --- a/Ruddarr/Views/Series/SeriesView+Toolbar.swift +++ b/Ruddarr/Views/Series/SeriesView+Toolbar.swift @@ -2,16 +2,50 @@ import SwiftUI extension SeriesView { @ToolbarContentBuilder - var toolbarSearchButton: some ToolbarContent { + var toolbarLibraryOptions: some ToolbarContent { if !instance.isVoid { ToolbarItem(placement: .primaryAction) { - NavigationLink(value: SeriesPath.search()) { - Image(systemName: "plus") + HStack { + toolbarContextMenu + toolbarSearchButton } } } } + var toolbarSearchButton: some View { + NavigationLink(value: SeriesPath.search()) { + Image(systemName: "plus") + } + } + + var toolbarContextMenu: some View { + Menu { + Section { + refreshLibraryButton + searchAllMissingButton + } + } label: { + ToolbarActionButton() + } + } + + var refreshLibraryButton: some View { + Button("Update Library", systemImage: "arrow.clockwise") { + Task { + await refreshLibrary() + } + } + } + + var searchAllMissingButton: some View { + Button("Search All Missing", systemImage: "magnifyingglass") { + Task { + await searchAllMissing() + } + } + } + @ToolbarContentBuilder var toolbarViewOptions: some ToolbarContent { ToolbarItem(placement: .navigation) { diff --git a/Ruddarr/Views/SeriesView.swift b/Ruddarr/Views/SeriesView.swift index bc69c750..8f3dc703 100644 --- a/Ruddarr/Views/SeriesView.swift +++ b/Ruddarr/Views/SeriesView.swift @@ -81,7 +81,7 @@ struct SeriesView: View { toolbarInstancePicker } - toolbarSearchButton + toolbarLibraryOptions } .scrollDismissesKeyboard(.immediately) .searchable( @@ -344,6 +344,22 @@ struct SeriesView: View { scheduleNextRun(time: DispatchTime.now(), seriesId, seasonId, episodeId) } + + func refreshLibrary() async { + guard await instance.series.command(.refreshSeriesLibrary) else { + return + } + + dependencies.toast.show(.libraryRefreshQueued) + } + + func searchAllMissing() async { + guard await instance.series.command(.missingEpisodeSearch) else { + return + } + + dependencies.toast.show(.missingEpisodesSearchQueued) + } } #Preview("Offline") {