diff --git a/Shared/Configuration/Story (iOS).entitlements b/Shared/Configuration/Story (iOS).entitlements
index c706fec1..a1a930d2 100644
--- a/Shared/Configuration/Story (iOS).entitlements
+++ b/Shared/Configuration/Story (iOS).entitlements
@@ -20,7 +20,5 @@
com.apple.security.network.client
- com.apple.security.personal-information.photos-library
-
diff --git a/Shared/Extensions/Episode-Extension.swift b/Shared/Extensions/Episode-Extension.swift
index 00c0cb1a..e40f1870 100644
--- a/Shared/Extensions/Episode-Extension.swift
+++ b/Shared/Extensions/Episode-Extension.swift
@@ -55,7 +55,7 @@ extension Episode {
guard let date else { return false }
return Date() >= date
}
-
+
// MARK: URL
var itemImageMedium: URL? {
#if os(tvOS)
diff --git a/Shared/Extensions/ItemContent-Extensions.swift b/Shared/Extensions/ItemContent-Extensions.swift
index 0b9ee9cb..b34af3e4 100644
--- a/Shared/Extensions/ItemContent-Extensions.swift
+++ b/Shared/Extensions/ItemContent-Extensions.swift
@@ -144,14 +144,9 @@ extension ItemContent {
return ""
}
var itemRating: String? {
- if let voteAverage {
- if voteAverage <= 0.9 {
- return nil
- } else {
- return NSLocalizedString("\(voteAverage.rounded(.down))/10", comment: "")
- }
- }
- return nil
+ guard let voteAverage else { return nil }
+ let formattedString = String(format: "%.1f", voteAverage)
+ return "\(formattedString)/10"
}
// MARK: Double
diff --git a/Shared/Helpers/EpisodeHelper.swift b/Shared/Helpers/EpisodeHelper.swift
index 550abeaf..ca8fc7a3 100644
--- a/Shared/Helpers/EpisodeHelper.swift
+++ b/Shared/Helpers/EpisodeHelper.swift
@@ -39,7 +39,26 @@ class EpisodeHelper {
} catch {
if Task.isCancelled { return nil }
let message = "Episode:\(episode.seasonNumber as Any)\nSeason:\(episode.seasonNumber as Any)\nShow: \(show).\nError: \(error.localizedDescription)"
- CronicaTelemetry.shared.handleMessage(message, for: "EpisodeHelper.fetchNextEpisode")
+ guard let showContent = try? await network.fetchItem(id: show, type: .tvShow) else {
+ CronicaTelemetry.shared.handleMessage(message, for: "EpisodeHelper.fetchNextEpisode")
+ return nil
+ }
+ let lastEpisodeToAir = showContent.lastEpisodeToAir
+ guard let lastEpisodeToAir else {
+ CronicaTelemetry.shared.handleMessage(message, for: "EpisodeHelper.fetchNextEpisode")
+ return nil
+ }
+ if lastEpisodeToAir.itemEpisodeNumber == episode.itemEpisodeNumber {
+ let hasLastEpisodeReleased = lastEpisodeToAir.isItemReleased
+ if showContent.itemStatus == .ended && hasLastEpisodeReleased {
+ let contentId = "\(show)@\(MediaType.tvShow.toInt)"
+ guard let watchlistItem = PersistenceController.shared.fetch(for: contentId) else {
+ CronicaTelemetry.shared.handleMessage(message, for: "EpisodeHelper.fetchNextEpisode")
+ return nil
+ }
+ PersistenceController.shared.updateWatched(for: watchlistItem)
+ }
+ }
return nil
}
}
diff --git a/Shared/Manager/DatesManager.swift b/Shared/Manager/DatesManager.swift
index 8b2896aa..e0ed6433 100644
--- a/Shared/Manager/DatesManager.swift
+++ b/Shared/Manager/DatesManager.swift
@@ -13,21 +13,24 @@ class DatesManager {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(dateFormatter)
- return decoder
+ return decoder
}()
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
+ formatter.timeZone = .current
formatter.dateFormat = "y,MM,dd"
return formatter
}()
static let dateString: DateFormatter = {
let formatter = DateFormatter()
+ formatter.timeZone = .current
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
private static var releaseDateFormatter: ISO8601DateFormatter {
let formatter = ISO8601DateFormatter()
+ formatter.timeZone = .current
formatter.formatOptions = .withFullDate
return formatter
}
diff --git a/Shared/Store/SettingsStore.swift b/Shared/Store/SettingsStore.swift
index cc888de4..6c2b91bc 100644
--- a/Shared/Store/SettingsStore.swift
+++ b/Shared/Store/SettingsStore.swift
@@ -14,7 +14,11 @@ class SettingsStore: ObservableObject {
@AppStorage("displayDeveloperSettings") var displayDeveloperSettings = false
@AppStorage("gesture") var gesture: UpdateItemProperties = .favorite
@AppStorage("appThemeColor") var appTheme: AppThemeColors = .blue
+ #if os(iOS)
+ @AppStorage("watchlistStyle") var watchlistStyle: SectionDetailsPreferredStyle = UIDevice.isIPhone ? .list : .poster
+ #else
@AppStorage("watchlistStyle") var watchlistStyle: SectionDetailsPreferredStyle = .card
+ #endif
@AppStorage("disableTranslucentBackground") var disableTranslucent = false
@AppStorage("user_theme") var currentTheme: AppTheme = .system
@AppStorage("openInYouTube") var openInYouTube = false
@@ -36,7 +40,11 @@ class SettingsStore: ObservableObject {
#else
@AppStorage("itemContentListDisplayType") var listsDisplayType: ItemContentListPreferredDisplayType = .standard
#endif
+ #if os(iOS)
+ @AppStorage("exploreDisplayType") var sectionStyleType: SectionDetailsPreferredStyle = UIDevice.isIPhone ? .card : .poster
+ #else
@AppStorage("exploreDisplayType") var sectionStyleType: SectionDetailsPreferredStyle = .card
+ #endif
@AppStorage("preferCompactUI") var isCompactUI = false
@AppStorage("selectedWatchProviderEnabled") var isSelectedWatchProviderEnabled = false
@AppStorage("selectedWatchProviders") var selectedWatchProviders = ""
@@ -60,7 +68,8 @@ class SettingsStore: ObservableObject {
#endif
@AppStorage("shareLinkPreference") var shareLinkPreference: ShareLinkPreference = .tmdb
@AppStorage("upNextStyle") var upNextStyle: UpNextDetailsPreferredStyle = .card
- @AppStorage("showDateOnWatchlistRow") var showDateOnWatchlist = false
+ @AppStorage("showDateOnWatchlistRow") var showDateOnWatchlist = true
+ @AppStorage("disableSearchFilter") var disableSearchFilter = false
#if os(macOS)
@AppStorage("quitAppWhenClosingWindow") var quitApp = false
#endif
diff --git a/Shared/View/Components/OverviewBoxView.swift b/Shared/View/Components/OverviewBoxView.swift
index 0f598f0a..1f7277fe 100644
--- a/Shared/View/Components/OverviewBoxView.swift
+++ b/Shared/View/Components/OverviewBoxView.swift
@@ -70,7 +70,7 @@ struct OverviewBoxView: View {
#endif
}
} label: {
- Text("About")
+ Text(type == .person ? "Biography" : "About")
.unredacted()
}
.onTapGesture {
diff --git a/Shared/View/CustomList/DefaultWatchlist.swift b/Shared/View/CustomList/DefaultWatchlist.swift
index 1923c16e..12a5fa18 100644
--- a/Shared/View/CustomList/DefaultWatchlist.swift
+++ b/Shared/View/CustomList/DefaultWatchlist.swift
@@ -110,12 +110,12 @@ struct DefaultWatchlist: View {
}
.padding(.horizontal, 64)
}
- if smartFiltersItems.isEmpty {
- empty
- } else {
- WatchlistCardSection(items: smartFiltersItems,
- title: "Search results", showPopup: $showPopup, popupType: $popupType)
- }
+ if smartFiltersItems.isEmpty {
+ empty
+ } else {
+ WatchlistCardSection(items: smartFiltersItems,
+ title: "Search results", showPopup: $showPopup, popupType: $popupType)
+ }
}
#else
if items.isEmpty {
@@ -173,7 +173,7 @@ struct DefaultWatchlist: View {
}
}
}
- #endif
+#endif
}
.sheet(isPresented: $showFilter) {
NavigationStack {
@@ -203,8 +203,8 @@ struct DefaultWatchlist: View {
}
#elseif os(macOS)
HStack {
- sortButton
filterButton
+ sortButton
styleButton
}
#endif
@@ -259,6 +259,15 @@ struct DefaultWatchlist: View {
private var sortButton: some View {
#if os(tvOS)
EmptyView()
+#elseif os(macOS)
+ Picker(selection: $sortOrder) {
+ ForEach(WatchlistSortOrder.allCases) { item in
+ Text(item.localizableName).tag(item)
+ }
+ } label: {
+ Label("Sort Order", systemImage: "arrow.up.arrow.down.circle")
+ .labelStyle(.iconOnly)
+ }
#else
Menu {
Picker(selection: $sortOrder) {
@@ -277,6 +286,16 @@ struct DefaultWatchlist: View {
#if os(iOS) || os(macOS)
private var styleButton: some View {
+ #if os(macOS)
+ Picker(selection: $settings.watchlistStyle) {
+ ForEach(SectionDetailsPreferredStyle.allCases) { item in
+ Text(item.title).tag(item)
+ }
+ } label: {
+ Label("watchlistDisplayTypePicker", systemImage: "circle.grid.2x2")
+ .labelStyle(.iconOnly)
+ }
+ #else
Menu {
Picker(selection: $settings.watchlistStyle) {
ForEach(SectionDetailsPreferredStyle.allCases) { item in
@@ -289,6 +308,7 @@ struct DefaultWatchlist: View {
Label("watchlistDisplayTypePicker", systemImage: "circle.grid.2x2")
.labelStyle(.iconOnly)
}
+ #endif
}
#endif
diff --git a/Shared/View/ItemContent/Platform/ItemContentPadView.swift b/Shared/View/ItemContent/Platform/ItemContentPadView.swift
index 5d3f7664..b15e8d67 100644
--- a/Shared/View/ItemContent/Platform/ItemContentPadView.swift
+++ b/Shared/View/ItemContent/Platform/ItemContentPadView.swift
@@ -52,7 +52,6 @@ struct ItemContentPadView: View {
}
- AttributionView().padding([.top, .bottom])
}
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
diff --git a/Shared/View/ItemContent/Platform/ItemContentPhoneView.swift b/Shared/View/ItemContent/Platform/ItemContentPhoneView.swift
index 92b04d19..6531c666 100644
--- a/Shared/View/ItemContent/Platform/ItemContentPhoneView.swift
+++ b/Shared/View/ItemContent/Platform/ItemContentPhoneView.swift
@@ -57,7 +57,6 @@ struct ItemContentPhoneView: View {
dismiss: $showReleaseDateInfo)
}
- AttributionView().padding([.top, .bottom])
}
.navigationTitle(navigationTitle)
.navigationBarTitleDisplayMode(.inline)
diff --git a/Shared/View/Keywords/TrendingKeywordsListView.swift b/Shared/View/Keywords/TrendingKeywordsListView.swift
index ecac8c14..5bd391ac 100644
--- a/Shared/View/Keywords/TrendingKeywordsListView.swift
+++ b/Shared/View/Keywords/TrendingKeywordsListView.swift
@@ -18,41 +18,43 @@ struct TrendingKeywordsListView: View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(viewModel.trendingKeywords) { keyword in
- NavigationLink(value: keyword) {
- WebImage(url: keyword.image, options: [.continueInBackground, .highPriority])
- .resizable()
- .placeholder {
- ZStack {
- Rectangle().fill(.gray.gradient)
- }
- }
- .aspectRatio(contentMode: .fill)
- .overlay {
- ZStack {
- Rectangle().fill(.black.opacity(0.5))
- VStack {
- Spacer()
- HStack {
- Text(keyword.name)
- .foregroundColor(.white)
- .font(.subheadline)
- .fontWeight(.semibold)
- .multilineTextAlignment(.leading)
- .lineLimit(2)
- Spacer()
- }
- .padding(.horizontal)
- .padding(.bottom, 8)
- }
- }
- }
- .frame(width: 160, height: 100, alignment: .center)
- .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
- .shadow(radius: 2)
- .buttonStyle(.plain)
- }
- .disabled(viewModel.isLoadingTrendingKeywords)
- .frame(width: 160, height: 100, alignment: .center)
+ if keyword.image != nil {
+ NavigationLink(value: keyword) {
+ WebImage(url: keyword.image, options: [.continueInBackground, .highPriority])
+ .resizable()
+ .placeholder {
+ ZStack {
+ Rectangle().fill(.gray.gradient)
+ }
+ }
+ .aspectRatio(contentMode: .fill)
+ .overlay {
+ ZStack {
+ Rectangle().fill(.black.opacity(0.5))
+ VStack {
+ Spacer()
+ HStack {
+ Text(keyword.name)
+ .foregroundColor(.white)
+ .font(.subheadline)
+ .fontWeight(.semibold)
+ .multilineTextAlignment(.leading)
+ .lineLimit(2)
+ Spacer()
+ }
+ .padding(.horizontal)
+ .padding(.bottom, 8)
+ }
+ }
+ }
+ .frame(width: 160, height: 100, alignment: .center)
+ .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
+ .shadow(radius: 2)
+ .buttonStyle(.plain)
+ }
+ .disabled(viewModel.isLoadingTrendingKeywords)
+ .frame(width: 160, height: 100, alignment: .center)
+ }
}
}
.padding([.horizontal, .bottom])
diff --git a/Shared/View/Navigation/ExploreView.swift b/Shared/View/Navigation/ExploreView.swift
index 2fe64359..2323539f 100644
--- a/Shared/View/Navigation/ExploreView.swift
+++ b/Shared/View/Navigation/ExploreView.swift
@@ -15,7 +15,7 @@ struct ExploreView: View {
@State private var popupType: ActionPopupItems?
@StateObject private var viewModel = ExploreViewModel()
@StateObject private var settings = SettingsStore.shared
- @State private var sortBy: TMDBSortBy = .popularity
+ @State private var sortBy: TMDBSortBy = .popularity
var body: some View {
VStack {
if settings.sectionStyleType == .list {
@@ -133,14 +133,22 @@ struct ExploreView: View {
}
}
} label: {
+ #if os(macOS)
+ Text("genreDiscoverFilterTitle")
+ #else
EmptyView()
+ #endif
}
#if os(iOS)
.pickerStyle(.inline)
#endif
} header: {
+#if os(iOS)
Text("genreDiscoverFilterTitle")
+#else
+ EmptyView()
+#endif
}
Section {
Toggle("hideAddedItemsDiscoverFilter", isOn: $viewModel.hideAddedItems)
@@ -190,7 +198,7 @@ struct ExploreView: View {
.foregroundColor(showFilters ? .secondary : nil)
}
.keyboardShortcut("f", modifiers: .command)
- //sortButton
+ //sortButton
#if os(macOS)
styleOptions
#endif
@@ -224,9 +232,9 @@ struct ExploreView: View {
await load()
}
}
- .onChange(of: sortBy) { newSortByValue in
- viewModel.loadMoreItems(sortBy: newSortByValue, reload: true)
- }
+ .onChange(of: sortBy) { newSortByValue in
+ viewModel.loadMoreItems(sortBy: newSortByValue, reload: true)
+ }
}
private var listStyle: some View {
@@ -307,33 +315,44 @@ struct ExploreView: View {
#if os(iOS) || os(macOS)
private var styleOptions: some View {
+#if os(macOS)
+ Picker(selection: $settings.sectionStyleType) {
+ ForEach(SectionDetailsPreferredStyle.allCases) { item in
+ Text(item.title).tag(item)
+ }
+ } label: {
+ Label("Style Picker", systemImage: "circle.grid.2x2")
+ .labelStyle(.iconOnly)
+ }
+#else
Menu {
Picker(selection: $settings.sectionStyleType) {
ForEach(SectionDetailsPreferredStyle.allCases) { item in
Text(item.title).tag(item)
}
} label: {
- Label("sectionStyleTypePicker", systemImage: "circle.grid.2x2")
+ Label("Style Picker", systemImage: "circle.grid.2x2")
}
} label: {
- Label("sectionStyleTypePicker", systemImage: "circle.grid.2x2")
+ Label("Style Picker", systemImage: "circle.grid.2x2")
.labelStyle(.iconOnly)
}
+#endif
+ }
+
+ private var sortButton: some View {
+ Menu {
+ Picker(selection: $sortBy) {
+ ForEach(TMDBSortBy.allCases) { item in
+ Text(item.localizedString).tag(item)
+ }
+ } label: {
+ Label("Sort By", systemImage: "arrow.up.arrow.down.circle")
+ }
+ } label: {
+ Label("Sort By", systemImage: "arrow.up.arrow.down.circle")
+ }
}
-
- private var sortButton: some View {
- Menu {
- Picker(selection: $sortBy) {
- ForEach(TMDBSortBy.allCases) { item in
- Text(item.localizedString).tag(item)
- }
- } label: {
- Label("Sort By", systemImage: "arrow.up.arrow.down.circle")
- }
- } label: {
- Label("Sort By", systemImage: "arrow.up.arrow.down.circle")
- }
- }
#endif
private func load() async {
diff --git a/Shared/View/Navigation/SearchView.swift b/Shared/View/Navigation/SearchView.swift
index ac59efc9..7d86ce86 100644
--- a/Shared/View/Navigation/SearchView.swift
+++ b/Shared/View/Navigation/SearchView.swift
@@ -16,124 +16,121 @@ struct SearchView: View {
@State private var scope: SearchItemsScope = .noScope
@State private var currentlyQuery = String()
var body: some View {
- ZStack {
- List {
- switch scope {
- case .noScope:
- ForEach(viewModel.items) { item in
- SearchItemView(item: item,
- showPopup: $showPopup,
- popupType: $popupType)
- }
- loadableProgressRing
- case .movies:
- ForEach(viewModel.items.filter { $0.itemContentMedia == .movie }) { item in
- SearchItemView(item: item,
- showPopup: $showPopup,
- popupType: $popupType)
- }
- loadableProgressRing
- case .shows:
- ForEach(viewModel.items.filter { $0.itemContentMedia == .tvShow && $0.media != .person }) { item in
- SearchItemView(item: item,
- showPopup: $showPopup,
- popupType: $popupType)
- }
- loadableProgressRing
- case .people:
- ForEach(viewModel.items.filter { $0.media == .person }) { item in
- SearchItemView(item: item,
- showPopup: $showPopup,
- popupType: $popupType)
- }
- loadableProgressRing
- }
- }
- .navigationTitle("Search")
- .navigationBarTitleDisplayMode(.large)
- .navigationDestination(for: Person.self) { person in
- PersonDetailsView(title: person.name, id: person.id)
- }
- .navigationDestination(for: ItemContent.self) { item in
- ItemContentDetails(title: item.itemTitle, id: item.id, type: item.itemContentMedia)
- }
- .navigationDestination(for: ProductionCompany.self) { item in
- CompanyDetails(company: item)
- }
- .navigationDestination(for: [ProductionCompany].self) { item in
- CompaniesListView(companies: item)
- }
- .navigationDestination(for: SearchItemContent.self) { item in
- if item.media == .person {
- PersonDetailsView(title: item.itemTitle, id: item.id)
- } else {
- ItemContentDetails(title: item.itemTitle, id: item.id, type: item.media)
- }
- }
- .navigationDestination(for: [String:[ItemContent]].self) { item in
- let keys = item.map { (key, _) in key }.first
- let value = item.map { (_, value) in value }.first
- if let keys, let value {
- ItemContentSectionDetails(title: keys, items: value)
- }
- }
- .navigationDestination(for: [Person].self) { items in
- DetailedPeopleList(items: items)
- }
- .navigationDestination(for: CombinedKeywords.self) { keyword in
- KeywordSectionView(keyword: keyword)
- }
- .searchable(text: $viewModel.query,
- placement: .navigationBarDrawer(displayMode: .always),
- prompt: Text("Movies, Shows, People"))
- .searchScopes($scope) {
- ForEach(SearchItemsScope.allCases) { scope in
- Text(scope.localizableTitle).tag(scope)
- }
- }
- .disableAutocorrection(true)
- .task(id: viewModel.query) {
- if currentlyQuery != viewModel.query {
- currentlyQuery = viewModel.query
- await viewModel.search(viewModel.query)
- }
- }
- .overlay(searchResults)
- .actionPopup(isShowing: $showPopup, for: popupType)
- }
- }
-
- @ViewBuilder
- private var searchResults: some View {
- switch viewModel.stage {
- case .none:
- ScrollView {
- VStack {
-// TrendingPeopleListView()
-// .environmentObject(viewModel)
- TrendingKeywordsListView()
- .environmentObject(viewModel)
- Spacer()
- }
- .task {
- //await viewModel.loadTrendingPeople()
- await viewModel.loadTrendingKeywords()
- }
- }
- case .searching:
- ProgressView("Searching")
- .foregroundColor(.secondary)
- .padding()
- case .empty:
- Label("No Results", systemImage: "minus.magnifyingglass")
- .font(.title)
- .foregroundColor(.secondary)
- case .failure:
- VStack {
- Label("Search failed, try again later.", systemImage: "text.magnifyingglass")
- }
- case .success: EmptyView()
- }
+ VStack {
+ switch viewModel.stage {
+ case .none:
+ ScrollView {
+ VStack {
+ TrendingKeywordsListView()
+ .environmentObject(viewModel)
+ Spacer()
+ }
+ .task {
+ await viewModel.loadTrendingKeywords()
+ }
+ }
+ case .searching:
+ ProgressView("Searching")
+ .foregroundColor(.secondary)
+ .padding()
+ case .empty:
+ Label("No Results", systemImage: "minus.magnifyingglass")
+ .font(.title)
+ .foregroundColor(.secondary)
+ .padding()
+ case .failure:
+ VStack {
+ Label("Search failed, try again later.", systemImage: "text.magnifyingglass")
+ .foregroundColor(.secondary)
+ .padding()
+ }
+ case .success:
+ List {
+ switch scope {
+ case .noScope:
+ ForEach(viewModel.items) { item in
+ SearchItemView(item: item,
+ showPopup: $showPopup,
+ popupType: $popupType)
+ }
+ if !viewModel.items.isEmpty {
+ loadableProgressRing
+ }
+ case .movies:
+ ForEach(viewModel.items.filter { $0.itemContentMedia == .movie }) { item in
+ SearchItemView(item: item,
+ showPopup: $showPopup,
+ popupType: $popupType)
+ }
+ loadableProgressRing
+ case .shows:
+ ForEach(viewModel.items.filter { $0.itemContentMedia == .tvShow && $0.media != .person }) { item in
+ SearchItemView(item: item,
+ showPopup: $showPopup,
+ popupType: $popupType)
+ }
+ loadableProgressRing
+ case .people:
+ ForEach(viewModel.items.filter { $0.media == .person }) { item in
+ SearchItemView(item: item,
+ showPopup: $showPopup,
+ popupType: $popupType)
+ }
+ loadableProgressRing
+ }
+ }
+ }
+ }
+ .navigationTitle("Search")
+ .navigationBarTitleDisplayMode(.large)
+ .navigationDestination(for: Person.self) { person in
+ PersonDetailsView(title: person.name, id: person.id)
+ }
+ .navigationDestination(for: ItemContent.self) { item in
+ ItemContentDetails(title: item.itemTitle, id: item.id, type: item.itemContentMedia)
+ }
+ .navigationDestination(for: ProductionCompany.self) { item in
+ CompanyDetails(company: item)
+ }
+ .navigationDestination(for: [ProductionCompany].self) { item in
+ CompaniesListView(companies: item)
+ }
+ .navigationDestination(for: SearchItemContent.self) { item in
+ if item.media == .person {
+ PersonDetailsView(title: item.itemTitle, id: item.id)
+ } else {
+ ItemContentDetails(title: item.itemTitle, id: item.id, type: item.media)
+ }
+ }
+ .navigationDestination(for: [String:[ItemContent]].self) { item in
+ let keys = item.map { (key, _) in key }.first
+ let value = item.map { (_, value) in value }.first
+ if let keys, let value {
+ ItemContentSectionDetails(title: keys, items: value)
+ }
+ }
+ .navigationDestination(for: [Person].self) { items in
+ DetailedPeopleList(items: items)
+ }
+ .navigationDestination(for: CombinedKeywords.self) { keyword in
+ KeywordSectionView(keyword: keyword)
+ }
+ .searchable(text: $viewModel.query,
+ placement: .navigationBarDrawer(displayMode: .always),
+ prompt: Text("Movies, Shows, People"))
+ .searchScopes($scope) {
+ ForEach(SearchItemsScope.allCases) { scope in
+ Text(scope.localizableTitle).tag(scope)
+ }
+ }
+ .disableAutocorrection(true)
+ .task(id: viewModel.query) {
+ if currentlyQuery != viewModel.query {
+ currentlyQuery = viewModel.query
+ await viewModel.search(viewModel.query)
+ }
+ }
+ .actionPopup(isShowing: $showPopup, for: popupType)
}
@ViewBuilder
diff --git a/Shared/View/Person/PersonDetailsView.swift b/Shared/View/Person/PersonDetailsView.swift
index 852c6cd7..54e6a372 100644
--- a/Shared/View/Person/PersonDetailsView.swift
+++ b/Shared/View/Person/PersonDetailsView.swift
@@ -11,7 +11,7 @@ import SDWebImageSwiftUI
struct PersonDetailsView: View {
let name: String
@State private var isLoading = true
- @State private var isFavorite = false
+ @State private var isFavorite = false
@StateObject private var viewModel: PersonDetailsViewModel
@State private var scope: WatchlistSearchScope = .noScope
@State private var showImageFullscreen = false
@@ -30,7 +30,7 @@ struct PersonDetailsView: View {
.padding([.bottom, .horizontal])
#if !os(tvOS)
if let overview = viewModel.person?.biography {
- OverviewBoxView(overview: overview, title: "", showAsPopover: true)
+ OverviewBoxView(overview: overview, title: "Biography", type: .person, showAsPopover: true)
.frame(width: 500)
.padding([.bottom, .trailing])
}
@@ -44,7 +44,7 @@ struct PersonDetailsView: View {
.padding()
#if !os(tvOS)
if let overview = viewModel.person?.biography {
- OverviewBoxView(overview: overview, title: "")
+ OverviewBoxView(overview: overview, title: "", type: .person)
.padding([.horizontal, .bottom])
}
#endif
@@ -54,10 +54,7 @@ struct PersonDetailsView: View {
FilmographyListView(filmography: viewModel.credits,
showPopup: $showPopup,
popupType: $popupType)
-
- AttributionView()
- .padding([.top, .bottom])
- .unredacted()
+ .padding(.bottom)
}
}
.actionPopup(isShowing: $showPopup, for: popupType)
@@ -78,11 +75,11 @@ struct PersonDetailsView: View {
.toolbar {
#if os(iOS)
ToolbarItem {
- shareButton
+ shareButton
}
#elseif os(macOS)
ToolbarItem(placement: .primaryAction) {
- shareButton
+ shareButton
}
#endif
}
@@ -148,53 +145,53 @@ struct PersonDetailsView: View {
}
}
}
-
- #if !os(tvOS)
- @ViewBuilder
- private var shareButton: some View {
- if let url = viewModel.person?.itemURL {
- ShareLink(item: url, message: Text(name))
- .disabled(!viewModel.isLoaded)
- }
- }
- #endif
-
- private var favoriteButton: some View {
- Button {
-
- } label: {
- Label("Favorite", systemImage: isFavorite ? "star.circle.fill" : "star.circle")
- .labelStyle(.iconOnly)
- }
-
- }
+
+#if !os(tvOS)
+ @ViewBuilder
+ private var shareButton: some View {
+ if let url = viewModel.person?.itemURL {
+ ShareLink(item: url, message: Text(name))
+ .disabled(!viewModel.isLoaded)
+ }
+ }
+#endif
+
+ private var favoriteButton: some View {
+ Button {
+
+ } label: {
+ Label("Favorite", systemImage: isFavorite ? "star.circle.fill" : "star.circle")
+ .labelStyle(.iconOnly)
+ }
+
+ }
#if os(iOS)
@ViewBuilder
private var search: some View {
if !viewModel.query.isEmpty {
- List {
- switch scope {
- case .noScope:
- ForEach(viewModel.credits.filter { ($0.itemTitle.localizedStandardContains(viewModel.query)) as Bool }) { item in
- ItemContentSearchRowView(item: item,
- showPopup: $showPopup,
- popupType: $popupType)
- }
- case .movies:
- ForEach(viewModel.credits.filter { ($0.itemTitle.localizedStandardContains(viewModel.query)) as Bool && $0.itemContentMedia == .movie }) { item in
- ItemContentSearchRowView(item: item,
- showPopup: $showPopup,
- popupType: $popupType)
- }
- case .shows:
- ForEach(viewModel.credits.filter { ($0.itemTitle.localizedStandardContains(viewModel.query)) as Bool && $0.itemContentMedia == .tvShow }) { item in
- ItemContentSearchRowView(item: item,
- showPopup: $showPopup,
- popupType: $popupType)
- }
- }
- }
+ List {
+ switch scope {
+ case .noScope:
+ ForEach(viewModel.credits.filter { ($0.itemTitle.localizedStandardContains(viewModel.query)) as Bool }) { item in
+ ItemContentSearchRowView(item: item,
+ showPopup: $showPopup,
+ popupType: $popupType)
+ }
+ case .movies:
+ ForEach(viewModel.credits.filter { ($0.itemTitle.localizedStandardContains(viewModel.query)) as Bool && $0.itemContentMedia == .movie }) { item in
+ ItemContentSearchRowView(item: item,
+ showPopup: $showPopup,
+ popupType: $popupType)
+ }
+ case .shows:
+ ForEach(viewModel.credits.filter { ($0.itemTitle.localizedStandardContains(viewModel.query)) as Bool && $0.itemContentMedia == .tvShow }) { item in
+ ItemContentSearchRowView(item: item,
+ showPopup: $showPopup,
+ popupType: $popupType)
+ }
+ }
+ }
}
}
#endif
@@ -209,7 +206,7 @@ struct PersonDetailsView: View {
.resizable()
.foregroundColor(.white.opacity(0.8))
.frame(width: 50, height: 50, alignment: .center)
- .unredacted()
+ .unredacted()
}
.clipShape(Circle())
}
diff --git a/Shared/View/Season/EpisodeFrameView.swift b/Shared/View/Season/EpisodeFrameView.swift
index e66ccb95..f09bb883 100644
--- a/Shared/View/Season/EpisodeFrameView.swift
+++ b/Shared/View/Season/EpisodeFrameView.swift
@@ -21,9 +21,9 @@ struct EpisodeFrameView: View {
@State private var showDetails = false
private let network = NetworkService.shared
@Binding var checkedIfWatched: Bool
- #if os(tvOS)
+#if os(tvOS)
@FocusState var isFocused
- #endif
+#endif
var body: some View {
#if os(tvOS)
VStack {
diff --git a/Shared/View/Settings/BehaviorSetting.swift b/Shared/View/Settings/BehaviorSetting.swift
index 381db98b..940528d6 100644
--- a/Shared/View/Settings/BehaviorSetting.swift
+++ b/Shared/View/Settings/BehaviorSetting.swift
@@ -49,6 +49,13 @@ struct BehaviorSetting: View {
Spacer()
}
}
+
+ Section {
+ Toggle(isOn: $store.disableSearchFilter) {
+ Text("Disable Search Filter")
+ Text("Search filter improve the search results, but has the downside of taking longer to load.")
+ }
+ }
#if os(macOS)
// Section {
diff --git a/Shared/View/WatchlistItem/Components/WatchlistItemRowView.swift b/Shared/View/WatchlistItem/Components/WatchlistItemRowView.swift
index 7b4967d7..9090e7d9 100644
--- a/Shared/View/WatchlistItem/Components/WatchlistItemRowView.swift
+++ b/Shared/View/WatchlistItem/Components/WatchlistItemRowView.swift
@@ -24,11 +24,6 @@ struct WatchlistItemRowView: View {
HStack {
image
.applyHoverEffect()
-#if !os(watchOS)
- .shadow(radius: 2.5)
-#else
- .padding(.vertical)
-#endif
VStack(alignment: .leading) {
HStack {
Text(content.itemTitle)
@@ -91,21 +86,6 @@ struct WatchlistItemRowView: View {
#endif
}
#endif
- .sheet(isPresented: $showCustomListView) {
- NavigationStack {
- ItemContentCustomListSelector(contentID: content.itemContentID,
- showView: $showCustomListView,
- title: content.itemTitle,
- image: content.backCompatibleCardImage)
- }
- .presentationDetents([.large])
-#if os(macOS)
- .frame(width: 500, height: 600, alignment: .center)
-#else
- .appTheme()
- .appTint()
-#endif
- }
.accessibilityElement(children: .combine)
#if !os(watchOS)
.watchlistContextMenu(item: content,
@@ -117,6 +97,21 @@ struct WatchlistItemRowView: View {
showCustomList: $showCustomListView,
popupType: $popupType,
showPopup: $showPopup)
+ .sheet(isPresented: $showCustomListView) {
+ NavigationStack {
+ ItemContentCustomListSelector(contentID: content.itemContentID,
+ showView: $showCustomListView,
+ title: content.itemTitle,
+ image: content.backCompatibleCardImage)
+ }
+ .presentationDetents([.large])
+#if os(macOS)
+ .frame(width: 500, height: 600, alignment: .center)
+#else
+ .appTheme()
+ .appTint()
+#endif
+ }
#endif
}
}
diff --git a/Shared/ViewModel/ItemContentViewModel.swift b/Shared/ViewModel/ItemContentViewModel.swift
index 3c5e9656..552f749e 100644
--- a/Shared/ViewModel/ItemContentViewModel.swift
+++ b/Shared/ViewModel/ItemContentViewModel.swift
@@ -66,7 +66,7 @@ class ItemContentViewModel: ObservableObject {
}
}
if trailers.isEmpty {
- trailers.append(contentsOf: content.itemTrailers)
+ trailers.append(contentsOf: content.itemTrailers.prefix(2))
}
if credits.isEmpty {
let cast = content.credits?.cast ?? []
diff --git a/Shared/ViewModel/SearchViewModel.swift b/Shared/ViewModel/SearchViewModel.swift
index 4f4d8b82..d92a81bf 100644
--- a/Shared/ViewModel/SearchViewModel.swift
+++ b/Shared/ViewModel/SearchViewModel.swift
@@ -37,7 +37,7 @@ import SwiftUI
]
@Published var trendingKeywords = [CombinedKeywords]()
@Published var isLoadingTrendingKeywords = true
- var stage: SearchStage = .none
+ @Published var stage: SearchStage = .none
func search(_ query: String) async {
if Task.isCancelled { return }
@@ -45,16 +45,16 @@ import SwiftUI
startPagination = false
withAnimation {
items.removeAll()
+ stage = .none
}
- stage = .none
return
}
- stage = .searching
+ withAnimation { stage = .searching }
let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedQuery.isEmpty else { return }
do {
if Task.isCancelled {
- stage = .none
+ withAnimation { stage = .none }
return
}
try await Task.sleep(nanoseconds: 300_000_000)
@@ -66,17 +66,21 @@ import SwiftUI
endPagination = false
let result = try await service.search(query: trimmedQuery, page: "1")
page += 1
- let filtered = await filter(for: result)
- items.append(contentsOf: filtered.sorted(by: { $0.itemPopularity > $1.itemPopularity }))
+ if SettingsStore.shared.disableSearchFilter {
+ items.append(contentsOf: result.sorted(by: { $0.itemPopularity > $1.itemPopularity }))
+ } else {
+ let filtered = await filter(for: result)
+ items.append(contentsOf: filtered.sorted(by: { $0.itemPopularity > $1.itemPopularity }))
+ }
if self.items.isEmpty {
- stage = .empty
+ withAnimation { stage = .empty }
return
}
- stage = .success
+ withAnimation { stage = .success }
startPagination = true
} catch {
if Task.isCancelled { return }
- stage = .failure
+ withAnimation { stage = .failure }
CronicaTelemetry.shared.handleMessage(error.localizedDescription,
for: "SearchViewModel.search()")
}
@@ -117,7 +121,7 @@ import SwiftUI
func loadTrendingKeywords() async {
if trendingKeywords.isEmpty {
for item in keywords.sorted(by: { $0.name < $1.name}) {
- let itemFromKeyword = try? await service.fetchKeyword(type: .tvShow,
+ let itemFromKeyword = try? await service.fetchKeyword(type: .movie,
page: 1,
keywords: item.id,
sortBy: TMDBSortBy.popularity.rawValue)
diff --git a/Story.xcodeproj/project.pbxproj b/Story.xcodeproj/project.pbxproj
index 77be7610..e19d9d73 100644
--- a/Story.xcodeproj/project.pbxproj
+++ b/Story.xcodeproj/project.pbxproj
@@ -2199,7 +2199,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "AppleWatch/Configuration/CronicaWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 230;
+ CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_ASSET_PATHS = "\"AppleWatch/Preview Content\"";
DEVELOPMENT_TEAM = 2NF329R2JB;
ENABLE_PREVIEWS = YES;
@@ -2212,7 +2212,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.5.8;
+ MARKETING_VERSION = 2.5.9;
PRODUCT_BUNDLE_IDENTIFIER = dev.alexandremadeira.Story.watchkitapp;
PRODUCT_NAME = Cronica;
SDKROOT = watchos;
@@ -2232,7 +2232,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "AppleWatch/Configuration/CronicaWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 230;
+ CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_ASSET_PATHS = "\"AppleWatch/Preview Content\"";
DEVELOPMENT_TEAM = 2NF329R2JB;
ENABLE_PREVIEWS = YES;
@@ -2245,7 +2245,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.5.8;
+ MARKETING_VERSION = 2.5.9;
PRODUCT_BUNDLE_IDENTIFIER = dev.alexandremadeira.Story.watchkitapp;
PRODUCT_NAME = Cronica;
SDKROOT = watchos;
@@ -2389,8 +2389,9 @@
CLANG_USE_OPTIMIZATION_PROFILE = YES;
CODE_SIGN_ENTITLEMENTS = "Shared/Configuration/Story (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 230;
+ CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 2NF329R2JB;
+ "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Shared/Configuration/Cronica--Info.plist";
@@ -2406,7 +2407,7 @@
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.3;
- MARKETING_VERSION = 2.5.8;
+ MARKETING_VERSION = 2.5.9;
PRODUCT_BUNDLE_IDENTIFIER = dev.alexandremadeira.Story;
PRODUCT_NAME = Cronica;
SDKROOT = iphoneos;
@@ -2434,8 +2435,9 @@
CLANG_USE_OPTIMIZATION_PROFILE = YES;
CODE_SIGN_ENTITLEMENTS = "Shared/Configuration/Story (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 230;
+ CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 2NF329R2JB;
+ "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Shared/Configuration/Cronica--Info.plist";
@@ -2451,7 +2453,7 @@
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.3;
- MARKETING_VERSION = 2.5.8;
+ MARKETING_VERSION = 2.5.9;
PRODUCT_BUNDLE_IDENTIFIER = dev.alexandremadeira.Story;
PRODUCT_NAME = Cronica;
SDKROOT = iphoneos;
@@ -2522,7 +2524,7 @@
CODE_SIGN_ENTITLEMENTS = CronicaWidget/Configuration/CronicaWidgetExtension.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 230;
+ CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 2NF329R2JB;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = CronicaWidget/Configuration/Info.plist;
@@ -2534,7 +2536,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.5.8;
+ MARKETING_VERSION = 2.5.9;
PRODUCT_BUNDLE_IDENTIFIER = dev.alexandremadeira.Story.CronicaWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2557,7 +2559,7 @@
CODE_SIGN_ENTITLEMENTS = CronicaWidget/Configuration/CronicaWidgetExtension.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 230;
+ CURRENT_PROJECT_VERSION = 235;
DEVELOPMENT_TEAM = 2NF329R2JB;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = CronicaWidget/Configuration/Info.plist;
@@ -2569,7 +2571,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.5.8;
+ MARKETING_VERSION = 2.5.9;
PRODUCT_BUNDLE_IDENTIFIER = dev.alexandremadeira.Story.CronicaWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;