Feed cache

This commit is contained in:
Arkadiusz Fal
2022-12-10 03:01:59 +01:00
parent eae04c9382
commit 971beddc8d
24 changed files with 484 additions and 237 deletions

View File

@@ -4,4 +4,11 @@ import SwiftUI
struct Constants {
static let yatteeProtocol = "yattee://"
static let overlayAnimation = Animation.linear(duration: 0.2)
static var progressViewScale: Double {
#if os(macOS)
0.4
#else
0.6
#endif
}
}

View File

@@ -53,13 +53,29 @@ struct FavoriteItemView: View {
#endif
.onAppear {
resource?.addObserver(store)
resource?.loadIfNeeded()
if item.section == .subscriptions {
cacheFeed(resource?.loadIfNeeded())
} else {
resource?.loadIfNeeded()
}
}
}
}
.onChange(of: accounts.current) { _ in
resource?.addObserver(store)
resource?.load()
if item.section == .subscriptions {
cacheFeed(resource?.load())
} else {
resource?.load()
}
}
}
private func cacheFeed(_ request: Request?) {
request?.onSuccess { response in
if let videos: [Video] = response.typedContent() {
FeedCacheModel.shared.storeFeed(account: accounts.current, videos: videos)
}
}
}
@@ -78,7 +94,7 @@ struct FavoriteItemView: View {
switch item.section {
case .subscriptions:
if accounts.app.supportsSubscriptions {
return accounts.api.feed
return accounts.api.feed(1)
}
case .popular:

View File

@@ -82,7 +82,7 @@ struct CommentView: View {
repliesButton
ProgressView()
.scaleEffect(progressViewScale, anchor: .center)
.scaleEffect(Constants.progressViewScale, anchor: .center)
.opacity(repliesID == comment.id && !comments.repliesLoaded ? 1 : 0)
.frame(maxHeight: 0)
}
@@ -200,14 +200,6 @@ struct CommentView: View {
#endif
}
private var progressViewScale: Double {
#if os(macOS)
0.4
#else
0.6
#endif
}
private var repliesList: some View {
Group {
let last = comments.replies.last

View File

@@ -89,7 +89,7 @@ struct SearchView: View {
filtersMenu
}
SearchTextField(favoriteItem: $favoriteItem)
SearchTextField()
}
#endif
}

View File

@@ -11,9 +11,9 @@ struct AdvancedSettings: View {
@State private var countries = [String]()
@State private var filesToShare = [MPVClient.logFile]
@State private var presentingInstanceForm = false
@State private var presentingShareSheet = false
@State private var savedFormInstanceID: Instance.ID?
private var settings = SettingsModel.shared
var body: some View {
VStack(alignment: .leading) {
@@ -36,9 +36,6 @@ struct AdvancedSettings: View {
.onChange(of: countryOfPublicInstances) { newCountry in
InstancesManifest.shared.setPublicAccount(newCountry, asCurrent: AccountsModel.shared.current?.isPublic ?? true)
}
.sheet(isPresented: $presentingInstanceForm) {
InstanceForm(savedInstanceID: $savedFormInstanceID)
}
#if os(tvOS)
.frame(maxWidth: 1000)
#endif
@@ -87,6 +84,11 @@ struct AdvancedSettings: View {
logButton
}
}
Section(header: SettingsHeader(text: "Cache")) {
clearCacheButton
cacheSize
}
}
@ViewBuilder var mpvFooter: some View {
@@ -128,13 +130,27 @@ struct AdvancedSettings: View {
}
#endif
private var addInstanceButton: some View {
private var clearCacheButton: some View {
Button {
presentingInstanceForm = true
settings.presentAlert(
Alert(
title: Text(
"Are you sure you want to clear cache?"
),
primaryButton: .destructive(Text("Clear"), action: CacheModel.shared.clear),
secondaryButton: .cancel()
)
)
} label: {
Label("Add Location...", systemImage: "plus")
Text("Clear all")
.foregroundColor(.red)
}
}
var cacheSize: some View {
Text(String(format: "Total size: %@", CacheModel.shared.totalSizeFormatted))
.foregroundColor(.secondary)
}
}
struct AdvancedSettings_Previews: PreviewProvider {

View File

@@ -164,7 +164,9 @@ struct HistorySettings: View {
struct HistorySettings_Previews: PreviewProvider {
static var previews: some View {
HistorySettings()
.injectFixtureEnvironmentObjects()
VStack(alignment: .leading) {
HistorySettings()
}
.frame(minHeight: 500)
}
}

View File

@@ -231,7 +231,7 @@ struct SettingsView: View {
case .locations:
return 600
case .advanced:
return 250
return 350
case .help:
return 650
}

View File

@@ -0,0 +1,79 @@
import Siesta
import SwiftUI
struct SubscriptionsView: View {
@ObservedObject private var model = SubscriptionsViewModel.shared
@ObservedObject private var accounts = AccountsModel.shared
var videos: [ContentItem] {
ContentItem.array(of: model.videos)
}
var body: some View {
BrowserPlayerControls {
SignInRequiredView(title: "Subscriptions".localized()) {
VerticalCells(items: videos) {
HStack {
Spacer()
#if os(tvOS)
Button {
model.loadResources(force: true)
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
.imageScale(.small)
.font(.caption2)
}
.padding(.horizontal, 10)
#endif
CacheStatusHeader(refreshTime: model.formattedFeedTime, isLoading: model.isLoading)
}
.environment(\.loadMoreContentHandler) { model.loadNextPage() }
.onAppear {
model.loadResources()
}
.onChange(of: accounts.current) { _ in
model.reset()
model.loadResources(force: true)
}
#if os(iOS)
.refreshControl { refreshControl in
model.loadResources(force: true) {
refreshControl.endRefreshing()
}
}
.backport
.refreshable {
await model.loadResources(force: true)
}
#endif
}
}
#if !os(tvOS)
.background(
Button("Refresh") {
model.loadResources(force: true)
}
.keyboardShortcut("r")
.opacity(0)
)
#endif
#if os(iOS)
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif
#if !os(macOS)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
model.loadResources()
}
#endif
}
}
struct SubscriptonsView_Previews: PreviewProvider {
static var previews: some View {
SubscriptionsView()
}
}

View File

@@ -0,0 +1,148 @@
import Foundation
import Siesta
final class SubscriptionsViewModel: ObservableObject {
static let shared = SubscriptionsViewModel()
@Published var isLoading = false
@Published var videos = [Video]()
@Published private var page = 1
private var accounts = AccountsModel.shared
var feed: Resource? {
accounts.api.feed(page)
}
func loadResources(force: Bool = false, onCompletion: @escaping () -> Void = {}) {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self else { return }
if !force {
self.loadCachedFeed()
}
if self.accounts.app == .invidious {
// Invidious for some reason won't refresh feed until homepage is loaded
DispatchQueue.main.async { [weak self] in
guard let self, let home = self.accounts.api.home else { return }
self.request(home, force: force)?
.onCompletion { _ in
self.loadFeed(force: force, onCompletion: onCompletion)
}
}
} else {
self.loadFeed(force: force, onCompletion: onCompletion)
}
}
}
func loadFeed(force: Bool = false, paginating: Bool = false, onCompletion: @escaping () -> Void = {}) {
DispatchQueue.main.async { [weak self] in
guard let self,
!self.isLoading,
let account = self.accounts.current
else {
self?.isLoading = false
onCompletion()
return
}
if paginating {
self.page += 1
} else {
self.page = 1
}
let feedBeforeLoad = self.feed
var request: Request?
if let feedBeforeLoad {
request = self.request(feedBeforeLoad, force: force)
}
if request != nil {
self.isLoading = true
}
request?
.onCompletion { _ in
self.isLoading = false
onCompletion()
}
.onSuccess { response in
if let videos: [Video] = response.typedContent() {
if paginating {
self.videos.append(contentsOf: videos)
} else {
self.videos = videos
FeedCacheModel.shared.storeFeed(account: account, videos: self.videos)
}
}
}
.onFailure { error in
NavigationModel.shared.presentAlert(title: "Could not refresh Subscriptions", message: error.userMessage)
}
}
}
func reset() {
videos.removeAll()
page = 1
}
func loadNextPage() {
guard accounts.app.paginatesSubscriptions, !isLoading else { return }
loadFeed(force: true, paginating: true)
}
var feedTime: Date? {
if let account = accounts.current {
return FeedCacheModel.shared.getFeedTime(account: account)
}
return nil
}
var formattedFeedTime: String {
if let feedTime {
let isSameDay = Calendar(identifier: .iso8601).isDate(feedTime, inSameDayAs: Date())
let formatter = isSameDay ? dateFormatterForTimeOnly : dateFormatter
return formatter.string(from: feedTime)
}
return ""
}
private func loadCachedFeed() {
let cache = FeedCacheModel.shared.retrieveFeed(account: accounts.current)
if !cache.isEmpty {
DispatchQueue.main.async { [weak self] in
self?.videos = cache
}
}
}
private var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}
private var dateFormatterForTimeOnly: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .medium
return formatter
}
private func request(_ resource: Resource, force: Bool = false) -> Request? {
if force {
return resource.load()
}
return resource.loadIfNeeded()
}
}

View File

@@ -0,0 +1,24 @@
import SwiftUI
struct CacheStatusHeader: View {
var refreshTime: String
var isLoading = false
var body: some View {
HStack(spacing: 6) {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(Constants.progressViewScale, anchor: .center)
.opacity(isLoading ? 1 : 0)
Text(refreshTime)
}
.font(.caption)
.foregroundColor(.secondary)
}
}
struct CacheStatusHeader_Previews: PreviewProvider {
static var previews: some View {
CacheStatusHeader(refreshTime: "15:10:20")
}
}

View File

@@ -1,96 +0,0 @@
import Siesta
import SwiftUI
struct SubscriptionsView: View {
@StateObject private var store = Store<[Video]>()
@ObservedObject private var accounts = AccountsModel.shared
var feed: Resource? {
accounts.api.feed
}
var videos: [ContentItem] {
ContentItem.array(of: store.collection)
}
var body: some View {
BrowserPlayerControls {
SignInRequiredView(title: "Subscriptions".localized()) {
VerticalCells(items: videos)
.onAppear {
loadResources()
}
.onChange(of: accounts.current) { _ in
loadResources(force: true)
}
#if os(iOS)
.refreshControl { refreshControl in
loadResources(force: true) {
refreshControl.endRefreshing()
}
}
.backport
.refreshable {
await loadResources(force: true)
}
#endif
}
}
#if !os(tvOS)
.background(
Button("Refresh") {
loadResources(force: true)
}
.keyboardShortcut("r")
.opacity(0)
)
#endif
#if os(iOS)
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
#endif
#if !os(macOS)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
loadResources()
}
#endif
}
private func loadResources(force: Bool = false, onCompletion: @escaping () -> Void = {}) {
feed?.addObserver(store)
if accounts.app == .invidious {
// Invidious for some reason won't refresh feed until homepage is loaded
if let request = force ? accounts.api.home?.load() : accounts.api.home?.loadIfNeeded() {
request.onSuccess { _ in
loadFeed(force: force, onCompletion: onCompletion)
}
} else {
loadFeed(force: force, onCompletion: onCompletion)
}
} else {
loadFeed(force: force, onCompletion: onCompletion)
}
}
private func loadFeed(force: Bool = false, onCompletion: @escaping () -> Void = {}) {
if let request = force ? feed?.load() : feed?.loadIfNeeded() {
request.onCompletion { _ in
onCompletion()
}
.onFailure { error in
NavigationModel.shared.presentAlert(title: "Could not refresh Subscriptions", message: error.userMessage)
}
} else {
onCompletion()
}
}
}
struct SubscriptonsView_Previews: PreviewProvider {
static var previews: some View {
SubscriptionsView()
.injectFixtureEnvironmentObjects()
}
}