Better empty state handling for RSS
This commit is contained in:
@ -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<String>
|
||||
@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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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")) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user