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
10 changes: 9 additions & 1 deletion Ruddarr/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,10 @@
}
}
},
"Coming Soon" : {
"comment" : "A title for a section of a movie search view that shows upcoming movies.",
"isCommentAutoGenerated" : true
},
"Connect a %@ instance under %@." : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -1342,6 +1346,10 @@
}
}
},
"Discover" : {
"comment" : "A label for the \"Discover\" tab in the movie search view.",
"isCommentAutoGenerated" : true
},
"Discovering new ways of making you wait." : {
"comment" : "Release search taunt",
"localizations" : {
Expand Down Expand Up @@ -5592,5 +5600,5 @@
}
}
},
"version" : "1.0"
"version" : "1.1"
}
59 changes: 49 additions & 10 deletions Ruddarr/Services/Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,30 @@ import SwiftUI
@Observable
class Discovery {
static let shared = Discovery()
static let url: String = "https://api.ruddarr.com"
static let url: String = "http://192.168.40.73:8787"

private var movieItems: DiscoveryItems?
private var seriesItems: DiscoveryItems?
private var movieUpcomingItems: DiscoveryItems?
private var seriesUpcomingItems: DiscoveryItems?

enum MediaType: String {
case movies
case series
}

enum BrowseMode: String {
case discover
case upcoming

var label: String {
switch self {
case .discover: "Popular"
case .upcoming: "Upcoming"
}
}
}

var movies: [DiscoveryItem] {
guard let items = movieItems?.popular else { return [] }
guard Platform.deviceType == .phone else { return items }
Expand All @@ -26,14 +40,38 @@ class Discovery {
return Array(items.prefix(24))
}

func fetch(_ type: MediaType) async {
var upcomingMovies: [DiscoveryItem] {
guard let items = movieUpcomingItems?.upcoming else { return [] }
guard Platform.deviceType == .phone else { return items }
return Array(items.prefix(24))
}

var upcomingSeries: [DiscoveryItem] {
guard let items = seriesUpcomingItems?.upcoming else { return [] }
guard Platform.deviceType == .phone else { return items }
return Array(items.prefix(24))
}

func fetch(_ type: MediaType, mode: BrowseMode = .discover) async {
switch type {
case .movies:
if isCurrentWindow(movieItems?.timestamp) { return }
movieItems = await load(.movies)
switch mode {
case .discover:
if isCurrentWindow(movieItems?.timestamp) { return }
movieItems = await load(.movies, mode: mode)
case .upcoming:
if isCurrentWindow(movieUpcomingItems?.timestamp) { return }
movieUpcomingItems = await load(.movies, mode: mode)
}
case .series:
if isCurrentWindow(seriesItems?.timestamp) { return }
seriesItems = await load(.series)
switch mode {
case .discover:
if isCurrentWindow(seriesItems?.timestamp) { return }
seriesItems = await load(.series, mode: mode)
case .upcoming:
if isCurrentWindow(seriesUpcomingItems?.timestamp) { return }
seriesUpcomingItems = await load(.series, mode: mode)
}
}
}

Expand All @@ -49,14 +87,14 @@ class Discovery {
return calendar.isDateInToday(date)
}

private func load(_ type: MediaType) async -> DiscoveryItems? {
private func load(_ type: MediaType, mode: BrowseMode = .discover) async -> DiscoveryItems? {
// return PreviewData.loadObject(name: "popular-\(type.rawValue)")

guard let baseURL = URL(string: Discovery.url) else { return nil }

do {
let url = baseURL
.appending(path: "/discover/\(type.rawValue)")
.appending(path: "/\(mode.rawValue)/\(type.rawValue)")
.appending(queryItems: [
URLQueryItem(name: "language", value: Locale.current.identifier(.bcp47))
])
Expand Down Expand Up @@ -85,7 +123,8 @@ class Discovery {

struct DiscoveryItems: Codable, Equatable {
let timestamp: String
let popular: [DiscoveryItem]
let popular: [DiscoveryItem]?
let upcoming: [DiscoveryItem]?
}

struct DiscoveryItem: Identifiable, Codable, Equatable {
Expand All @@ -98,7 +137,7 @@ struct DiscoveryItem: Identifiable, Codable, Equatable {
let vote_average: Double
let vote_count: Int
let score: Double
let poster_path: String
let poster_path: String?

enum ItemType: String, Codable {
case movie
Expand Down
78 changes: 54 additions & 24 deletions Ruddarr/Views/Movies/Search/MovieSearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,58 @@ import Combine

struct MovieSearchView: View {
@State var searchQuery: String
@State private var searchPresented: Bool = true
@State private var searchPresented: Bool = false
@State private var browseMode: Discovery.BrowseMode = .discover

@Environment(RadarrInstance.self) private var instance
@Environment(\.isSearching) private var isSearching

let searchTextPublisher = PassthroughSubject<String, Never>()

var body: some View {
@Bindable var discovery = Discovery.shared
@Bindable var movieLookup = instance.lookup

ScrollView {
if movieLookup.sortedItems.isEmpty && searchQuery.isEmpty {
MediaGrid(items: discovery.movies) { item in
DiscoveryGridPoster(item: item)
} header: {
Text("Popular This Week")
.padding(.top, 12)
VStack(spacing: 0) {
if searchQuery.isEmpty && !isSearching {
Picker(selection: $browseMode, label: EmptyView()) {
Text(Discovery.BrowseMode.discover.label).tag(Discovery.BrowseMode.discover)
Text(Discovery.BrowseMode.upcoming.label).tag(Discovery.BrowseMode.upcoming)
}
.viewBottomPadding()
.scenePadding(.horizontal)
.opacity(discovery.movies.isEmpty ? 0 : 1)
.animation(.easeIn, value: discovery.movies)
} else {
MediaGrid(items: movieLookup.sortedItems) { movie in
NavigationLink(value: movie.exists
? MoviesPath.movie(movie.id)
: MoviesPath.preview(try? JSONEncoder().encode(movie))
) {
MovieGridPoster(movie: movie)
}.buttonStyle(.plain)
.pickerStyle(.segmented)
.padding(.horizontal)
.padding(.vertical, 8)
}

ScrollView {
if movieLookup.sortedItems.isEmpty && searchQuery.isEmpty {
MediaGrid(items: browseItems) { item in
DiscoveryGridPoster(item: item)
} header: {
Text(browseHeader)
.padding(.top, 12)
}
.viewBottomPadding()
.scenePadding(.horizontal)
.opacity(browseItems.isEmpty ? 0 : 1)
.animation(.easeIn, value: browseItems)
} else {
MediaGrid(items: movieLookup.sortedItems) { movie in
NavigationLink(value: movie.exists
? MoviesPath.movie(movie.id)
: MoviesPath.preview(try? JSONEncoder().encode(movie))
) {
MovieGridPoster(movie: movie)
}.buttonStyle(.plain)
}
.padding(.top, 12)
.scenePadding(.horizontal)
.viewBottomPadding()
}
.padding(.top, 12)
.scenePadding(.horizontal)
.viewBottomPadding()
}
.id(browseMode)
.scrollDismissesKeyboard(.immediately)
}
.scrollDismissesKeyboard(.immediately)
.searchable(
text: $searchQuery,
isPresented: $searchPresented,
Expand All @@ -54,6 +69,7 @@ struct MovieSearchView: View {
}
.task {
await discovery.fetch(.movies)
await discovery.fetch(.movies, mode: .upcoming)
}
.onSubmit(of: .search) {
searchTextPublisher.send(searchQuery)
Expand Down Expand Up @@ -81,6 +97,20 @@ struct MovieSearchView: View {
}
}

var browseItems: [DiscoveryItem] {
switch browseMode {
case .discover: Discovery.shared.movies
case .upcoming: Discovery.shared.upcomingMovies
}
}

var browseHeader: String {
switch browseMode {
case .discover: "Popular This Week"
case .upcoming: "Coming Soon"
}
}

func performSearch() {
Task {
await instance.lookup.search(query: searchQuery)
Expand Down
70 changes: 50 additions & 20 deletions Ruddarr/Views/Series/Search/SeriesSearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,56 @@ import Combine

struct SeriesSearchView: View {
@State var searchQuery: String
@State private var searchPresented: Bool = true
@State private var searchPresented: Bool = false
@State private var browseMode: Discovery.BrowseMode = .discover

@Environment(SonarrInstance.self) private var instance
@Environment(\.isSearching) private var isSearching

let searchTextPublisher = PassthroughSubject<String, Never>()

var body: some View {
@Bindable var discovery = Discovery.shared
@Bindable var seriesLookup = instance.lookup

ScrollView {
if seriesLookup.sortedItems.isEmpty && searchQuery.isEmpty {
MediaGrid(items: discovery.series) { item in
DiscoveryGridPoster(item: item)
} header: {
Text("Popular This Week")
.padding(.top, 12)
VStack(spacing: 0) {
if searchQuery.isEmpty && !isSearching {
Picker(selection: $browseMode, label: EmptyView()) {
Text(Discovery.BrowseMode.discover.label).tag(Discovery.BrowseMode.discover)
Text(Discovery.BrowseMode.upcoming.label).tag(Discovery.BrowseMode.upcoming)
}
.viewBottomPadding()
.scenePadding(.horizontal)
.opacity(discovery.series.isEmpty ? 0 : 1)
.animation(.easeIn, value: discovery.series)
} else {
MediaGrid(items: seriesLookup.sortedItems) { series in
SeriesSearchItem(series: series)
.environment(instance)
.pickerStyle(.segmented)
.padding(.horizontal)
.padding(.vertical, 8)
}

ScrollView {
if seriesLookup.sortedItems.isEmpty && searchQuery.isEmpty {
MediaGrid(items: browseItems) { item in
DiscoveryGridPoster(item: item)
} header: {
Text(browseHeader)
.padding(.top, 12)
}
.viewBottomPadding()
.scenePadding(.horizontal)
.opacity(browseItems.isEmpty ? 0 : 1)
.animation(.easeIn, value: browseItems)
} else {
MediaGrid(items: seriesLookup.sortedItems) { series in
SeriesSearchItem(series: series)
.environment(instance)
}
.padding(.top, 12)
.scenePadding(.horizontal)
.viewBottomPadding()
}
.padding(.top, 12)
.scenePadding(.horizontal)
.viewBottomPadding()
}
.id(browseMode)
.scrollDismissesKeyboard(.immediately)
}
.navigationTitle("Search")
.safeNavigationBarTitleDisplayMode(.large)
.scrollDismissesKeyboard(.immediately)
.searchable(
text: $searchQuery,
isPresented: $searchPresented,
Expand All @@ -52,6 +67,7 @@ struct SeriesSearchView: View {
}
.task {
await discovery.fetch(.series)
await discovery.fetch(.series, mode: .upcoming)
}
.onSubmit(of: .search) {
searchTextPublisher.send(searchQuery)
Expand Down Expand Up @@ -79,6 +95,20 @@ struct SeriesSearchView: View {
}
}

var browseItems: [DiscoveryItem] {
switch browseMode {
case .discover: Discovery.shared.series
case .upcoming: Discovery.shared.upcomingSeries
}
}

var browseHeader: String {
switch browseMode {
case .discover: "Popular This Week"
case .upcoming: "Coming Soon"
}
}

func performSearch() {
Task { @MainActor in
await instance.lookup.search(query: searchQuery)
Expand Down
6 changes: 3 additions & 3 deletions Ruddarr/Views/Shared/MediaGrid+Content.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ struct DiscoveryGridPoster: View {
let items: DiscoveryItems = PreviewData.loadObject(name: "popular-movies")

VStack {
DiscoveryGridPoster(item: items.popular[3])
DiscoveryGridPoster(item: items.popular[12])
DiscoveryGridPoster(item: items.popular[13])
DiscoveryGridPoster(item: items.popular![3])
DiscoveryGridPoster(item: items.popular![12])
DiscoveryGridPoster(item: items.popular![13])
}
.environment(RadarrInstance())
.environment(SonarrInstance())
Expand Down
Loading