diff --git a/Shared/Home/FavoriteItemView.swift b/Shared/Home/FavoriteItemView.swift index 9002c19b..aa8f9fd2 100644 --- a/Shared/Home/FavoriteItemView.swift +++ b/Shared/Home/FavoriteItemView.swift @@ -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 } diff --git a/Shared/Home/HomeView.swift b/Shared/Home/HomeView.swift index 3632207c..13b59a07 100644 --- a/Shared/Home/HomeView.swift +++ b/Shared/Home/HomeView.swift @@ -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 = { diff --git a/Shared/Home/QueueView.swift b/Shared/Home/QueueView.swift index 298126d2..052cfef6 100644 --- a/Shared/Home/QueueView.swift +++ b/Shared/Home/QueueView.swift @@ -6,7 +6,7 @@ struct QueueView: View { @ObservedObject private var player = PlayerModel.shared var body: some View { - LazyVStack { + VStack { if !items.isEmpty { Button { withAnimation { diff --git a/Shared/Player/PlaybackSettings.swift b/Shared/Player/PlaybackSettings.swift index b3fdc275..eee698d4 100644 --- a/Shared/Player/PlaybackSettings.swift +++ b/Shared/Player/PlaybackSettings.swift @@ -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 diff --git a/Shared/Videos/HorizontalCells.swift b/Shared/Videos/HorizontalCells.swift index 069f5f59..129a57d4 100644 --- a/Shared/Videos/HorizontalCells.swift +++ b/Shared/Videos/HorizontalCells.swift @@ -34,6 +34,7 @@ struct HorizontalCells: View { } .frame(height: cellHeight) .edgesIgnoringSafeArea(.horizontal) + .animation(nil, value: contentItems.count) } var contentItems: [ContentItem] { diff --git a/Shared/Videos/ListView.swift b/Shared/Videos/ListView.swift index 8ed10b35..6a82fe15 100644 --- a/Shared/Videos/ListView.swift +++ b/Shared/Videos/ListView.swift @@ -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] {