mirror of
https://github.com/yattee/yattee.git
synced 2024-11-09 15:58:20 +00:00
Feed cache
This commit is contained in:
parent
eae04c9382
commit
971beddc8d
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,4 +84,8 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(username)
|
||||
}
|
||||
|
||||
var feedCacheKey: String {
|
||||
"feed-\(id)"
|
||||
}
|
||||
}
|
||||
|
@ -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? {
|
||||
|
@ -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? {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -30,6 +30,10 @@ enum VideosApp: String, CaseIterable {
|
||||
supportsAccounts
|
||||
}
|
||||
|
||||
var paginatesSubscriptions: Bool {
|
||||
self == .invidious
|
||||
}
|
||||
|
||||
var supportsTrendingCategories: Bool {
|
||||
self == .invidious
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
62
Model/Cache/FeedCacheModel.swift
Normal file
62
Model/Cache/FeedCacheModel.swift
Normal 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"
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ extension PlayerModel {
|
||||
|
||||
func removeHistory() {
|
||||
removeAllWatches()
|
||||
CacheModel.shared.removeAll()
|
||||
CacheModel.shared.clearBookmarks()
|
||||
}
|
||||
|
||||
func removeWatch(_ watch: Watch) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -89,7 +89,7 @@ struct SearchView: View {
|
||||
filtersMenu
|
||||
}
|
||||
|
||||
SearchTextField(favoriteItem: $favoriteItem)
|
||||
SearchTextField()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -231,7 +231,7 @@ struct SettingsView: View {
|
||||
case .locations:
|
||||
return 600
|
||||
case .advanced:
|
||||
return 250
|
||||
return 350
|
||||
case .help:
|
||||
return 650
|
||||
}
|
||||
|
79
Shared/Subscriptions/SubscriptionsView.swift
Normal file
79
Shared/Subscriptions/SubscriptionsView.swift
Normal 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()
|
||||
}
|
||||
}
|
148
Shared/Subscriptions/SubscriptionsViewModel.swift
Normal file
148
Shared/Subscriptions/SubscriptionsViewModel.swift
Normal 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()
|
||||
}
|
||||
}
|
24
Shared/Views/CacheStatusHeader.swift
Normal file
24
Shared/Views/CacheStatusHeader.swift
Normal 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")
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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" */;
|
||||
|
Loading…
Reference in New Issue
Block a user