mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 09:19:46 +00:00
Fix pull-to-refresh scroll offset not resetting in InstanceBrowseView
Move .refreshable from the outer GeometryReader onto the ScrollView itself so SwiftUI can properly coordinate the scroll offset bounce-back. The ScrollView was inside an .overlay() which doesn't participate in the parent's layout system, breaking the offset reset. Closes #917
This commit is contained in:
@@ -36,6 +36,7 @@ struct InstanceBrowseView: View {
|
|||||||
@State private var feedPage = 1
|
@State private var feedPage = 1
|
||||||
@State private var hasMoreFeedResults = true
|
@State private var hasMoreFeedResults = true
|
||||||
@State private var isLoadingMoreFeed = false
|
@State private var isLoadingMoreFeed = false
|
||||||
|
@State private var contentLoadTask: Task<Void, Never>?
|
||||||
@State private var feedLoadedVideoCount = 0 // Track count when last load was triggered
|
@State private var feedLoadedVideoCount = 0 // Track count when last load was triggered
|
||||||
|
|
||||||
// View options (persisted per instance)
|
// View options (persisted per instance)
|
||||||
@@ -101,10 +102,10 @@ struct InstanceBrowseView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let backgroundStyle: ListBackgroundStyle = listStyle == .inset ? .grouped : .plain
|
let backgroundStyle: ListBackgroundStyle = listStyle == .inset ? .grouped : .plain
|
||||||
backgroundStyle.color
|
GeometryReader { geometry in
|
||||||
.ignoresSafeArea()
|
backgroundStyle.color
|
||||||
.overlay(
|
.ignoresSafeArea()
|
||||||
GeometryReader { geometry in
|
.overlay(
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Tab picker (hidden during search)
|
// Tab picker (hidden during search)
|
||||||
@@ -186,11 +187,14 @@ struct InstanceBrowseView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: geometry.size.width, initial: true) { _, newWidth in
|
.refreshable {
|
||||||
viewWidth = newWidth
|
await startContentLoad(forceRefresh: true)
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
.onChange(of: geometry.size.width, initial: true) { _, newWidth in
|
||||||
|
viewWidth = newWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
.navigationTitle(instance.displayName)
|
.navigationTitle(instance.displayName)
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.toolbarTitleDisplayMode(.inlineLarge)
|
.toolbarTitleDisplayMode(.inlineLarge)
|
||||||
@@ -246,16 +250,13 @@ struct InstanceBrowseView: View {
|
|||||||
// Load watch entries for hide watched feature
|
// Load watch entries for hide watched feature
|
||||||
loadWatchEntries()
|
loadWatchEntries()
|
||||||
|
|
||||||
await loadContent()
|
await startContentLoad()
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in
|
||||||
loadWatchEntries()
|
loadWatchEntries()
|
||||||
}
|
}
|
||||||
.onChange(of: selectedTab) { _, _ in
|
.onChange(of: selectedTab) { _, _ in
|
||||||
Task { await loadContent() }
|
Task { await startContentLoad() }
|
||||||
}
|
|
||||||
.refreshable {
|
|
||||||
await loadContent(forceRefresh: true)
|
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.searchable(
|
.searchable(
|
||||||
@@ -596,7 +597,7 @@ struct InstanceBrowseView: View {
|
|||||||
Text(error)
|
Text(error)
|
||||||
} actions: {
|
} actions: {
|
||||||
Button(String(localized: "common.retry")) {
|
Button(String(localized: "common.retry")) {
|
||||||
Task { await loadContent(forceRefresh: true) }
|
Task { await startContentLoad(forceRefresh: true) }
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
}
|
}
|
||||||
@@ -821,7 +822,17 @@ struct InstanceBrowseView: View {
|
|||||||
watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
|
watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadContent(forceRefresh: Bool = false) async {
|
private func startContentLoad(forceRefresh: Bool = false) async {
|
||||||
|
// Cancel any in-flight load before starting a new one
|
||||||
|
contentLoadTask?.cancel()
|
||||||
|
let task = Task {
|
||||||
|
await performLoadContent(forceRefresh: forceRefresh)
|
||||||
|
}
|
||||||
|
contentLoadTask = task
|
||||||
|
await task.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performLoadContent(forceRefresh: Bool = false) async {
|
||||||
guard let appEnvironment else {
|
guard let appEnvironment else {
|
||||||
errorMessage = "App not initialized"
|
errorMessage = "App not initialized"
|
||||||
isLoading = false
|
isLoading = false
|
||||||
@@ -842,7 +853,7 @@ struct InstanceBrowseView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = true
|
isLoading = !hasData // Only show loading spinner when no existing data
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -865,8 +876,8 @@ struct InstanceBrowseView: View {
|
|||||||
if forceRefresh {
|
if forceRefresh {
|
||||||
feedPage = 1
|
feedPage = 1
|
||||||
hasMoreFeedResults = true
|
hasMoreFeedResults = true
|
||||||
feedVideos = []
|
// Don't clear feedVideos here — keep old data visible
|
||||||
selectedFeedChannelID = nil
|
// until the API call succeeds and replaces it.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load subscriptions and feed based on instance type
|
// Load subscriptions and feed based on instance type
|
||||||
@@ -937,6 +948,10 @@ struct InstanceBrowseView: View {
|
|||||||
let playlists = try await api.userPlaylists(instance: instance, sid: credential)
|
let playlists = try await api.userPlaylists(instance: instance, sid: credential)
|
||||||
userPlaylists = playlists
|
userPlaylists = playlists
|
||||||
}
|
}
|
||||||
|
} catch is CancellationError {
|
||||||
|
// Task was cancelled (e.g., by SwiftUI during pull-to-refresh) — don't show error
|
||||||
|
} catch let error as APIError where error == .cancelled {
|
||||||
|
// HTTP request was cancelled — don't show error
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user