mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
Persist author cache to disk for instant channel info across restarts
Back the in-memory authorCache with a JSON file in ~/Library/Caches/AuthorCache/. Disk is lazy-loaded on first lookup and saved asynchronously on each cache update. Capped at 500 entries to prevent unbounded growth. - Cache author data from video detail API responses (PlayerService, VideoInfoView) - Replace ChannelView's private CachedChannelHeader with shared CachedChannelData - Enrich author with cached avatar/subscriber count in VideoChannelRow, TVDetailsPanel, VideoInfoView
This commit is contained in:
@@ -8,25 +8,6 @@
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
/// Cached channel data for showing header immediately while loading.
|
||||
private struct CachedChannelHeader {
|
||||
let name: String
|
||||
let thumbnailURL: URL?
|
||||
let bannerURL: URL?
|
||||
|
||||
init(from subscription: Subscription) {
|
||||
name = subscription.name
|
||||
thumbnailURL = subscription.avatarURL
|
||||
bannerURL = subscription.bannerURL
|
||||
}
|
||||
|
||||
init(from recentChannel: RecentChannel) {
|
||||
name = recentChannel.name
|
||||
thumbnailURL = recentChannel.thumbnailURLString.flatMap { URL(string: $0) }
|
||||
bannerURL = nil // RecentChannel doesn't store banner
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelView: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@@ -52,7 +33,7 @@ struct ChannelView: View {
|
||||
@State private var showingUnsubscribeConfirmation = false
|
||||
@State private var scrollOffset: CGFloat = 0
|
||||
@State private var scrollToTop: Bool = false
|
||||
@State private var cachedHeader: CachedChannelHeader?
|
||||
@State private var cachedHeader: CachedChannelData?
|
||||
|
||||
// View options (persisted)
|
||||
@AppStorage("channel.layout") private var layout: VideoListLayout = .list
|
||||
@@ -393,7 +374,7 @@ struct ChannelView: View {
|
||||
|
||||
/// Shows cached header with a spinner below while loading full channel data.
|
||||
@ViewBuilder
|
||||
private func loadingContent(_ cached: CachedChannelHeader) -> some View {
|
||||
private func loadingContent(_ cached: CachedChannelData) -> some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
@@ -1526,11 +1507,7 @@ struct ChannelView: View {
|
||||
isSubscribed = subscription != nil
|
||||
|
||||
// Load cached header data for immediate display
|
||||
if let subscription {
|
||||
cachedHeader = CachedChannelHeader(from: subscription)
|
||||
} else if let recentChannel = appEnvironment.dataManager.recentChannelEntry(forChannelID: channelID) {
|
||||
cachedHeader = CachedChannelHeader(from: recentChannel)
|
||||
}
|
||||
cachedHeader = CachedChannelData.load(for: channelID, using: appEnvironment.dataManager)
|
||||
|
||||
// Fetch channel and videos independently to handle partial failures gracefully
|
||||
async let channelTask: Result<Channel, Error> = await {
|
||||
@@ -1576,7 +1553,7 @@ struct ChannelView: View {
|
||||
id: ChannelID(source: source, channelID: channelID),
|
||||
name: cached.name,
|
||||
description: nil,
|
||||
subscriberCount: nil,
|
||||
subscriberCount: cached.subscriberCount,
|
||||
thumbnailURL: cached.thumbnailURL,
|
||||
bannerURL: cached.bannerURL
|
||||
)
|
||||
|
||||
@@ -92,6 +92,8 @@ struct VideoStatsRow: View {
|
||||
|
||||
/// Displays channel info with avatar, name, subscriber count, and context menu.
|
||||
struct VideoChannelRow: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
|
||||
let author: Author
|
||||
let source: ContentSource
|
||||
let yatteeServerURL: URL?
|
||||
@@ -101,6 +103,12 @@ struct VideoChannelRow: View {
|
||||
var showSubscriberCount: Bool = true
|
||||
var isLoadingDetails: Bool = false
|
||||
|
||||
/// Author enriched with cached channel data (avatar, subscriber count) from local stores.
|
||||
private var enrichedAuthor: Author {
|
||||
guard let dataManager = appEnvironment?.dataManager else { return author }
|
||||
return author.enriched(using: dataManager)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
if let onChannelTap {
|
||||
@@ -129,21 +137,21 @@ struct VideoChannelRow: View {
|
||||
private var channelContent: some View {
|
||||
HStack(spacing: 10) {
|
||||
ChannelAvatarView(
|
||||
author: author,
|
||||
author: enrichedAuthor,
|
||||
size: 40,
|
||||
yatteeServerURL: yatteeServerURL,
|
||||
source: source
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(author.name)
|
||||
Text(enrichedAuthor.name)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
|
||||
if showSubscriberCount {
|
||||
Group {
|
||||
if let subscribers = author.formattedSubscriberCount {
|
||||
if let subscribers = enrichedAuthor.formattedSubscriberCount {
|
||||
Text(subscribers)
|
||||
} else if isLoadingDetails && video.supportsAPIStats {
|
||||
Text("1.2M subscribers")
|
||||
|
||||
@@ -152,10 +152,17 @@ struct TVDetailsPanel: View {
|
||||
|
||||
// MARK: - Channel Row
|
||||
|
||||
/// Author enriched with cached channel data (avatar, subscriber count) from local stores.
|
||||
private var enrichedAuthor: Author? {
|
||||
guard let video else { return nil }
|
||||
guard let dataManager = appEnvironment?.dataManager else { return video.author }
|
||||
return video.author.enriched(using: dataManager)
|
||||
}
|
||||
|
||||
private var channelRow: some View {
|
||||
HStack(spacing: 16) {
|
||||
// Channel avatar
|
||||
if let thumbnailURL = video?.author.thumbnailURL {
|
||||
if let thumbnailURL = enrichedAuthor?.thumbnailURL {
|
||||
AsyncImage(url: thumbnailURL) { image in
|
||||
image
|
||||
.resizable()
|
||||
@@ -179,12 +186,12 @@ struct TVDetailsPanel: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Channel name
|
||||
Text(video?.author.name ?? "")
|
||||
Text(enrichedAuthor?.name ?? "")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
// Subscriber count
|
||||
if let subscriberCount = video?.author.subscriberCount {
|
||||
if let subscriberCount = enrichedAuthor?.subscriberCount {
|
||||
Text("channel.subscriberCount \(CountFormatter.compact(subscriberCount))")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
|
||||
@@ -765,21 +765,22 @@ struct VideoInfoView: View {
|
||||
|
||||
/// Channel row content used in both tappable and non-tappable variants
|
||||
private func channelRowContent(for video: Video) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
let enrichedAuthor = appEnvironment.map { video.author.enriched(using: $0.dataManager) } ?? video.author
|
||||
return HStack(spacing: 10) {
|
||||
ChannelAvatarView(
|
||||
author: video.author,
|
||||
author: enrichedAuthor,
|
||||
size: 40,
|
||||
yatteeServerURL: yatteeServerURL,
|
||||
source: video.authorSource
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(video.author.name)
|
||||
Text(enrichedAuthor.name)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
|
||||
if let subscribers = video.author.formattedSubscriberCount {
|
||||
if let subscribers = enrichedAuthor.formattedSubscriberCount {
|
||||
Text(subscribers)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -1784,6 +1785,7 @@ struct VideoInfoView: View {
|
||||
// Use extractURL method - just use the video part
|
||||
let (fullVideo, _, _) = try await contentService.extractURL(originalURL, instance: instance)
|
||||
loadedVideoDetails[videoID] = fullVideo
|
||||
CachedChannelData.cacheAuthor(fullVideo.author)
|
||||
} catch {
|
||||
// Fail silently - use partial video data we have
|
||||
}
|
||||
@@ -1807,6 +1809,7 @@ struct VideoInfoView: View {
|
||||
instance: instance
|
||||
)
|
||||
loadedVideoDetails[videoID] = fullVideo
|
||||
CachedChannelData.cacheAuthor(fullVideo.author)
|
||||
} catch {
|
||||
// Fail silently - just use the partial video data we have
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user