From 568edcd8ff55225b7333c6c4439aec44b7f4427b Mon Sep 17 00:00:00 2001 From: Alexandre Madeira Date: Tue, 29 Aug 2023 17:58:34 -0300 Subject: [PATCH] Improved time zone support. --- Shared/Configuration/Story (iOS).entitlements | 2 - Shared/Extensions/Episode-Extension.swift | 2 +- .../Extensions/ItemContent-Extensions.swift | 11 +- Shared/Helpers/EpisodeHelper.swift | 21 +- Shared/Manager/DatesManager.swift | 5 +- Shared/Store/SettingsStore.swift | 11 +- Shared/View/Components/OverviewBoxView.swift | 2 +- Shared/View/CustomList/DefaultWatchlist.swift | 36 ++- .../Platform/ItemContentPadView.swift | 1 - .../Platform/ItemContentPhoneView.swift | 1 - .../Keywords/TrendingKeywordsListView.swift | 72 +++--- Shared/View/Navigation/ExploreView.swift | 61 +++-- Shared/View/Navigation/SearchView.swift | 233 +++++++++--------- Shared/View/Person/PersonDetailsView.swift | 101 ++++---- Shared/View/Season/EpisodeFrameView.swift | 4 +- Shared/View/Settings/BehaviorSetting.swift | 7 + .../Components/WatchlistItemRowView.swift | 35 ++- Shared/ViewModel/ItemContentViewModel.swift | 2 +- Shared/ViewModel/SearchViewModel.swift | 24 +- Story.xcodeproj/project.pbxproj | 26 +- 20 files changed, 361 insertions(+), 296 deletions(-) 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;