mirror of
https://github.com/yattee/yattee.git
synced 2026-07-19 22:02:10 +00:00
Show channel search bar and notification icon during loading
The .searchable modifier lived inside iOSChannelContent, which body only renders once the channel network fetch completes, so the search field appeared only after videos loaded. Hoist the search modifier chain into a channelSearchable wrapper around the load-state Group in body so it shows immediately in every state (iOS nav-bar drawer and macOS toolbar). channelMenuIcon gated the notification lookup behind the network-loaded channel object, forcing the icon to person.fill while loading and swapping to bell.fill afterward. Use channel?.id.channelID ?? channelID (the same fallback as refreshSubscription) so the bell state is correct from the loading phase, since notificationsEnabled(for:) only needs the ID string.
This commit is contained in:
@@ -155,17 +155,22 @@ struct ChannelView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let channel {
|
||||
channelContent(channel)
|
||||
} else if let cachedHeader {
|
||||
// Show header with cached data + spinner for content area
|
||||
loadingContent(cachedHeader)
|
||||
} else if isLoading {
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = errorMessage {
|
||||
errorView(error)
|
||||
// The search modifier is hoisted above this state-switching Group so the
|
||||
// search bar is present in every load state (loading spinner / cached header
|
||||
// / loaded content) instead of only appearing once `channel != nil`.
|
||||
channelSearchable {
|
||||
Group {
|
||||
if let channel {
|
||||
channelContent(channel)
|
||||
} else if let cachedHeader {
|
||||
// Show header with cached data + spinner for content area
|
||||
loadingContent(cachedHeader)
|
||||
} else if isLoading {
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = errorMessage {
|
||||
errorView(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(showInsetBackground ? viewBackgroundColor : .clear)
|
||||
@@ -182,6 +187,57 @@ struct ChannelView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the channel `.searchable` field and its search handlers around the given
|
||||
/// content. Kept outside `iOSChannelContent` so the search bar shows regardless of
|
||||
/// channel load state. On tvOS there is no `.searchable` (it uses a custom search
|
||||
/// tab), so this is a passthrough.
|
||||
@ViewBuilder
|
||||
private func channelSearchable(@ViewBuilder _ content: () -> some View) -> some View {
|
||||
#if os(tvOS)
|
||||
content()
|
||||
#else
|
||||
content()
|
||||
#if os(iOS)
|
||||
.if(supportsChannelSearch) { view in
|
||||
view.searchable(
|
||||
text: $searchText,
|
||||
isPresented: $isSearchActive,
|
||||
placement: .navigationBarDrawer(displayMode: .automatic),
|
||||
prompt: Text("channel.search.placeholder")
|
||||
)
|
||||
}
|
||||
#elseif os(macOS)
|
||||
.if(supportsChannelSearch) { view in
|
||||
view.searchable(
|
||||
text: $searchText,
|
||||
isPresented: $isSearchActive,
|
||||
placement: .toolbar,
|
||||
prompt: Text("channel.search.placeholder")
|
||||
)
|
||||
}
|
||||
#endif
|
||||
.onSubmit(of: .search) {
|
||||
Task {
|
||||
await performSearch()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchText) { _, newValue in
|
||||
if newValue.isEmpty && isSearchActive {
|
||||
// User cleared the search text, reset search state
|
||||
searchResults = .empty
|
||||
}
|
||||
}
|
||||
.onChange(of: isSearchActive) { _, isActive in
|
||||
if !isActive {
|
||||
// Search was dismissed, clear results
|
||||
hasSearched = false
|
||||
searchResults = .empty
|
||||
searchText = ""
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func loadWatchEntries() {
|
||||
watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
|
||||
}
|
||||
@@ -365,44 +421,6 @@ struct ChannelView: View {
|
||||
.toolbarBackground(collapseProgress > 0.8 ? .visible : .hidden, for: .navigationBar)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.if(supportsChannelSearch) { view in
|
||||
view.searchable(
|
||||
text: $searchText,
|
||||
isPresented: $isSearchActive,
|
||||
placement: .navigationBarDrawer(displayMode: .automatic),
|
||||
prompt: Text("channel.search.placeholder")
|
||||
)
|
||||
}
|
||||
#elseif os(macOS)
|
||||
.if(supportsChannelSearch) { view in
|
||||
view.searchable(
|
||||
text: $searchText,
|
||||
isPresented: $isSearchActive,
|
||||
placement: .toolbar,
|
||||
prompt: Text("channel.search.placeholder")
|
||||
)
|
||||
}
|
||||
#endif
|
||||
.onSubmit(of: .search) {
|
||||
Task {
|
||||
await performSearch()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchText) { _, newValue in
|
||||
if newValue.isEmpty && isSearchActive {
|
||||
// User cleared the search text, reset search state
|
||||
searchResults = .empty
|
||||
}
|
||||
}
|
||||
.onChange(of: isSearchActive) { _, isActive in
|
||||
if !isActive {
|
||||
// Search was dismissed, clear results
|
||||
hasSearched = false
|
||||
searchResults = .empty
|
||||
searchText = ""
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
String(localized: "channel.unsubscribe.confirmation.title"),
|
||||
isPresented: $showingUnsubscribeConfirmation,
|
||||
@@ -1032,9 +1050,10 @@ struct ChannelView: View {
|
||||
/// Icon for the channel menu based on subscription/notification state
|
||||
private var channelMenuIcon: String {
|
||||
if isSubscribed {
|
||||
let notificationsEnabled = channel.map {
|
||||
appEnvironment?.dataManager.notificationsEnabled(for: $0.id.channelID) ?? false
|
||||
} ?? false
|
||||
// Use the immediately-available channel ID so notification status is correct
|
||||
// during loading, before the network-loaded `channel` is set.
|
||||
let effectiveChannelID = channel?.id.channelID ?? channelID
|
||||
let notificationsEnabled = appEnvironment?.dataManager.notificationsEnabled(for: effectiveChannelID) ?? false
|
||||
return notificationsEnabled ? "bell.fill" : "person.fill"
|
||||
}
|
||||
return "person.badge.plus"
|
||||
@@ -1056,8 +1075,9 @@ struct ChannelView: View {
|
||||
}
|
||||
|
||||
// Notifications toggle (only visible when subscribed)
|
||||
if isSubscribed, let channel {
|
||||
let notificationsEnabled = appEnvironment?.dataManager.notificationsEnabled(for: channel.id.channelID) ?? false
|
||||
if isSubscribed {
|
||||
let effectiveChannelID = channel?.id.channelID ?? channelID
|
||||
let notificationsEnabled = appEnvironment?.dataManager.notificationsEnabled(for: effectiveChannelID) ?? false
|
||||
Button {
|
||||
toggleNotifications()
|
||||
} label: {
|
||||
|
||||
Reference in New Issue
Block a user