From adefd1b2eb5fc29769f59fd64f67eba4dc16ee47 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Dec 2023 18:31:09 +0800 Subject: [PATCH] Better empty state handling for RSS --- .../App/Views/Home/HomeFeedViewIOS.swift | 60 ++++++- .../App/Views/Home/HomeFeedViewModel.swift | 2 + .../App/Views/Home/LibraryAddFeedView.swift | 156 ++++++++++++++++++ .../Sources/App/Views/LibraryTabView.swift | 15 +- .../App/Views/Profile/FiltersView.swift | 4 +- .../App/Views/TabBar/CustomTabBar.swift | 6 +- 6 files changed, 234 insertions(+), 9 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 101628983..65ddf6267 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -357,7 +357,9 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var isListScrolled: Bool @Binding var prefersListLayout: Bool @Binding var isEditMode: EditMode + @State private var showAddFeedView = false @State private var showHideFeatureAlert = false + @State private var showHideFollowingAlert = false @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel @@ -584,6 +586,46 @@ struct AnimatingCellHeight: AnimatableModifier { }.redacted(reason: .placeholder) } + var emptyState: some View { + if viewModel.folder == "following" { + return AnyView( + VStack(alignment: .center, spacing: 20) { + Text("You don't have any Feed items.") + .font(Font.system(size: 18, weight: .bold)) + + Text("Add an RSS/Atom feed") + .foregroundColor(Color.blue) + .onTapGesture { + showAddFeedView = true + } + + Text("Hide the Following tab") + .foregroundColor(Color.blue) + .onTapGesture { + showHideFollowingAlert = true + } + } + .frame(minHeight: 400) + .frame(maxWidth: .infinity) + .padding() + ) + } else { + return AnyView(Group { + Spacer() + + VStack(alignment: .center, spacing: 20) { + Text("No results found for this query") + .font(Font.system(size: 18, weight: .bold)) + } + .frame(minHeight: 400) + .frame(maxWidth: .infinity) + .padding() + + Spacer() + }) + } + } + var listItems: some View { ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10) @@ -663,6 +705,9 @@ struct AnimatingCellHeight: AnimatableModifier { if viewModel.showLoadingBar { redactedItems + } else if viewModel.fetcher.items.isEmpty { + emptyState + .listRowSeparator(.hidden, edges: .all) } else { listItems } @@ -688,13 +733,26 @@ struct AnimatingCellHeight: AnimatableModifier { shouldScrollToTop = true } } + .sheet(isPresented: $showAddFeedView) { + NavigationView { + LibraryAddFeedView() + } + } .alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.", isPresented: $showHideFeatureAlert) { Button("OK", role: .destructive) { viewModel.hideFeatureSection = true } Button(LocalText.cancelGeneric, role: .cancel) { self.showHideFeatureAlert = false } - }.introspectNavigationController { nav in + } + .alert("The Following tab will be hidden. You can add it back from the filter settings in your profile.", + isPresented: $showHideFollowingAlert) { + Button("OK", role: .destructive) { + viewModel.hideFollowingTab = true + } + Button(LocalText.cancelGeneric, role: .cancel) { self.showHideFollowingAlert = false } + } + .introspectNavigationController { nav in nav.navigationBar.shadowImage = UIImage() nav.navigationBar.setBackgroundImage(UIImage(), for: .default) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 96e4f2b81..6943fa16e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -44,6 +44,8 @@ import Views @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false @AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue + @AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false + @Published var appliedFilter: InternalFilter? { didSet { let filterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter") ?? folder diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift new file mode 100644 index 000000000..420fb4cf0 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryAddFeedView.swift @@ -0,0 +1,156 @@ + +import Introspect +import Models +import Services +import SwiftUI +import Views + +@MainActor final class LibraryAddFeedViewModel: NSObject, ObservableObject { + @Published var isLoading = false + @Published var errorMessage: String = "" + @Published var showErrorMessage: Bool = false + + @Environment(\.dismiss) private var dismiss + + func addLink(dataService: DataService, newLinkURL: String, dismiss: DismissAction) { + isLoading = true + Task { + if URL(string: newLinkURL) == nil { + error("Invalid link") + } else { + let result = try? await dataService.saveURL(id: UUID().uuidString, url: newLinkURL) + if result == nil { + error("Error adding link") + } else { + dismiss() + } + } + isLoading = false + } + } + + func error(_ msg: String) { + errorMessage = msg + showErrorMessage = true + isLoading = false + } +} + +struct LibraryAddFeedView: View { + @StateObject var viewModel = LibraryAddFeedViewModel() + + @State var newLinkURL: String = "" + @EnvironmentObject var dataService: DataService + @Environment(\.dismiss) private var dismiss + + enum FocusField: Hashable { + case addLinkEditor + } + + @FocusState private var focusedField: FocusField? + + var body: some View { + Group { + #if os(iOS) + Form { + innerBody + .navigationTitle("Add Link") + .navigationBarTitleDisplayMode(.inline) + } + #else + innerBody + #endif + } + #if os(macOS) + .padding() + #endif + .onAppear { + focusedField = .addLinkEditor + } + .navigationTitle("Add Link") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + dismissButton + } + ToolbarItem(placement: .navigationBarTrailing) { + viewModel.isLoading ? AnyView(ProgressView()) : AnyView(addButton) + } + } + #endif + .alert(viewModel.errorMessage, + isPresented: $viewModel.showErrorMessage) { + Button(LocalText.genericOk, role: .cancel) { viewModel.showErrorMessage = false } + } + } + + var cancelButton: some View { + Button( + action: { dismiss() }, + label: { Text(LocalText.cancelGeneric).foregroundColor(.appGrayTextContrast) } + ) + } + + var pasteboardString: String? { + #if os(iOS) + UIPasteboard.general.url?.absoluteString + #else + NSPasteboard.general.string(forType: NSPasteboard.PasteboardType.URL) + #endif + } + + var innerBody: some View { + Group { + TextField("Add Link", text: $newLinkURL) + #if os(iOS) + .keyboardType(.URL) + #endif + .autocorrectionDisabled(true) + .textFieldStyle(StandardTextFieldStyle()) + .focused($focusedField, equals: .addLinkEditor) + + Button(action: { + if let url = pasteboardString { + newLinkURL = url + } else { + viewModel.error("No URL on pasteboard") + } + }, label: { + Text("Get from pasteboard") + }) + + #if os(macOS) + Spacer() + HStack { + cancelButton + Spacer() + addButton + } + .frame(maxWidth: .infinity) + #endif + } + } + + var addButton: some View { + Button( + action: { + viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss) + }, + label: { Text("Add").bold() } + ) + .keyboardShortcut(.defaultAction) + .onSubmit { + viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss) + } + .disabled(viewModel.isLoading) + } + + var dismissButton: some View { + Button( + action: { dismiss() }, + label: { Text(LocalText.genericClose) } + ) + .disabled(viewModel.isLoading) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index fd36257a8..ecf6bd394 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -19,6 +19,7 @@ struct LibraryTabView: View { @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController + @AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false @AppStorage(UserDefaultKey.lastSelectedTabItem.rawValue) var selectedTab = "inbox" @State var showExpandedAudioPlayer = false @@ -53,11 +54,13 @@ struct LibraryTabView: View { var body: some View { VStack(spacing: 0) { TabView(selection: $selectedTab) { - NavigationView { - HomeFeedContainerView(viewModel: followingViewModel) - .navigationBarTitleDisplayMode(.inline) - .navigationViewStyle(.stack) - }.tag("following") + if !hideFollowingTab { + NavigationView { + HomeFeedContainerView(viewModel: followingViewModel) + .navigationBarTitleDisplayMode(.inline) + .navigationViewStyle(.stack) + }.tag("following") + } NavigationView { HomeFeedContainerView(viewModel: libraryViewModel) @@ -80,7 +83,7 @@ struct LibraryTabView: View { .frame(height: 1) .frame(maxWidth: .infinity) } - CustomTabBar(selectedTab: $selectedTab) + CustomTabBar(selectedTab: $selectedTab, hideFollowingTab: hideFollowingTab) .padding(0) } .fullScreenCover(isPresented: $showExpandedAudioPlayer) { diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/FiltersView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/FiltersView.swift index 39ebe6966..66214abf9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/FiltersView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/FiltersView.swift @@ -11,6 +11,7 @@ import Views @Published var networkError = false @Published var libraryFilters = [InternalFilter]() + @AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false func loadFilters(dataService: DataService) async { @@ -50,7 +51,8 @@ struct FiltersView: View { private var innerBody: some View { List { Section { - Toggle("Hide Feature Section", isOn: $viewModel.hideFeatureSection) + Toggle("Hide following tab", isOn: $viewModel.hideFollowingTab) + Toggle("Hide feature section", isOn: $viewModel.hideFeatureSection) } Section(header: Text("Saved Searches")) { diff --git a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift index 5d32c63b9..4c489aeb8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift @@ -3,9 +3,13 @@ import SwiftUI struct CustomTabBar: View { @Binding var selectedTab: String + let hideFollowingTab: Bool + var body: some View { HStack(spacing: 0) { - TabBarButton(key: "following", image: Image.tabFollowing, selectedTab: $selectedTab) + if !hideFollowingTab { + TabBarButton(key: "following", image: Image.tabFollowing, selectedTab: $selectedTab) + } TabBarButton(key: "inbox", image: Image.tabLibrary, selectedTab: $selectedTab) TabBarButton(key: "profile", image: Image.tabProfile, selectedTab: $selectedTab) }