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,81 +4,6 @@ import SwiftUI
struct FixtureEnvironmentObjectsModifier: ViewModifier {
func body(content: Content) -> some View {
content
.environmentObject(AccountsModel())
.environmentObject(InstancesManifest())
.environmentObject(invidious)
.environmentObject(NavigationModel())
.environmentObject(NetworkStateModel())
.environmentObject(PipedAPI())
.environmentObject(player)
.environmentObject(playerControls)
.environmentObject(PlayerTimeModel())
.environmentObject(PlaylistsModel())
.environmentObject(RecentsModel())
.environmentObject(SettingsModel())
.environmentObject(subscriptions)
.environmentObject(ThumbnailsModel())
}
private var invidious: InvidiousAPI {
let api = InvidiousAPI()
api.validInstance = true
return api
}
private var player: PlayerModel {
let player = PlayerModel()
player.currentItem = PlayerQueueItem(
Video(
app: .invidious,
videoID: "https://a/b/c",
title: "Video Name",
author: "",
length: 0,
published: "2 days ago",
views: 43434,
description: "The 14\" and 16\" MacBook Pros are incredible. I can finally retire the travel iMac.\nThat shirt! http://shop.MKBHD.com\nMacBook Pro skins: https://dbrand.com/macbooks\n\n0:00 Intro\n1:38 Top Notch Design\n2:27 Let's Talk Ports\n7:11 RIP Touchbar\n8:20 The new displays\n10:12 Living with the notch\n12:37 Performance\n19:39 Battery\n20:30 So should you get it?\n\nThe Verge Review: https://youtu.be/ftU1HzBKd5Y\nTyler Stalman Review: https://youtu.be/I10WMJV96ns\nDeveloper's tweet: https://twitter.com/softwarejameson/status/1455971162060697613?s=09&t=WbOkVKgDdcegIdyOdurSNQ&utm_source=pocket_mylist\n\nTech I'm using right now: https://www.amazon.com/shop/MKBHD\n\nIntro Track: http://youtube.com/20syl\nPlaylist of MKBHD Intro music: https://goo.gl/B3AWV5\n\nLaptop provided by Apple for review.\n\n~\nhttp://twitter.com/MKBHD\nhttp://instagram.com/MKBHD\nhttp://facebook.com/MKBHD",
channel: .init(id: "", name: "Channel Name"),
likes: 2332,
dislikes: 30,
keywords: ["Video", "Computer", "Long Long Keyword"],
chapters: [
.init(
title: "Abc",
image: URL(string: "https://pipedproxy.kavin.rocks/vi/rr2XfL_df3o/hqdefault_29633.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg%3D%3D&rs=AOn4CLDFDm9D5SvsIA7D3v5n5KZahLs_UA&host=i.ytimg.com")!,
start: 3
),
.init(
title: "Def",
image: URL(string: "https://pipedproxy.kavin.rocks/vi/rr2XfL_df3o/hqdefault_98900.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg%3D%3D&rs=AOn4CLCfjXJBJb2O2q0jT0RHIi7hARVahw&host=i.ytimg.com")!,
start: 33
)
]
)
)
#if os(iOS)
player.playerSize = .init(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
#endif
let local = (1 ... 10).map { Video.local(URL(string: "https://\($0)")!) }
let videos = Video.allFixtures + local
player.queue = videos.map { PlayerQueueItem($0) }
return player
}
private var playerControls: PlayerControlsModel {
PlayerControlsModel(presentingControls: true)
}
private var subscriptions: SubscriptionsModel {
let subscriptions = SubscriptionsModel()
subscriptions.channels = Video.allFixtures.map { $0.channel }
return subscriptions
}
}

View File

@ -84,4 +84,8 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
func hash(into hasher: inout Hasher) {
hasher.combine(username)
}
var feedCacheKey: String {
"feed-\(id)"
}
}

View File

