From 3c9e04d243a6c702e0ea4ece772f4a53030cec80 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 22 Jul 2023 19:02:59 +0200 Subject: [PATCH] Channels performance improvements Add settings: Show channel avatars in channels lists Show channel avatars in videos lists Fix #508 --- Model/Cache/FeedCacheModel.swift | 14 ++++--- Model/Cache/SubscribedChannelsModel.swift | 18 ++++----- Shared/Channels/ChannelAvatarView.swift | 15 +++++++- Shared/Channels/ChannelListItem.swift | 3 +- Shared/Channels/ChannelVideosView.swift | 1 + Shared/Defaults.swift | 3 ++ Shared/Navigation/AppSidebarRecents.swift | 5 ++- .../Navigation/AppSidebarSubscriptions.swift | 38 ++++--------------- Shared/Settings/BrowsingSettings.swift | 5 +++ Shared/Settings/SettingsView.swift | 2 +- Shared/Subscriptions/ChannelsView.swift | 6 +-- Shared/Videos/VideoBanner.swift | 6 +-- Shared/Videos/VideoCell.swift | 26 +++++++------ Shared/Views/ControlsBar.swift | 10 +++-- 14 files changed, 81 insertions(+), 71 deletions(-) diff --git a/Model/Cache/FeedCacheModel.swift b/Model/Cache/FeedCacheModel.swift index 002d32d0..bd71d10a 100644 --- a/Model/Cache/FeedCacheModel.swift +++ b/Model/Cache/FeedCacheModel.swift @@ -18,12 +18,14 @@ struct FeedCacheModel: CacheModel { ) func storeFeed(account: Account, videos: [Video]) { - let date = iso8601DateFormatter.string(from: Date()) - logger.info("caching feed \(account.feedCacheKey) -- \(date)") - let feedTimeObject: JSON = ["date": date] - let videosObject: JSON = ["videos": videos.prefix(cacheLimit).map { $0.json.object }] - try? storage?.setObject(feedTimeObject, forKey: feedTimeCacheKey(account.feedCacheKey)) - try? storage?.setObject(videosObject, forKey: account.feedCacheKey) + DispatchQueue.global(qos: .background).async { + let date = iso8601DateFormatter.string(from: Date()) + logger.info("caching feed \(account.feedCacheKey) -- \(date)") + let feedTimeObject: JSON = ["date": date] + let videosObject: JSON = ["videos": videos.prefix(cacheLimit).map { $0.json.object }] + try? storage?.setObject(feedTimeObject, forKey: feedTimeCacheKey(account.feedCacheKey)) + try? storage?.setObject(videosObject, forKey: account.feedCacheKey) + } } func retrieveFeed(account: Account) -> [Video] { diff --git a/Model/Cache/SubscribedChannelsModel.swift b/Model/Cache/SubscribedChannelsModel.swift index 32844608..d7209fca 100644 --- a/Model/Cache/SubscribedChannelsModel.swift +++ b/Model/Cache/SubscribedChannelsModel.swift @@ -85,7 +85,6 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel { self.error = nil if let channels: [Channel] = resource.typedContent() { self.channels = channels - channels.forEach { ChannelsCacheModel.shared.storeIfMissing($0) } self.storeChannels(account: account, channels: channels) FeedModel.shared.calculateUnwatchedFeed() onSuccess() @@ -95,7 +94,6 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel { } } - func loadCachedChannels(_ account: Account) { let cache = getChannels(account: account) if !cache.isEmpty { @@ -106,16 +104,18 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel { } func storeChannels(account: Account, channels: [Channel]) { - let date = iso8601DateFormatter.string(from: Date()) - logger.info("caching channels \(channelsDateCacheKey(account)) -- \(date)") + DispatchQueue.global(qos: .background).async { + let date = self.iso8601DateFormatter.string(from: Date()) + self.logger.info("caching channels \(self.channelsDateCacheKey(account)) -- \(date)") - channels.forEach { ChannelsCacheModel.shared.storeIfMissing($0) } + channels.forEach { ChannelsCacheModel.shared.storeIfMissing($0) } - let dateObject: JSON = ["date": date] - let channelsObject: JSON = ["channels": channels.map(\.json).map(\.object)] + let dateObject: JSON = ["date": date] + let channelsObject: JSON = ["channels": channels.map(\.json).map(\.object)] - try? storage?.setObject(dateObject, forKey: channelsDateCacheKey(account)) - try? storage?.setObject(channelsObject, forKey: channelsCacheKey(account)) + try? self.storage?.setObject(dateObject, forKey: self.channelsDateCacheKey(account)) + try? self.storage?.setObject(channelsObject, forKey: self.channelsCacheKey(account)) + } } func getChannels(account: Account) -> [Channel] { diff --git a/Shared/Channels/ChannelAvatarView.swift b/Shared/Channels/ChannelAvatarView.swift index 9e9ff81f..90b96928 100644 --- a/Shared/Channels/ChannelAvatarView.swift +++ b/Shared/Channels/ChannelAvatarView.swift @@ -9,11 +9,13 @@ struct ChannelAvatarView: View { @ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var subscribedChannels = SubscribedChannelsModel.shared + @State private var url: URL? + var body: some View { ZStack(alignment: .bottomTrailing) { Group { Group { - if let url = channel?.thumbnailURLOrCached { + if let url { ThumbnailView(url: url) } else { ZStack { @@ -31,6 +33,7 @@ struct ChannelAvatarView: View { .font(.system(size: 20)) .contentShape(Rectangle()) } + .onAppear(perform: updateURL) } } .clipShape(Circle()) @@ -54,6 +57,16 @@ struct ChannelAvatarView: View { } .imageScale(.small) } + + func updateURL() { + DispatchQueue.global(qos: .userInitiated).async { + if let url = channel?.thumbnailURLOrCached { + DispatchQueue.main.async { + self.url = url + } + } + } + } } struct ChannelAvatarView_Previews: PreviewProvider { diff --git a/Shared/Channels/ChannelListItem.swift b/Shared/Channels/ChannelListItem.swift index 7366dfe5..dc358d40 100644 --- a/Shared/Channels/ChannelListItem.swift +++ b/Shared/Channels/ChannelListItem.swift @@ -61,7 +61,8 @@ struct ChannelListItem: View { private var label: some View { HStack(alignment: .top, spacing: 12) { VStack { - ChannelAvatarView(channel: channel) + ChannelAvatarView(channel: channel, subscribedBadge: false) + .id("channel-avatar-\(channel.id)") #if os(tvOS) .frame(width: 90, height: 90) #else diff --git a/Shared/Channels/ChannelVideosView.swift b/Shared/Channels/ChannelVideosView.swift index b64f1b4f..65994672 100644 --- a/Shared/Channels/ChannelVideosView.swift +++ b/Shared/Channels/ChannelVideosView.swift @@ -205,6 +205,7 @@ struct ChannelVideosView: View { var thumbnail: some View { ChannelAvatarView(channel: store.item?.channel) + .id("channel-avatar-\(store.item?.channel?.id ?? "")") #if os(tvOS) .frame(width: 80, height: 80, alignment: .trailing) #else diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 27052dae..0f4198b2 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -148,6 +148,9 @@ extension Defaults.Keys { #endif static let expandVideoDescription = Key("expandVideoDescription", default: expandVideoDescriptionDefault) + static let showChannelAvatarInChannelsLists = Key("showChannelAvatarInChannelsLists", default: true) + static let showChannelAvatarInVideosListing = Key("showChannelAvatarInVideosListing", default: true) + #if os(tvOS) static let pauseOnHidingPlayerDefault = true #else diff --git a/Shared/Navigation/AppSidebarRecents.swift b/Shared/Navigation/AppSidebarRecents.swift index b412d854..d20d40ed 100644 --- a/Shared/Navigation/AppSidebarRecents.swift +++ b/Shared/Navigation/AppSidebarRecents.swift @@ -50,6 +50,8 @@ struct RecentNavigationLink: View { var recents = RecentsModel.shared @ObservedObject private var navigation = NavigationModel.shared + @Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists + var recent: RecentItem var systemImage: String? let destination: DestinationContent @@ -71,9 +73,10 @@ struct RecentNavigationLink: View { HStack { if recent.type == .channel, let channel = recent.channel, - channel.thumbnailURLOrCached != nil + showChannelAvatarInChannelsLists { ChannelAvatarView(channel: channel, subscribedBadge: false) + .id("channel-avatar-\(channel.id)") .frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize) Text(channel.name) diff --git a/Shared/Navigation/AppSidebarSubscriptions.swift b/Shared/Navigation/AppSidebarSubscriptions.swift index 8a1083d7..d88ff831 100644 --- a/Shared/Navigation/AppSidebarSubscriptions.swift +++ b/Shared/Navigation/AppSidebarSubscriptions.swift @@ -8,6 +8,11 @@ struct AppSidebarSubscriptions: View { @ObservedObject private var subscriptions = SubscribedChannelsModel.shared @Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges + @Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop + @Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists + + @State private var channelLinkActive = false + @State private var channelForLink: Channel? var body: some View { Section(header: Text("Subscriptions")) { @@ -16,9 +21,10 @@ struct AppSidebarSubscriptions: View { LazyView(ChannelVideosView(channel: channel)) } label: { HStack { - if channel.thumbnailURLOrCached != nil { + if showChannelAvatarInChannelsLists { ChannelAvatarView(channel: channel, subscribedBadge: false) .frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize) + Text(channel.name) } else { Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name)) @@ -26,13 +32,10 @@ struct AppSidebarSubscriptions: View { Spacer() } + .lineLimit(1) .badge(showUnwatchedFeedBadges ? feedCount.unwatchedByChannelText(channel) : nil) } .contextMenu { - if subscriptions.isSubscribing(channel.id) { - toggleWatchedButton(channel) - } - Button("Unsubscribe") { navigation.presentUnsubscribeAlert(channel, subscriptions: subscriptions) } @@ -41,31 +44,6 @@ struct AppSidebarSubscriptions: View { } } } - - @ViewBuilder func toggleWatchedButton(_ channel: Channel) -> some View { - if feed.canMarkChannelAsWatched(channel.id) { - markChannelAsWatchedButton(channel) - } else { - markChannelAsUnwatchedButton(channel) - } - } - - func markChannelAsWatchedButton(_ channel: Channel) -> some View { - Button { - feed.markChannelAsWatched(channel.id) - } label: { - Label("Mark channel feed as watched", systemImage: "checkmark.circle.fill") - } - .disabled(!feed.canMarkAllFeedAsWatched) - } - - func markChannelAsUnwatchedButton(_ channel: Channel) -> some View { - Button { - feed.markChannelAsUnwatched(channel.id) - } label: { - Label("Mark channel feed as unwatched", systemImage: "checkmark.circle") - } - } } struct AppSidebarSubscriptions_Previews: PreviewProvider { diff --git a/Shared/Settings/BrowsingSettings.swift b/Shared/Settings/BrowsingSettings.swift index bd1a5b4a..f687d0f8 100644 --- a/Shared/Settings/BrowsingSettings.swift +++ b/Shared/Settings/BrowsingSettings.swift @@ -25,6 +25,8 @@ struct BrowsingSettings: View { @Default(.playerButtonIsExpanded) private var playerButtonIsExpanded @Default(.playerBarMaxWidth) private var playerBarMaxWidth @Default(.expandChannelDescription) private var expandChannelDescription + @Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists + @Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing @ObservedObject private var accounts = AccountsModel.shared @@ -186,6 +188,9 @@ struct BrowsingSettings: View { } Toggle("Keep channels with unwatched videos on top of subscriptions list", isOn: $keepChannelsWithUnwatchedFeedOnTop) + + Toggle("Show channel avatars in channels lists", isOn: $showChannelAvatarInChannelsLists) + Toggle("Show channel avatars in videos lists", isOn: $showChannelAvatarInVideosListing) } } diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index 036794ba..933c9b1c 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -243,7 +243,7 @@ struct SettingsView: View { private var windowHeight: Double { switch selection { case .browsing: - return 720 + return 800 case .player: return 480 case .controls: diff --git a/Shared/Subscriptions/ChannelsView.swift b/Shared/Subscriptions/ChannelsView.swift index 4dec5b2b..d58936c0 100644 --- a/Shared/Subscriptions/ChannelsView.swift +++ b/Shared/Subscriptions/ChannelsView.swift @@ -12,6 +12,7 @@ struct ChannelsView: View { @Default(.showCacheStatus) private var showCacheStatus @Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges @Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop + @Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists @State private var channelLinkActive = false @State private var channelForLink: Channel? @@ -21,10 +22,9 @@ struct ChannelsView: View { Section(header: header) { ForEach(channels) { channel in let label = HStack { - if let url = channel.thumbnailURLOrCached { - ThumbnailView(url: url) + if showChannelAvatarInChannelsLists { + ChannelAvatarView(channel: channel, subscribedBadge: false) .frame(width: 35, height: 35) - .clipShape(RoundedRectangle(cornerRadius: 35)) } else { Image(systemName: RecentsModel.symbolSystemImage(channel.name)) .imageScale(.large) diff --git a/Shared/Videos/VideoBanner.swift b/Shared/Videos/VideoBanner.swift index fd7a4042..db79db72 100644 --- a/Shared/Videos/VideoBanner.swift +++ b/Shared/Videos/VideoBanner.swift @@ -16,6 +16,7 @@ struct VideoBanner: View { @Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor @Default(.timeOnThumbnail) private var timeOnThumbnail @Default(.roundedThumbnails) private var roundedThumbnails + @Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing @Environment(\.inChannelView) private var inChannelView @Environment(\.inNavigationView) private var inNavigationView @@ -85,10 +86,9 @@ struct VideoBanner: View { if !inChannelView, !video.isLocal || video.localStreamIsRemoteURL { ChannelLinkView(channel: video.channel) { HStack(spacing: Constants.channelDetailsStackSpacing) { - if let url = video.channel.thumbnailURLOrCached, video != .fixture { - ThumbnailView(url: url) + if video != .fixture, showChannelAvatarInVideosListing { + ChannelAvatarView(channel: video.channel) .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) - .clipShape(Circle()) } channelLabel diff --git a/Shared/Videos/VideoCell.swift b/Shared/Videos/VideoCell.swift index 4d96844f..0d0b2020 100644 --- a/Shared/Videos/VideoCell.swift +++ b/Shared/Videos/VideoCell.swift @@ -24,6 +24,7 @@ struct VideoCell: View { @Default(.watchedVideoStyle) private var watchedVideoStyle @Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor @Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior + @Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing private var navigation: NavigationModel { .shared } private var player: PlayerModel { .shared } @@ -161,17 +162,22 @@ struct VideoCell: View { HStack(spacing: Constants.channelDetailsStackSpacing) { if !inChannelView, - let url = video.channel.thumbnailURLOrCached, + showChannelAvatarInVideosListing, video != .fixture { ChannelLinkView(channel: video.channel) { - ThumbnailView(url: url) - .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) - .clipShape(Circle()) + if showChannelAvatarInVideosListing { + ChannelAvatarView(channel: video.channel) + .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) + } else { + channelLabel(badge: false) + } } } - if !channelOnThumbnail, !inChannelView { + if !channelOnThumbnail, + !inChannelView + { ChannelLinkView(channel: video.channel) { channelLabel(badge: false) } @@ -264,12 +270,9 @@ struct VideoCell: View { if !channelOnThumbnail, !inChannelView { ChannelLinkView(channel: video.channel) { HStack(spacing: Constants.channelDetailsStackSpacing) { - if let url = video.channel.thumbnailURLOrCached, - video != .fixture - { - ThumbnailView(url: url) + if video != .fixture, showChannelAvatarInVideosListing { + ChannelAvatarView(channel: video.channel) .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) - .clipShape(Circle()) } channelLabel(badge: false) @@ -295,9 +298,8 @@ struct VideoCell: View { video != .fixture { ChannelLinkView(channel: video.channel) { - ThumbnailView(url: url) + ChannelAvatarView(channel: video.channel) .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) - .clipShape(Circle()) } } diff --git a/Shared/Views/ControlsBar.swift b/Shared/Views/ControlsBar.swift index f9a017ad..6ed6737c 100644 --- a/Shared/Views/ControlsBar.swift +++ b/Shared/Views/ControlsBar.swift @@ -167,6 +167,7 @@ struct ControlsBar: View { channel: model.videoForDisplay?.channel, video: model.videoForDisplay ) + .id("channel-avatar-\(model.videoForDisplay?.id ?? "")") .frame(width: barHeight - 10, height: barHeight - 10) } .contextMenu { contextMenu } @@ -176,12 +177,13 @@ struct ControlsBar: View { channel: model.videoForDisplay?.channel, video: model.videoForDisplay ) + .id("channel-avatar-\(model.videoForDisplay?.id ?? "")") #if !os(tvOS) - .highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil) - .gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil) + .highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil) + .gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil) #endif - .frame(width: barHeight - 10, height: barHeight - 10) - .contextMenu { contextMenu } + .frame(width: barHeight - 10, height: barHeight - 10) + .contextMenu { contextMenu } } if expansionState == .full {