Improve layout stability and disable unwanted animations

Added height reservation to FavoriteItemView to prevent layout shifts during content loading. Changed HomeView to use LazyVStack for better performance. Converted QueueView from LazyVStack to VStack. Disabled animations on content count changes across multiple views to prevent jarring layout transitions. Added width constraint to stream button in PlaybackSettings.
This commit is contained in:
Arkadiusz Fal
2025-11-14 20:02:07 +01:00
parent 6c3da98465
commit a0a54bced9
6 changed files with 108 additions and 77 deletions

View File

@@ -34,7 +34,7 @@ struct FavoriteItemView: View {
var body: some View {
Group {
if isVisible {
VStack(alignment: .leading, spacing: 2) {
VStack(alignment: .leading, spacing: 0) {
itemControl
.contextMenu { contextMenu }
.contentShape(Rectangle())
@@ -64,24 +64,34 @@ struct FavoriteItemView: View {
#else
.padding(.horizontal, 15)
#endif
.frame(height: expectedContentHeight)
} else {
Group {
switch widgetListingStyle {
case .horizontalCells:
HorizontalCells(items: limitedItems)
case .list:
ListView(items: limitedItems)
.padding(.vertical, 10)
#if os(tvOS)
.padding(.leading, 40)
#else
.padding(.horizontal, 15)
#endif
ZStack(alignment: .topLeading) {
// Reserve space immediately to prevent layout shift
Color.clear
.frame(height: expectedContentHeight)
// Actual content renders within the reserved space
Group {
switch widgetListingStyle {
case .horizontalCells:
HorizontalCells(items: limitedItems)
case .list:
ListView(items: limitedItems)
.padding(.vertical, 10)
#if os(tvOS)
.padding(.leading, 40)
#else
.padding(.horizontal, 15)
#endif
}
}
.environment(\.inChannelView, inChannelView)
}
.environment(\.inChannelView, inChannelView)
.animation(nil, value: store.contentItems.count)
}
}
.animation(nil, value: store.contentItems.count)
.contentShape(Rectangle())
.onAppear {
if item.section == .history {
@@ -233,6 +243,23 @@ struct FavoriteItemView: View {
favoritesModel.listingStyle(item)
}
var expectedContentHeight: Double {
switch widgetListingStyle {
case .horizontalCells:
#if os(tvOS)
return 600
#else
return 290
#endif
case .list:
// Approximate height for list view items
let itemCount = favoritesModel.limit(item)
let itemHeight: Double = 70 // Approximate height per item
let padding: Double = 20
return Double(itemCount) * itemHeight + padding
}
}
func loadCacheAndResource(force: Bool = false) {
guard let resource else { return }

View File

@@ -30,91 +30,93 @@ struct HomeView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
#if !os(tvOS)
HStack {
if showOpenActionsInHome {
AccentButton(text: "Files", imageSystemName: "folder") {
NavigationModel.shared.presentingFileImporter = true
}
AccentButton(text: "Paste", imageSystemName: "doc.on.clipboard.fill") {
OpenVideosModel.shared.openURLsFromClipboard(playbackMode: .playNow)
}
AccentButton(imageSystemName: "ellipsis") {
NavigationModel.shared.presentingOpenVideos = true
}
.frame(maxWidth: 40)
}
}
#endif
#if os(tvOS)
HStack {
if showOpenActionsInHome {
Button {
NavigationModel.shared.presentingOpenVideos = true
} label: {
Label("Open Video", systemImage: "globe")
LazyVStack(spacing: 0, pinnedViews: []) {
VStack {
#if !os(tvOS)
HStack {
if showOpenActionsInHome {
AccentButton(text: "Files", imageSystemName: "folder") {
NavigationModel.shared.presentingFileImporter = true
}
AccentButton(text: "Paste", imageSystemName: "doc.on.clipboard.fill") {
OpenVideosModel.shared.openURLsFromClipboard(playbackMode: .playNow)
}
AccentButton(imageSystemName: "ellipsis") {
NavigationModel.shared.presentingOpenVideos = true
}
.frame(maxWidth: 40)
}
}
Button {
NavigationModel.shared.presentingAccounts = true
} label: {
Label("Locations", systemImage: "globe")
}
Spacer()
HideWatchedButtons()
HideShortsButtons()
Button {
NavigationModel.shared.presentingSettings = true
} label: {
Label("Settings", systemImage: "gear")
}
}
#if os(tvOS)
.font(.caption)
.imageScale(.small)
.foregroundColor(.primary)
#endif
#endif
}
.padding(.top, 15)
#if os(tvOS)
.padding(.horizontal, 40)
#else
.padding(.horizontal, 15)
#endif
if showQueueInHome {
QueueView()
#if os(tvOS)
HStack {
if showOpenActionsInHome {
Button {
NavigationModel.shared.presentingOpenVideos = true
} label: {
Label("Open Video", systemImage: "globe")
}
}
Button {
NavigationModel.shared.presentingAccounts = true
} label: {
Label("Locations", systemImage: "globe")
}
Spacer()
HideWatchedButtons()
HideShortsButtons()
Button {
NavigationModel.shared.presentingSettings = true
} label: {
Label("Settings", systemImage: "gear")
}
}
.font(.caption)
.imageScale(.small)
.foregroundColor(.primary)
#endif
}
.padding(.top, 15)
.padding(.bottom, 15)
#if os(tvOS)
.padding(.horizontal, 40)
#else
.padding(.horizontal, 15)
#endif
}
if !accounts.current.isNil, showFavoritesInHome {
VStack(alignment: .leading) {
if showQueueInHome {
QueueView()
#if os(tvOS)
.padding(.horizontal, 40)
#else
.padding(.horizontal, 15)
#endif
}
if !accounts.current.isNil, showFavoritesInHome {
#if os(tvOS)
ForEach(Defaults[.favorites]) { item in
FavoriteItemView(item: item, favoritesChanged: $favoritesChanged)
.animation(nil, value: favoritesChanged)
}
#else
ForEach(favorites) { item in
FavoriteItemView(item: item, favoritesChanged: $favoritesChanged)
.animation(nil, value: favoritesChanged)
#if os(macOS)
.workaroundForVerticalScrollingBug()
#endif
}
#endif
}
}
#if !os(tvOS)
Color.clear.padding(.bottom, 60)
#endif
#if !os(tvOS)
Color.clear.padding(.bottom, 60)
#endif
}
}
.animation(nil, value: favoritesChanged)
.onAppear {
updateTask = Task {
async let favoritesUpdates: Void = {

View File

@@ -6,7 +6,7 @@ struct QueueView: View {
@ObservedObject private var player = PlayerModel.shared
var body: some View {
LazyVStack {
VStack {
if !items.isEmpty {
Button {
withAnimation {

View File

@@ -411,7 +411,7 @@ struct PlaybackSettings: View {
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)
.frame(height: 40, alignment: .trailing)
.frame(width: 140, height: 40, alignment: .trailing)
#else
StreamControl(focusedField: $focusedField)
#endif

View File

@@ -34,6 +34,7 @@ struct HorizontalCells: View {
}
.frame(height: cellHeight)
.edgesIgnoringSafeArea(.horizontal)
.animation(nil, value: contentItems.count)
}
var contentItems: [ContentItem] {

View File

@@ -10,9 +10,10 @@ struct ListView: View {
ContentItemView(item: item)
.environment(\.listingStyle, .list)
.environment(\.noListingDividers, limit == 1)
.transition(.opacity)
.transition(.identity)
}
}
.animation(nil, value: limitedItems.count)
}
var limitedItems: [ContentItem] {