@ -9,7 +9,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
static let basePath = "/api/v1"
@Published var account: Account!
@Published var validInstance = true
static func withAnonymousAccountForInstanceURL(_ url: URL) -> InvidiousAPI {
.init(account: Instance(app: .invidious, apiURLString: url.absoluteString).anonymousAccount)
@ -35,8 +34,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
func setAccount(_ account: Account) {
self.account = account
validInstance = account.anonymous
configure()
if !account.anonymous {
@ -45,31 +42,15 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
func validate() {
validateInstance()
validateSID()
}
func validateInstance() {
guard !validInstance else {
return
}
home?
.load()
.onSuccess { _ in
self.validInstance = true
}
.onFailure { _ in
self.validInstance = false
}
}
func validateSID() {
guard signedIn, !(account.token?.isEmpty ?? true) else {
return
}
feed?
notifications?
.load()
.onFailure { _ in
self.updateToken(force: true)
@ -273,8 +254,17 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.url, path: "/feed/subscriptions")
}
var feed: Resource? {
func feed(_ page: Int?) -> Resource? {
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
.withParam("page", String(page ?? 1))
}
var feed: Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/feed"))
}
var notifications: Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/notifications"))
}
var subscriptions: Resource? {

View File

@ -70,7 +70,7 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
return
}
feed?
feed(1)?
.load()
.onFailure { _ in
self.updateToken(force: true)
@ -262,8 +262,9 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.url, path: "/feed/subscriptions")
}
var feed: Resource? {
func feed(_ page: Int?) -> Resource? {
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
.withParam("page", String(page ?? 1))
}
var subscriptions: Resource? {

View File

@ -220,7 +220,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.instance.apiURL, path: "subscriptions")
}
var feed: Resource? {
func feed(_: Int?) -> Resource? {
resource(baseURL: account.instance.apiURL, path: "feed")
.withParam("authToken", account.token)
}

View File

@ -18,8 +18,8 @@ protocol VideosAPI {
func video(_ id: Video.ID) -> Resource
func feed(_ page: Int?) -> Resource?
var subscriptions: Resource? { get }
var feed: Resource? { get }
var home: Resource? { get }
var popular: Resource? { get }
var playlists: Resource? { get }

View File

@ -30,6 +30,10 @@ enum VideosApp: String, CaseIterable {
supportsAccounts
}
var paginatesSubscriptions: Bool {
self == .invidious
}
var supportsTrendingCategories: Bool {
self == .invidious
}

View File

@ -1,16 +1,40 @@
import Cache
import Foundation
import Logging
import SwiftyJSON
struct CacheModel {
static var shared = CacheModel()
static let jsonToDataTransformer: (JSON) -> Data = { try! $0.rawData() }
static let jsonFromDataTransformer: (Data) -> JSON = { try! JSON(data: $0) }
static let jsonTransformer = Transformer(toData: jsonToDataTransformer, fromData: jsonFromDataTransformer)
let logger = Logger(label: "stream.yattee.cache")
static let bookmarksGroup = "group.stream.yattee.app.bookmarks"
let bookmarksDefaults = UserDefaults(suiteName: Self.bookmarksGroup)
func removeAll() {
func clearBookmarks() {
guard let bookmarksDefaults else { return }
bookmarksDefaults.dictionaryRepresentation().keys.forEach(bookmarksDefaults.removeObject(forKey:))
}
func clear() {
FeedCacheModel.shared.clear()
VideosCacheModel.shared.clear()
}
var totalSize: Int {
(FeedCacheModel.shared.storage.totalDiskStorageSize ?? 0) +
(VideosCacheModel.shared.storage.totalDiskStorageSize ?? 0)
}
var totalSizeFormatted: String {
totalSizeFormatter.string(fromByteCount: Int64(totalSize))
}
private var totalSizeFormatter: ByteCountFormatter {
.init()
}
}

View File

@ -0,0 +1,62 @@
import Cache
import Foundation
import Logging
import SwiftyJSON
struct FeedCacheModel {
static let shared = FeedCacheModel()
let logger = Logger(label: "stream.yattee.cache.feed")
static let diskConfig = DiskConfig(name: "feed")
static let memoryConfig = MemoryConfig()
let storage = try! Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
transformer: CacheModel.jsonTransformer
)
func storeFeed(account: Account, videos: [Video]) {
let date = dateFormatter.string(from: Date())
logger.info("caching feed \(account.feedCacheKey) -- \(date)")
let feedTimeObject: JSON = ["date": date]
let videosObject: JSON = ["videos": videos.map(\.json).map(\.object)]
try? storage.setObject(feedTimeObject, forKey: feedTimeCacheKey(account.feedCacheKey))
try? storage.setObject(videosObject, forKey: account.feedCacheKey)
}
func retrieveFeed(account: Account) -> [Video] {
logger.info("retrieving cache for \(account.feedCacheKey)")
if let json = try? storage.object(forKey: account.feedCacheKey),
let videos = json.dictionaryValue["videos"]
{
return videos.arrayValue.map { Video.from($0) }
}
return []
}
func getFeedTime(account: Account) -> Date? {
if let json = try? storage.object(forKey: feedTimeCacheKey(account.feedCacheKey)),
let string = json.dictionaryValue["date"]?.string,
let date = dateFormatter.date(from: string)
{
return date
}
return nil
}
func clear() {
try? storage.removeAll()
}
private var dateFormatter: ISO8601DateFormatter {
.init()
}
private func feedTimeCacheKey(_ feedCacheKey: String) -> String {
"\(feedCacheKey)-feedTime"
}
}

View File

@ -7,31 +7,31 @@ struct VideosCacheModel {
static let shared = VideosCacheModel()
let logger = Logger(label: "stream.yattee.cache.videos")
static let jsonToDataTransformer: (JSON) -> Data = { try! $0.rawData() }
static let jsonFromDataTransformer: (Data) -> JSON = { try! JSON(data: $0) }
static let jsonTransformer = Transformer(toData: jsonToDataTransformer, fromData: jsonFromDataTransformer)
static let diskConfig = DiskConfig(name: "videos")
static let memoryConfig = MemoryConfig()
static let videosStorageDiskConfig = DiskConfig(name: "videos")
static let vidoesStorageMemoryConfig = MemoryConfig()
let videosStorage = try! Storage<String, JSON>(
diskConfig: Self.videosStorageDiskConfig,
memoryConfig: Self.vidoesStorageMemoryConfig,
transformer: Self.jsonTransformer
let storage = try! Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
transformer: CacheModel.jsonTransformer
)
func storeVideo(_ video: Video) {
logger.info("caching \(video.cacheKey)")
try? videosStorage.setObject(video.json, forKey: video.cacheKey)
try? storage.setObject(video.json, forKey: video.cacheKey)
}
func retrieveVideo(_ cacheKey: String) -> Video? {
logger.info("retrieving cache for \(cacheKey)")
if let json = try? videosStorage.object(forKey: cacheKey) {
if let json = try? storage.object(forKey: cacheKey) {
return Video.from(json)
}
return nil
}
func clear() {
try? storage.removeAll()
}
}

View File

@ -103,7 +103,7 @@ extension PlayerModel {
func removeHistory() {
removeAllWatches()
CacheModel.shared.removeAll()
CacheModel.shared.clearBookmarks()
}
func removeWatch(_ watch: Watch) {

View File

@ -279,7 +279,7 @@ extension PlayerModel {
}
func loadQueueVideoDetails(_ item: PlayerQueueItem) {
guard !accounts.current.isNil, !item.hasDetailsLoaded else { return }
guard !accounts.current.isNil, !item.hasDetailsLoaded, let video = item.video else { return }
let videoID = item.video?.videoID ?? item.videoID
@ -292,7 +292,7 @@ extension PlayerModel {
return
}
playerAPI(item.video).loadDetails(item, completionHandler: { [weak self] newItem in
playerAPI(video)?.loadDetails(item, completionHandler: { [weak self] newItem in
guard let self else { return }
self.queue.filter { $0.videoID == item.videoID }.forEach { item in

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()
}
}

View File

@ -550,6 +550,11 @@
377F9F7B294403F20043F856 /* VideosCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377F9F7A294403F20043F856 /* VideosCacheModel.swift */; };
377F9F7C294403F20043F856 /* VideosCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377F9F7A294403F20043F856 /* VideosCacheModel.swift */; };
377F9F7D294403F20043F856 /* VideosCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377F9F7A294403F20043F856 /* VideosCacheModel.swift */; };
377F9F7F2944175F0043F856 /* FeedCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377F9F7E2944175F0043F856 /* FeedCacheModel.swift */; };
377F9F802944175F0043F856 /* FeedCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377F9F7E2944175F0043F856 /* FeedCacheModel.swift */; };
377F9F812944175F0043F856 /* FeedCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377F9F7E2944175F0043F856 /* FeedCacheModel.swift */; };
377F9F83294417B40043F856 /* Cache in Frameworks */ = {isa = PBXBuildFile; productRef = 377F9F82294417B40043F856 /* Cache */; };
377F9F85294417FA0043F856 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377F9F84294417FA0043F856 /* SwiftyJSON */; };
377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7D4267A080300A6BBAF /* SwiftyJSON */; };
377FC7DB267A080300A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7DA267A080300A6BBAF /* Logging */; };
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; };
@ -818,6 +823,12 @@
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */; };
37E6D79C2944AE1A00550C3D /* SubscriptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E6D79B2944AE1A00550C3D /* SubscriptionsViewModel.swift */; };
37E6D79D2944AE1A00550C3D /* SubscriptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E6D79B2944AE1A00550C3D /* SubscriptionsViewModel.swift */; };
37E6D79E2944AE1A00550C3D /* SubscriptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E6D79B2944AE1A00550C3D /* SubscriptionsViewModel.swift */; };
37E6D7A02944CD3800550C3D /* CacheStatusHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */; };
37E6D7A12944CD3800550C3D /* CacheStatusHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */; };
37E6D7A22944CD3800550C3D /* CacheStatusHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */; };
37E70923271CD43000D34DDE /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E70922271CD43000D34DDE /* WelcomeScreen.swift */; };
37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E70922271CD43000D34DDE /* WelcomeScreen.swift */; };
37E70925271CD43000D34DDE /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E70922271CD43000D34DDE /* WelcomeScreen.swift */; };
@ -1229,6 +1240,7 @@
377ABC47286E5887009C986F /* Sequence+Unique.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Unique.swift"; sourceTree = "<group>"; };
377E17132928265900894889 /* ListRowSeparator+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListRowSeparator+Backport.swift"; sourceTree = "<group>"; };
377F9F7A294403F20043F856 /* VideosCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosCacheModel.swift; sourceTree = "<group>"; };
377F9F7E2944175F0043F856 /* FeedCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCacheModel.swift; sourceTree = "<group>"; };
377FF88A291A60310028EB0B /* OpenVideosModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenVideosModel.swift; sourceTree = "<group>"; };
377FF88E291A99580028EB0B /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
37824309291E58D6005DEC1C /* Open in Yattee.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Open in Yattee.entitlements"; sourceTree = "<group>"; };
@ -1348,6 +1360,8 @@
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; };
37E2EEAA270656EC00170416 /* BrowserPlayerControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPlayerControls.swift; sourceTree = "<group>"; };
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = "<group>"; };
37E6D79B2944AE1A00550C3D /* SubscriptionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsViewModel.swift; sourceTree = "<group>"; };
37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStatusHeader.swift; sourceTree = "<group>"; };
37E70922271CD43000D34DDE /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = "<group>"; };
37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettingsButton.swift; sourceTree = "<group>"; };
37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsOverlay.swift; sourceTree = "<group>"; };
@ -1397,7 +1411,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
377F9F85294417FA0043F856 /* SwiftyJSON in Frameworks */,
37DA0F20291DD6B8009B38CF /* Logging in Frameworks */,
377F9F83294417B40043F856 /* Cache in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1744,9 +1760,9 @@
37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */,
3784B23C2728B85300B09468 /* ShareButton.swift */,
376B2E0626F920D600B1D64D /* SignInRequiredView.swift */,
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
37E70922271CD43000D34DDE /* WelcomeScreen.swift */,
37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */,
);
path = Views;
sourceTree = "<group>";
@ -1977,6 +1993,7 @@
isa = PBXGroup;
children = (
37F5E8B9291BEF69006C15F5 /* CacheModel.swift */,
377F9F7E2944175F0043F856 /* FeedCacheModel.swift */,
377F9F7A294403F20043F856 /* VideosCacheModel.swift */,
);
path = Cache;
@ -2124,6 +2141,7 @@
371AAE2626CEBF1600901972 /* Playlists */,
3782B95527557A2400990149 /* Search */,
37484C1726FC836500287258 /* Settings */,
37E6D79A2944ADCB00550C3D /* Subscriptions */,
371AAE2526CEBF0B00901972 /* Trending */,
371AAE2726CEBF4700901972 /* Videos */,
371AAE2826CEC7D900901972 /* Views */,
@ -2294,6 +2312,15 @@
path = Vendor;
sourceTree = "<group>";
};
37E6D79A2944ADCB00550C3D /* Subscriptions */ = {
isa = PBXGroup;
children = (
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
37E6D79B2944AE1A00550C3D /* SubscriptionsViewModel.swift */,
);
path = Subscriptions;
sourceTree = "<group>";
};
37EBD8C227AF0D7C00F1C24B /* Backends */ = {
isa = PBXGroup;
children = (
@ -2368,6 +2395,8 @@
name = "Open in Yattee";
packageProductDependencies = (
37DA0F1F291DD6B8009B38CF /* Logging */,
377F9F82294417B40043F856 /* Cache */,
377F9F84294417FA0043F856 /* SwiftyJSON */,
);
productName = "Open in Yattee";
productReference = 37095E7F291DC85400301883 /* Open in Yattee.appex */;
@ -2872,6 +2901,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
37E6D79C2944AE1A00550C3D /* SubscriptionsViewModel.swift in Sources */,
374710052755291C00CE0F87 /* SearchTextField.swift in Sources */,
37494EA529200B14000DF176 /* DocumentsView.swift in Sources */,
374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */,
@ -2920,6 +2950,7 @@
37D2E0D028B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */,
3727B74A27872A920021C15E /* VisualEffectBlur-iOS.swift in Sources */,
3709528829283A21001ECA40 /* RecentDocumentsView.swift in Sources */,
377F9F7F2944175F0043F856 /* FeedCacheModel.swift in Sources */,
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
374AB3D728BCAF0000DF56FB /* SeekModel.swift in Sources */,
37130A5F277657300033018A /* PersistenceController.swift in Sources */,
@ -3015,6 +3046,7 @@
37169AA22729D98A0011DE61 /* InstancesBridge.swift in Sources */,
37C3A24527235DA70087A57A /* ChannelPlaylist.swift in Sources */,
37030FFF27B04DCC00ECDDAA /* PlayerControls.swift in Sources */,
37E6D7A02944CD3800550C3D /* CacheStatusHeader.swift in Sources */,
374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
375F7410289DC35A00747050 /* PlayerBackendView.swift in Sources */,
37FB28412721B22200A57617 /* ContentItem.swift in Sources */,
@ -3268,6 +3300,7 @@
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
37E6D79D2944AE1A00550C3D /* SubscriptionsViewModel.swift in Sources */,
37732FF52703D32400F04329 /* Sidebar.swift in Sources */,
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
377ABC49286E5887009C986F /* Sequence+Unique.swift in Sources */,
@ -3296,6 +3329,7 @@
37732FF12703A26300F04329 /* AccountValidationStatus.swift in Sources */,
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */,
37E6D7A12944CD3800550C3D /* CacheStatusHeader.swift in Sources */,
37B2631B2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */,
376BE50C27349108009AD608 /* BrowsingSettings.swift in Sources */,
@ -3312,6 +3346,7 @@
374924E4292141320017D862 /* InspectorView.swift in Sources */,
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
377F9F802944175F0043F856 /* FeedCacheModel.swift in Sources */,
373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */,
37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */,
37130A60277657300033018A /* PersistenceController.swift in Sources */,
@ -3475,6 +3510,7 @@
37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
3718B9A12921A9640003DB2E /* VideoDetails.swift in Sources */,
378AE93D274EDFB3006A4EE1 /* Backport.swift in Sources */,
377F9F812944175F0043F856 /* FeedCacheModel.swift in Sources */,
37130A5D277657090033018A /* Yattee.xcdatamodeld in Sources */,
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
37AAF29226740715007FC770 /* Channel.swift in Sources */,
@ -3589,11 +3625,13 @@
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */,
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
37E6D7A22944CD3800550C3D /* CacheStatusHeader.swift in Sources */,
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
37F0F4F0286F734400C06C2E /* AdvancedSettings.swift in Sources */,
373197DA2732060100EF734F /* RelatedView.swift in Sources */,
37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */,
377ABC4A286E5887009C986F /* Sequence+Unique.swift in Sources */,
37E6D79E2944AE1A00550C3D /* SubscriptionsViewModel.swift in Sources */,
37D4B19926717E1500C925CA /* Video.swift in Sources */,
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
@ -3699,6 +3737,7 @@
MARKETING_VERSION = 1.4.3;
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.app.Open-in-Yattee";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -4732,6 +4771,16 @@
package = 374D11E52943C56300CB4350 /* XCRemoteSwiftPackageReference "Cache" */;
productName = Cache;
};
377F9F82294417B40043F856 /* Cache */ = {
isa = XCSwiftPackageProductDependency;
package = 374D11E52943C56300CB4350 /* XCRemoteSwiftPackageReference "Cache" */;
productName = Cache;
};
377F9F84294417FA0043F856 /* SwiftyJSON */ = {
isa = XCSwiftPackageProductDependency;
package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
productName = SwiftyJSON;
};
377FC7D4267A080300A6BBAF /* SwiftyJSON */ = {
isa = XCSwiftPackageProductDependency;
package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */;