Send a creation where the button said it would go

Creating a playlist while an Invidious account was the selected context
wrote it to the device. No request left the app, the row never appeared in
the account's list, and the form had come up without its visibility picker
— the two symptoms of the same thing: the sheet took the local branch.

The destination was decided correctly and then read wrongly. The button
resolved the account and put it in a companion `@State`, and the sheet's
content closure — created by an earlier body evaluation — read that
property as it stood *then*, which was nil. Measured on device: the create
resolved the account and validity fine, and one millisecond later the sheet
rendered the local form while every live value around it still named the
account. Whether the stale read is corrected depends on whether anything
re-renders the view before the form is saved, which is why the same steps
against Piped had landed on the server.

The destination now travels with the presentation as
`PlaylistCreationTarget`, so the sheet is handed the account rather than
looking one up. One resolver answers for both the create affordances and
the row menus, and it is where the rule lives: a context reading an
account's data creates on that account or creates nothing at all — a
toolbar item can outlive the condition that put it there, and a write with
nowhere to go is not a write to somewhere else. A context that fell back to
local reads is the one case that does create locally, and it is honest:
those are the rows on screen, with the banner above them saying why.

Two more from the same pass:

- Subscriptions stopped giving two answers at once. Under a fallback the
  local feed asks for a Yattee Server nobody has configured, so "Add a
  subscriptions account in Settings" was stacking under a banner saying
  this account needs signing in again. The banner names the cause, so the
  feed error stands down while it is up.

- The playlists banner no longer lands on the search field. A navigation
  bar carrying a search field keeps its full height over content that
  cannot scroll, and the empty, loading and error states could not — they
  scroll now, which also gives pull to refresh somewhere to live.
This commit is contained in:
Arkadiusz Fal
2026-09-05 23:41:25 +02:00
parent d41c6938f2
commit 6bf3942529
4 changed files with 273 additions and 39 deletions

View File

@@ -165,11 +165,24 @@ struct SubscriptionsView: View {
}
#endif
/// The feed error worth showing, if any.
///
/// Nothing while the context banner is up: a selected account that cannot be
/// used reads local data, and the local feed then reports the Yattee Server
/// it has no reason to have — "add a subscriptions account in Settings"
/// under a banner saying *this* account needs signing in again is two
/// answers to one question. The banner names the cause, so it is the one
/// that stays.
private var displayedFeedError: FeedLoadState.FeedLoadError? {
guard contextUnavailability == nil,
case let .error(error) = feedCache.feedLoadState
else { return nil }
return error
}
private var isShowingFullScreenError: Bool {
if case .error = feedCache.feedLoadState, feedCache.videos.isEmpty {
return true
}
return false
displayedFeedError != nil && feedCache.videos.isEmpty
}
private var dataManager: DataManager? { appEnvironment?.dataManager }
@@ -249,19 +262,21 @@ struct SubscriptionsView: View {
.background(Color.black.opacity(0.3))
#endif
case .error(let error):
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
Text(errorMessage(for: error))
.font(.caption)
case .error:
if let error = displayedFeedError {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
Text(errorMessage(for: error))
.font(.caption)
}
.foregroundStyle(.red)
.padding(.horizontal)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
#if os(tvOS)
.background(Color.black.opacity(0.3))
#endif
}
.foregroundStyle(.red)
.padding(.horizontal)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
#if os(tvOS)
.background(Color.black.opacity(0.3))
#endif
case .loadingMore:
HStack(spacing: 8) {
@@ -640,7 +655,7 @@ struct SubscriptionsView: View {
/// Feed content rows or empty/loading states.
@ViewBuilder
private var feedContentRows: some View {
if case .error(let feedError) = feedCache.feedLoadState, feedCache.videos.isEmpty {
if let feedError = displayedFeedError, feedCache.videos.isEmpty {
// Show specific error states
switch feedError {
case .yatteeServerRequired:
@@ -730,7 +745,7 @@ struct SubscriptionsView: View {
#endif
// Content
if case .error(let feedError) = feedCache.feedLoadState, feedCache.videos.isEmpty {
if let feedError = displayedFeedError, feedCache.videos.isEmpty {
// Show specific error states
switch feedError {
case .yatteeServerRequired: