Channels performance improvements

Add settings:
Show channel avatars in channels lists
Show channel avatars in videos lists

Fix #508
This commit is contained in:
Arkadiusz Fal 2023-07-22 19:02:59 +02:00
parent 37a96a01db
commit 3c9e04d243
14 changed files with 81 additions and 71 deletions

View File

@ -18,12 +18,14 @@ struct FeedCacheModel: CacheModel {
) )
func storeFeed(account: Account, videos: [Video]) { func storeFeed(account: Account, videos: [Video]) {
let date = iso8601DateFormatter.string(from: Date()) DispatchQueue.global(qos: .background).async {
logger.info("caching feed \(account.feedCacheKey) -- \(date)") let date = iso8601DateFormatter.string(from: Date())
let feedTimeObject: JSON = ["date": date] logger.info("caching feed \(account.feedCacheKey) -- \(date)")
let videosObject: JSON = ["videos": videos.prefix(cacheLimit).map { $0.json.object }] let feedTimeObject: JSON = ["date": date]
try? storage?.setObject(feedTimeObject, forKey: feedTimeCacheKey(account.feedCacheKey)) let videosObject: JSON = ["videos": videos.prefix(cacheLimit).map { $0.json.object }]
try? storage?.setObject(videosObject, forKey: account.feedCacheKey) try? storage?.setObject(feedTimeObject, forKey: feedTimeCacheKey(account.feedCacheKey))
try? storage?.setObject(videosObject, forKey: account.feedCacheKey)
}
} }
func retrieveFeed(account: Account) -> [Video] { func retrieveFeed(account: Account) -> [Video] {

View File

@ -85,7 +85,6 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
self.error = nil self.error = nil
if let channels: [Channel] = resource.typedContent() { if let channels: [Channel] = resource.typedContent() {
self.channels = channels self.channels = channels
channels.forEach { ChannelsCacheModel.shared.storeIfMissing($0) }
self.storeChannels(account: account, channels: channels) self.storeChannels(account: account, channels: channels)
FeedModel.shared.calculateUnwatchedFeed() FeedModel.shared.calculateUnwatchedFeed()
onSuccess() onSuccess()
@ -95,7 +94,6 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
} }
} }
func loadCachedChannels(_ account: Account) { func loadCachedChannels(_ account: Account) {
let cache = getChannels(account: account) let cache = getChannels(account: account)
if !cache.isEmpty { if !cache.isEmpty {
@ -106,16 +104,18 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
} }
func storeChannels(account: Account, channels: [Channel]) { func storeChannels(account: Account, channels: [Channel]) {
let date = iso8601DateFormatter.string(from: Date()) DispatchQueue.global(qos: .background).async {
logger.info("caching channels \(channelsDateCacheKey(account)) -- \(date)") 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 dateObject: JSON = ["date": date]
let channelsObject: JSON = ["channels": channels.map(\.json).map(\.object)] let channelsObject: JSON = ["channels": channels.map(\.json).map(\.object)]
try? storage?.setObject(dateObject, forKey: channelsDateCacheKey(account)) try? self.storage?.setObject(dateObject, forKey: self.channelsDateCacheKey(account))
try? storage?.setObject(channelsObject, forKey: channelsCacheKey(account)) try? self.storage?.setObject(channelsObject, forKey: self.channelsCacheKey(account))
}
} }
func getChannels(account: Account) -> [Channel] { func getChannels(account: Account) -> [Channel] {

View File

@ -9,11 +9,13 @@ struct ChannelAvatarView: View {
@ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var accounts = AccountsModel.shared
@ObservedObject private var subscribedChannels = SubscribedChannelsModel.shared @ObservedObject private var subscribedChannels = SubscribedChannelsModel.shared
@State private var url: URL?
var body: some View { var body: some View {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
Group { Group {
Group { Group {
if let url = channel?.thumbnailURLOrCached { if let url {
ThumbnailView(url: url) ThumbnailView(url: url)
} else { } else {
ZStack { ZStack {
@ -31,6 +33,7 @@ struct ChannelAvatarView: View {
.font(.system(size: 20)) .font(.system(size: 20))
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.onAppear(perform: updateURL)
} }
} }
.clipShape(Circle()) .clipShape(Circle())
@ -54,6 +57,16 @@ struct ChannelAvatarView: View {
} }
.imageScale(.small) .imageScale(.small)
} }
func updateURL() {
DispatchQueue.global(qos: .userInitiated).async {
if let url = channel?.thumbnailURLOrCached {
DispatchQueue.main.async {
self.url = url
}
}
}
}
} }
struct ChannelAvatarView_Previews: PreviewProvider { struct ChannelAvatarView_Previews: PreviewProvider {

View File

@ -61,7 +61,8 @@ struct ChannelListItem: View {
private var label: some View { private var label: some View {
HStack(alignment: .top, spacing: 12) { HStack(alignment: .top, spacing: 12) {
VStack { VStack {
ChannelAvatarView(channel: channel) ChannelAvatarView(channel: channel, subscribedBadge: false)
.id("channel-avatar-\(channel.id)")
#if os(tvOS) #if os(tvOS)
.frame(width: 90, height: 90) .frame(width: 90, height: 90)
#else #else

View File

@ -205,6 +205,7 @@ struct ChannelVideosView: View {
var thumbnail: some View { var thumbnail: some View {
ChannelAvatarView(channel: store.item?.channel) ChannelAvatarView(channel: store.item?.channel)
.id("channel-avatar-\(store.item?.channel?.id ?? "")")
#if os(tvOS) #if os(tvOS)
.frame(width: 80, height: 80, alignment: .trailing) .frame(width: 80, height: 80, alignment: .trailing)
#else #else

View File

@ -148,6 +148,9 @@ extension Defaults.Keys {
#endif #endif
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault) static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
static let showChannelAvatarInChannelsLists = Key<Bool>("showChannelAvatarInChannelsLists", default: true)
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
#if os(tvOS) #if os(tvOS)
static let pauseOnHidingPlayerDefault = true static let pauseOnHidingPlayerDefault = true
#else #else

View File

@ -50,6 +50,8 @@ struct RecentNavigationLink<DestinationContent: View>: View {
var recents = RecentsModel.shared var recents = RecentsModel.shared
@ObservedObject private var navigation = NavigationModel.shared @ObservedObject private var navigation = NavigationModel.shared
@Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists
var recent: RecentItem var recent: RecentItem
var systemImage: String? var systemImage: String?
let destination: DestinationContent let destination: DestinationContent
@ -71,9 +73,10 @@ struct RecentNavigationLink<DestinationContent: View>: View {
HStack { HStack {
if recent.type == .channel, if recent.type == .channel,
let channel = recent.channel, let channel = recent.channel,
channel.thumbnailURLOrCached != nil showChannelAvatarInChannelsLists
{ {
ChannelAvatarView(channel: channel, subscribedBadge: false) ChannelAvatarView(channel: channel, subscribedBadge: false)
.id("channel-avatar-\(channel.id)")
.frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize) .frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize)
Text(channel.name) Text(channel.name)

View File

@ -8,6 +8,11 @@ struct AppSidebarSubscriptions: View {
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared @ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges @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 { var body: some View {
Section(header: Text("Subscriptions")) { Section(header: Text("Subscriptions")) {
@ -16,9 +21,10 @@ struct AppSidebarSubscriptions: View {
LazyView(ChannelVideosView(channel: channel)) LazyView(ChannelVideosView(channel: channel))
} label: { } label: {
HStack { HStack {
if channel.thumbnailURLOrCached != nil { if showChannelAvatarInChannelsLists {
ChannelAvatarView(channel: channel, subscribedBadge: false) ChannelAvatarView(channel: channel, subscribedBadge: false)
.frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize) .frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize)
Text(channel.name) Text(channel.name)
} else { } else {
Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name)) Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name))
@ -26,13 +32,10 @@ struct AppSidebarSubscriptions: View {
Spacer() Spacer()
} }
.lineLimit(1)
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedByChannelText(channel) : nil) .badge(showUnwatchedFeedBadges ? feedCount.unwatchedByChannelText(channel) : nil)
} }
.contextMenu { .contextMenu {
if subscriptions.isSubscribing(channel.id) {
toggleWatchedButton(channel)
}
Button("Unsubscribe") { Button("Unsubscribe") {
navigation.presentUnsubscribeAlert(channel, subscriptions: subscriptions) 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 { struct AppSidebarSubscriptions_Previews: PreviewProvider {

View File

@ -25,6 +25,8 @@ struct BrowsingSettings: View {
@Default(.playerButtonIsExpanded) private var playerButtonIsExpanded @Default(.playerButtonIsExpanded) private var playerButtonIsExpanded
@Default(.playerBarMaxWidth) private var playerBarMaxWidth @Default(.playerBarMaxWidth) private var playerBarMaxWidth
@Default(.expandChannelDescription) private var expandChannelDescription @Default(.expandChannelDescription) private var expandChannelDescription
@Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists
@Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing
@ObservedObject private var accounts = AccountsModel.shared @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("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)
} }
} }

View File

@ -243,7 +243,7 @@ struct SettingsView: View {
private var windowHeight: Double { private var windowHeight: Double {
switch selection { switch selection {
case .browsing: case .browsing:
return 720 return 800
case .player: case .player:
return 480 return 480
case .controls: case .controls:

View File

@ -12,6 +12,7 @@ struct ChannelsView: View {
@Default(.showCacheStatus) private var showCacheStatus @Default(.showCacheStatus) private var showCacheStatus
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges @Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
@Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop @Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop
@Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists
@State private var channelLinkActive = false @State private var channelLinkActive = false
@State private var channelForLink: Channel? @State private var channelForLink: Channel?
@ -21,10 +22,9 @@ struct ChannelsView: View {
Section(header: header) { Section(header: header) {
ForEach(channels) { channel in ForEach(channels) { channel in
let label = HStack { let label = HStack {
if let url = channel.thumbnailURLOrCached { if showChannelAvatarInChannelsLists {
ThumbnailView(url: url) ChannelAvatarView(channel: channel, subscribedBadge: false)
.frame(width: 35, height: 35) .frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 35))
} else { } else {
Image(systemName: RecentsModel.symbolSystemImage(channel.name)) Image(systemName: RecentsModel.symbolSystemImage(channel.name))
.imageScale(.large) .imageScale(.large)

View File

@ -16,6 +16,7 @@ struct VideoBanner: View {
@Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor @Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor
@Default(.timeOnThumbnail) private var timeOnThumbnail @Default(.timeOnThumbnail) private var timeOnThumbnail
@Default(.roundedThumbnails) private var roundedThumbnails @Default(.roundedThumbnails) private var roundedThumbnails
@Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing
@Environment(\.inChannelView) private var inChannelView @Environment(\.inChannelView) private var inChannelView
@Environment(\.inNavigationView) private var inNavigationView @Environment(\.inNavigationView) private var inNavigationView
@ -85,10 +86,9 @@ struct VideoBanner: View {
if !inChannelView, !video.isLocal || video.localStreamIsRemoteURL { if !inChannelView, !video.isLocal || video.localStreamIsRemoteURL {
ChannelLinkView(channel: video.channel) { ChannelLinkView(channel: video.channel) {
HStack(spacing: Constants.channelDetailsStackSpacing) { HStack(spacing: Constants.channelDetailsStackSpacing) {
if let url = video.channel.thumbnailURLOrCached, video != .fixture { if video != .fixture, showChannelAvatarInVideosListing {
ThumbnailView(url: url) ChannelAvatarView(channel: video.channel)
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
.clipShape(Circle())
} }
channelLabel channelLabel

View File

@ -24,6 +24,7 @@ struct VideoCell: View {
@Default(.watchedVideoStyle) private var watchedVideoStyle @Default(.watchedVideoStyle) private var watchedVideoStyle
@Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor @Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor
@Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior @Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior
@Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing
private var navigation: NavigationModel { .shared } private var navigation: NavigationModel { .shared }
private var player: PlayerModel { .shared } private var player: PlayerModel { .shared }
@ -161,17 +162,22 @@ struct VideoCell: View {
HStack(spacing: Constants.channelDetailsStackSpacing) { HStack(spacing: Constants.channelDetailsStackSpacing) {
if !inChannelView, if !inChannelView,
let url = video.channel.thumbnailURLOrCached, showChannelAvatarInVideosListing,
video != .fixture video != .fixture
{ {
ChannelLinkView(channel: video.channel) { ChannelLinkView(channel: video.channel) {
ThumbnailView(url: url) if showChannelAvatarInVideosListing {
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) ChannelAvatarView(channel: video.channel)
.clipShape(Circle()) .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
} else {
channelLabel(badge: false)
}
} }
} }
if !channelOnThumbnail, !inChannelView { if !channelOnThumbnail,
!inChannelView
{
ChannelLinkView(channel: video.channel) { ChannelLinkView(channel: video.channel) {
channelLabel(badge: false) channelLabel(badge: false)
} }
@ -264,12 +270,9 @@ struct VideoCell: View {
if !channelOnThumbnail, !inChannelView { if !channelOnThumbnail, !inChannelView {
ChannelLinkView(channel: video.channel) { ChannelLinkView(channel: video.channel) {
HStack(spacing: Constants.channelDetailsStackSpacing) { HStack(spacing: Constants.channelDetailsStackSpacing) {
if let url = video.channel.thumbnailURLOrCached, if video != .fixture, showChannelAvatarInVideosListing {
video != .fixture ChannelAvatarView(channel: video.channel)
{
ThumbnailView(url: url)
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
.clipShape(Circle())
} }
channelLabel(badge: false) channelLabel(badge: false)
@ -295,9 +298,8 @@ struct VideoCell: View {
video != .fixture video != .fixture
{ {
ChannelLinkView(channel: video.channel) { ChannelLinkView(channel: video.channel) {
ThumbnailView(url: url) ChannelAvatarView(channel: video.channel)
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize) .frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
.clipShape(Circle())
} }
} }

View File

@ -167,6 +167,7 @@ struct ControlsBar: View {
channel: model.videoForDisplay?.channel, channel: model.videoForDisplay?.channel,
video: model.videoForDisplay video: model.videoForDisplay
) )
.id("channel-avatar-\(model.videoForDisplay?.id ?? "")")
.frame(width: barHeight - 10, height: barHeight - 10) .frame(width: barHeight - 10, height: barHeight - 10)
} }
.contextMenu { contextMenu } .contextMenu { contextMenu }
@ -176,12 +177,13 @@ struct ControlsBar: View {
channel: model.videoForDisplay?.channel, channel: model.videoForDisplay?.channel,
video: model.videoForDisplay video: model.videoForDisplay
) )
.id("channel-avatar-\(model.videoForDisplay?.id ?? "")")
#if !os(tvOS) #if !os(tvOS)
.highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil) .highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil)
.gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil) .gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil)
#endif #endif
.frame(width: barHeight - 10, height: barHeight - 10) .frame(width: barHeight - 10, height: barHeight - 10)
.contextMenu { contextMenu } .contextMenu { contextMenu }
} }
if expansionState == .full { if expansionState == .full {