mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
parent
d58026bcef
commit
f1e132a909
@ -109,17 +109,22 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
content.json.arrayValue.map(self.extractChannel)
|
content.json.arrayValue.map(self.extractChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
|
configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPage in
|
||||||
self.extractChannel(from: content.json)
|
self.extractChannelPage(from: content.json, forceNotLast: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTransformer(pathPattern("channels/*/videos"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPage in
|
||||||
|
self.extractChannelPage(from: content.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channels/*/latest"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("channels/*/latest"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.dictionaryValue["videos"]?.arrayValue.map(self.extractVideo) ?? []
|
content.json.dictionaryValue["videos"]?.arrayValue.map(self.extractVideo) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channels/*/playlists"), requestMethods: [.get]) { (content: Entity<JSON>) -> [ContentItem] in
|
["latest", "playlists", "streams", "shorts", "channels", "videos"].forEach { type in
|
||||||
let playlists = (content.json.dictionaryValue["playlists"]?.arrayValue ?? []).compactMap { self.extractChannelPlaylist(from: $0) }
|
configureTransformer(pathPattern("channels/*/\(type)"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPage in
|
||||||
return ContentItem.array(of: playlists)
|
self.extractChannelPage(from: content.json)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPlaylist in
|
configureTransformer(pathPattern("playlists/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> ChannelPlaylist in
|
||||||
@ -266,11 +271,18 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
.onCompletion { _ in onCompletion() }
|
.onCompletion { _ in onCompletion() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func channel(_ id: String, contentType: Channel.ContentType, data _: String?) -> Resource {
|
func channel(_ id: String, contentType: Channel.ContentType, data _: String?, page: String?) -> Resource {
|
||||||
if contentType == .playlists {
|
if page.isNil, contentType == .videos {
|
||||||
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)/playlists"))
|
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)"))
|
||||||
}
|
}
|
||||||
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)"))
|
|
||||||
|
var resource = resource(baseURL: account.url, path: basePathAppending("channels/\(id)/\(contentType.invidiousID)"))
|
||||||
|
|
||||||
|
if let page, !page.isEmpty {
|
||||||
|
resource = resource.withParam("continuation", page)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelByName(_: String) -> Resource? {
|
func channelByName(_: String) -> Resource? {
|
||||||
@ -504,6 +516,14 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
thumbnailURL = "\(accountUrlComponents.scheme ?? "https"):\(thumbnailURL)"
|
thumbnailURL = "\(accountUrlComponents.scheme ?? "https"):\(thumbnailURL)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tabs = json["tabs"].arrayValue.compactMap { name in
|
||||||
|
if let name = name.string, let type = Channel.ContentType.from(name) {
|
||||||
|
return Channel.Tab(contentType: type, data: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return Channel(
|
return Channel(
|
||||||
app: .invidious,
|
app: .invidious,
|
||||||
id: json["authorId"].stringValue,
|
id: json["authorId"].stringValue,
|
||||||
@ -514,7 +534,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
subscriptionsCount: json["subCount"].int,
|
subscriptionsCount: json["subCount"].int,
|
||||||
subscriptionsText: json["subCountText"].string,
|
subscriptionsText: json["subCountText"].string,
|
||||||
totalViews: json["totalViews"].int,
|
totalViews: json["totalViews"].int,
|
||||||
videos: json.dictionaryValue["latestVideos"]?.arrayValue.map(extractVideo) ?? []
|
videos: json.dictionaryValue["latestVideos"]?.arrayValue.map(extractVideo) ?? [],
|
||||||
|
tabs: tabs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,6 +573,33 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static var contentItemsKeys = ["items", "videos", "latestVideos", "playlists", "relatedChannels"]
|
||||||
|
|
||||||
|
private func extractChannelPage(from json: JSON, forceNotLast: Bool = false) -> ChannelPage {
|
||||||
|
let nextPage = json.dictionaryValue["continuation"]?.string
|
||||||
|
var contentItems = [ContentItem]()
|
||||||
|
|
||||||
|
var items = [ContentItem]()
|
||||||
|
|
||||||
|
if let key = Self.contentItemsKeys.first(where: { json.dictionaryValue.keys.contains($0) }),
|
||||||
|
let items = json.dictionaryValue[key]
|
||||||
|
{
|
||||||
|
contentItems = extractContentItems(from: items)
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = false
|
||||||
|
if !forceNotLast {
|
||||||
|
last = nextPage?.isEmpty ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChannelPage(
|
||||||
|
results: contentItems,
|
||||||
|
channel: extractChannel(from: json),
|
||||||
|
nextPage: nextPage,
|
||||||
|
last: last
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func extractStreams(from json: JSON) -> [Stream] {
|
private func extractStreams(from json: JSON) -> [Stream] {
|
||||||
let hls = extractHLSStreams(from: json)
|
let hls = extractHLSStreams(from: json)
|
||||||
if json["liveNow"].boolValue {
|
if json["liveNow"].boolValue {
|
||||||
@ -668,4 +716,33 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func extractContentItems(from json: JSON) -> [ContentItem] {
|
||||||
|
json.arrayValue.compactMap { extractContentItem(from: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractContentItem(from json: JSON) -> ContentItem? {
|
||||||
|
let type = json.dictionaryValue["type"]?.string
|
||||||
|
|
||||||
|
if type == "channel" {
|
||||||
|
return ContentItem(channel: extractChannel(from: json))
|
||||||
|
} else if type == "playlist" {
|
||||||
|
return ContentItem(playlist: extractChannelPlaylist(from: json))
|
||||||
|
} else if type == "video" {
|
||||||
|
return ContentItem(video: extractVideo(from: json))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Channel.ContentType {
|
||||||
|
var invidiousID: String {
|
||||||
|
switch self {
|
||||||
|
case .livestreams:
|
||||||
|
return "streams"
|
||||||
|
default:
|
||||||
|
return rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -284,7 +284,7 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
|
|||||||
.onCompletion { _ in onCompletion() }
|
.onCompletion { _ in onCompletion() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func channel(_ id: String, contentType: Channel.ContentType, data _: String?) -> Resource {
|
func channel(_ id: String, contentType: Channel.ContentType, data _: String?, page _: String?) -> Resource {
|
||||||
if contentType == .playlists {
|
if contentType == .playlists {
|
||||||
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)/playlists"))
|
return resource(baseURL: account.url, path: basePathAppending("channels/\(id)/playlists"))
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import SwiftyJSON
|
|||||||
final class PipedAPI: Service, ObservableObject, VideosAPI {
|
final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||||
static var disallowedVideoCodecs = ["av01"]
|
static var disallowedVideoCodecs = ["av01"]
|
||||||
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"]
|
static var authorizedEndpoints = ["subscriptions", "subscribe", "unsubscribe", "user/playlists"]
|
||||||
|
static var contentItemsKeys = ["items", "content", "relatedStreams"]
|
||||||
|
|
||||||
@Published var account: Account!
|
@Published var account: Account!
|
||||||
|
|
||||||
@ -40,8 +41,25 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
$0.headers["Authorization"] = self.account.token
|
$0.headers["Authorization"] = self.account.token
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> Channel? in
|
configureTransformer(pathPattern("channel/*")) { (content: Entity<JSON>) -> ChannelPage in
|
||||||
self.extractChannel(from: content.json)
|
let nextPage = content.json.dictionaryValue["nextpage"]?.string
|
||||||
|
let channel = self.extractChannel(from: content.json)
|
||||||
|
return ChannelPage(
|
||||||
|
results: self.extractContentItems(from: self.contentItemsDictionary(from: content.json)),
|
||||||
|
channel: channel,
|
||||||
|
nextPage: nextPage,
|
||||||
|
last: nextPage.isNil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTransformer(pathPattern("/nextpage/channel/*")) { (content: Entity<JSON>) -> ChannelPage in
|
||||||
|
let nextPage = content.json.dictionaryValue["nextpage"]?.string
|
||||||
|
return ChannelPage(
|
||||||
|
results: self.extractContentItems(from: self.contentItemsDictionary(from: content.json)),
|
||||||
|
channel: self.extractChannel(from: content.json),
|
||||||
|
nextPage: nextPage,
|
||||||
|
last: nextPage.isNil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("channels/tabs*")) { (content: Entity<JSON>) -> [ContentItem] in
|
configureTransformer(pathPattern("channels/tabs*")) { (content: Entity<JSON>) -> [ContentItem] in
|
||||||
@ -159,13 +177,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
resource(baseURL: account.url, path: "login")
|
resource(baseURL: account.url, path: "login")
|
||||||
}
|
}
|
||||||
|
|
||||||
func channel(_ id: String, contentType: Channel.ContentType, data: String?) -> Resource {
|
func channel(_ id: String, contentType: Channel.ContentType, data: String?, page: String?) -> Resource {
|
||||||
if contentType == .videos {
|
let path = page.isNil ? "channel" : "nextpage/channel"
|
||||||
return resource(baseURL: account.url, path: "channel/\(id)")
|
|
||||||
|
var channel: Siesta.Resource
|
||||||
|
|
||||||
|
if contentType == .videos || data.isNil {
|
||||||
|
channel = resource(baseURL: account.url, path: "\(path)/\(id)")
|
||||||
|
} else {
|
||||||
|
channel = resource(baseURL: account.url, path: "channels/tabs")
|
||||||
|
.withParam("data", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resource(baseURL: account.url, path: "channels/tabs")
|
if let page, !page.isEmpty {
|
||||||
.withParam("data", data)
|
channel = channel.withParam("nextpage", page)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelByName(_ name: String) -> Resource? {
|
func channelByName(_ name: String) -> Resource? {
|
||||||
@ -700,4 +728,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return Chapter(title: title, image: image, start: start)
|
return Chapter(title: title, image: image, start: start)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func contentItemsDictionary(from content: JSON) -> JSON {
|
||||||
|
if let key = Self.contentItemsKeys.first(where: { content.dictionaryValue.keys.contains($0) }),
|
||||||
|
let items = content.dictionaryValue[key]
|
||||||
|
{
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
return .null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ protocol VideosAPI {
|
|||||||
|
|
||||||
static func withAnonymousAccountForInstanceURL(_ url: URL) -> Self
|
static func withAnonymousAccountForInstanceURL(_ url: URL) -> Self
|
||||||
|
|
||||||
func channel(_ id: String, contentType: Channel.ContentType, data: String?) -> Resource
|
func channel(_ id: String, contentType: Channel.ContentType, data: String?, page: String?) -> Resource
|
||||||
func channelByName(_ name: String) -> Resource?
|
func channelByName(_ name: String) -> Resource?
|
||||||
func channelByUsername(_ username: String) -> Resource?
|
func channelByUsername(_ username: String) -> Resource?
|
||||||
func channelVideos(_ id: String) -> Resource
|
func channelVideos(_ id: String) -> Resource
|
||||||
@ -72,8 +72,8 @@ protocol VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension VideosAPI {
|
extension VideosAPI {
|
||||||
func channel(_ id: String, contentType: Channel.ContentType, data: String? = nil) -> Resource {
|
func channel(_ id: String, contentType: Channel.ContentType, data: String? = nil, page: String? = nil) -> Resource {
|
||||||
channel(id, contentType: contentType, data: data)
|
channel(id, contentType: contentType, data: data, page: page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadDetails(
|
func loadDetails(
|
||||||
|
@ -34,11 +34,11 @@ struct ChannelsCacheModel: CacheModel {
|
|||||||
store(channel)
|
store(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieve(_ cacheKey: String) -> Channel? {
|
func retrieve(_ cacheKey: String) -> ChannelPage? {
|
||||||
logger.debug("retrieving cache for \(cacheKey)")
|
logger.debug("retrieving cache for \(cacheKey)")
|
||||||
|
|
||||||
if let json = try? storage?.object(forKey: cacheKey) {
|
if let json = try? storage?.object(forKey: cacheKey) {
|
||||||
return Channel.from(json)
|
return ChannelPage(channel: Channel.from(json))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -110,12 +110,12 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
|
|||||||
if let json = try? storage?.object(forKey: channelsCacheKey(account)),
|
if let json = try? storage?.object(forKey: channelsCacheKey(account)),
|
||||||
let channels = json.dictionaryValue["channels"]
|
let channels = json.dictionaryValue["channels"]
|
||||||
{
|
{
|
||||||
return channels.arrayValue.map { json in
|
return channels.arrayValue.compactMap { json in
|
||||||
let channel = Channel.from(json)
|
let channel = Channel.from(json)
|
||||||
if !channel.hasExtendedDetails,
|
if !channel.hasExtendedDetails,
|
||||||
let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey)
|
let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey)
|
||||||
{
|
{
|
||||||
return cache
|
return cache.channel
|
||||||
}
|
}
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
|
@ -11,6 +11,15 @@ struct Channel: Identifiable, Hashable {
|
|||||||
case shorts
|
case shorts
|
||||||
case channels
|
case channels
|
||||||
|
|
||||||
|
static func from(_ name: String) -> Self? {
|
||||||
|
let rawValueMatch = allCases.first { $0.rawValue == name }
|
||||||
|
guard rawValueMatch.isNil else { return rawValueMatch! }
|
||||||
|
|
||||||
|
if name == "streams" { return .livestreams }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
rawValue
|
rawValue
|
||||||
}
|
}
|
||||||
@ -110,7 +119,6 @@ struct Channel: Identifiable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hasData(for contentType: ContentType) -> Bool {
|
func hasData(for contentType: ContentType) -> Bool {
|
||||||
guard contentType != .videos, contentType != .playlists else { return true }
|
|
||||||
return tabs.contains { $0.contentType == contentType }
|
return tabs.contains { $0.contentType == contentType }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +140,7 @@ struct Channel: Identifiable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var thumbnailURLOrCached: URL? {
|
var thumbnailURLOrCached: URL? {
|
||||||
thumbnailURL ?? ChannelsCacheModel.shared.retrieve(cacheKey)?.thumbnailURL
|
thumbnailURL ?? ChannelsCacheModel.shared.retrieve(cacheKey)?.channel?.thumbnailURL
|
||||||
}
|
}
|
||||||
|
|
||||||
var json: JSON {
|
var json: JSON {
|
||||||
|
8
Model/ChannelPage.swift
Normal file
8
Model/ChannelPage.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ChannelPage {
|
||||||
|
var results = [ContentItem]()
|
||||||
|
var channel: Channel?
|
||||||
|
var nextPage: String?
|
||||||
|
var last = false
|
||||||
|
}
|
@ -11,11 +11,12 @@ struct ChannelVideosView: View {
|
|||||||
@State private var shareURL: URL?
|
@State private var shareURL: URL?
|
||||||
@State private var subscriptionToggleButtonDisabled = false
|
@State private var subscriptionToggleButtonDisabled = false
|
||||||
|
|
||||||
|
@State private var page: ChannelPage?
|
||||||
@State private var contentType = Channel.ContentType.videos
|
@State private var contentType = Channel.ContentType.videos
|
||||||
@StateObject private var contentTypeItems = Store<[ContentItem]>()
|
@StateObject private var contentTypeItems = Store<[ContentItem]>()
|
||||||
|
|
||||||
@State private var descriptionExpanded = false
|
@State private var descriptionExpanded = false
|
||||||
@StateObject private var store = Store<Channel>()
|
@StateObject private var store = Store<ChannelPage>()
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@ -35,14 +36,10 @@ struct ChannelVideosView: View {
|
|||||||
@Default(.hideShorts) private var hideShorts
|
@Default(.hideShorts) private var hideShorts
|
||||||
|
|
||||||
var presentedChannel: Channel? {
|
var presentedChannel: Channel? {
|
||||||
store.item ?? channel ?? recents.presentedChannel
|
store.item?.channel ?? channel ?? recents.presentedChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentItems: [ContentItem] {
|
var contentItems: [ContentItem] {
|
||||||
guard contentType != .videos else {
|
|
||||||
return ContentItem.array(of: presentedChannel?.videos ?? [])
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentTypeItems.collection
|
return contentTypeItems.collection
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +98,7 @@ struct ChannelVideosView: View {
|
|||||||
banner
|
banner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.environment(\.loadMoreContentHandler) { loadNextPage() }
|
||||||
.environment(\.inChannelView, true)
|
.environment(\.inChannelView, true)
|
||||||
.environment(\.listingStyle, channelPlaylistListingStyle)
|
.environment(\.listingStyle, channelPlaylistListingStyle)
|
||||||
.environment(\.hideShorts, hideShorts)
|
.environment(\.hideShorts, hideShorts)
|
||||||
@ -180,14 +178,10 @@ struct ChannelVideosView: View {
|
|||||||
store.replace(cache)
|
store.replace(cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
resource?.loadIfNeeded()?.onSuccess { response in
|
load()
|
||||||
if let channel: Channel = response.typedContent() {
|
|
||||||
ChannelsCacheModel.shared.store(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onChange(of: contentType) { _ in
|
.onChange(of: contentType) { _ in
|
||||||
resource?.load()
|
load()
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@ -218,7 +212,7 @@ struct ChannelVideosView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var thumbnail: some View {
|
var thumbnail: some View {
|
||||||
ChannelAvatarView(channel: store.item)
|
ChannelAvatarView(channel: store.item?.channel)
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.frame(width: 80, height: 80, alignment: .trailing)
|
.frame(width: 80, height: 80, alignment: .trailing)
|
||||||
#else
|
#else
|
||||||
@ -238,7 +232,7 @@ struct ChannelVideosView: View {
|
|||||||
|
|
||||||
var subscriptionsLabel: some View {
|
var subscriptionsLabel: some View {
|
||||||
Group {
|
Group {
|
||||||
if let subscribers = store.item?.subscriptionsString {
|
if let subscribers = store.item?.channel?.subscriptionsString {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text(subscribers)
|
Text(subscribers)
|
||||||
Image(systemName: "person.2.fill")
|
Image(systemName: "person.2.fill")
|
||||||
@ -257,7 +251,7 @@ struct ChannelVideosView: View {
|
|||||||
|
|
||||||
var viewsLabel: some View {
|
var viewsLabel: some View {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
if let views = store.item?.totalViewsString {
|
if let views = store.item?.channel?.totalViewsString {
|
||||||
Text(views)
|
Text(views)
|
||||||
|
|
||||||
Image(systemName: "eye.fill")
|
Image(systemName: "eye.fill")
|
||||||
@ -328,7 +322,7 @@ struct ChannelVideosView: View {
|
|||||||
Picker("Content type", selection: $contentType) {
|
Picker("Content type", selection: $contentType) {
|
||||||
if let channel = presentedChannel {
|
if let channel = presentedChannel {
|
||||||
ForEach(Channel.ContentType.allCases, id: \.self) { type in
|
ForEach(Channel.ContentType.allCases, id: \.self) { type in
|
||||||
if channel.hasData(for: type) {
|
if type == .videos || type == .playlists || channel.hasData(for: type) {
|
||||||
Label(type.description, systemImage: type.systemImage).tag(type)
|
Label(type.description, systemImage: type.systemImage).tag(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -341,11 +335,11 @@ struct ChannelVideosView: View {
|
|||||||
|
|
||||||
let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil
|
let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil
|
||||||
let resource = accounts.api.channel(channel.id, contentType: contentType, data: data)
|
let resource = accounts.api.channel(channel.id, contentType: contentType, data: data)
|
||||||
|
|
||||||
if contentType == .videos {
|
if contentType == .videos {
|
||||||
resource.addObserver(store)
|
resource.addObserver(store)
|
||||||
} else {
|
|
||||||
resource.addObserver(contentTypeItems)
|
|
||||||
}
|
}
|
||||||
|
resource.addObserver(contentTypeItems)
|
||||||
|
|
||||||
return resource
|
return resource
|
||||||
}
|
}
|
||||||
@ -424,6 +418,42 @@ struct ChannelVideosView: View {
|
|||||||
Label("Mark channel feed as unwatched", systemImage: "checkmark.circle")
|
Label("Mark channel feed as unwatched", systemImage: "checkmark.circle")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func load() {
|
||||||
|
resource?.load().onSuccess { response in
|
||||||
|
if let page: ChannelPage = response.typedContent() {
|
||||||
|
if let channel = page.channel {
|
||||||
|
ChannelsCacheModel.shared.store(channel)
|
||||||
|
}
|
||||||
|
self.page = page
|
||||||
|
self.contentTypeItems.replace(page.results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { error in
|
||||||
|
navigation.presentAlert(title: "Could not load channel data", message: error.userMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNextPage() {
|
||||||
|
guard let channel = presentedChannel, let pageToLoad = page, !pageToLoad.last else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = pageToLoad.nextPage
|
||||||
|
if contentType == .videos, !pageToLoad.last {
|
||||||
|
next = next ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil
|
||||||
|
accounts.api.channel(channel.id, contentType: contentType, data: data, page: next).load().onSuccess { response in
|
||||||
|
if let page: ChannelPage = response.typedContent() {
|
||||||
|
self.page = page
|
||||||
|
let keys = self.contentTypeItems.collection.map(\.cacheKey)
|
||||||
|
let items = self.contentTypeItems.collection + page.results.filter { !keys.contains($0.cacheKey) }
|
||||||
|
self.contentTypeItems.replace(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChannelVideosView_Previews: PreviewProvider {
|
struct ChannelVideosView_Previews: PreviewProvider {
|
||||||
|
@ -74,9 +74,10 @@ struct FavoriteItemView: View {
|
|||||||
case let .channel(_, id, name):
|
case let .channel(_, id, name):
|
||||||
var channel = Channel(app: .invidious, id: id, name: name)
|
var channel = Channel(app: .invidious, id: id, name: name)
|
||||||
if let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey),
|
if let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey),
|
||||||
!cache.videos.isEmpty
|
let cacheChannel = cache.channel,
|
||||||
|
!cacheChannel.videos.isEmpty
|
||||||
{
|
{
|
||||||
contentItems = ContentItem.array(of: cache.videos)
|
contentItems = ContentItem.array(of: cacheChannel.videos)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess = { response in
|
onSuccess = { response in
|
||||||
|
@ -830,6 +830,9 @@
|
|||||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
|
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
|
||||||
|
37C7B21429ABD9F20013C196 /* ChannelPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7B21329ABD9F20013C196 /* ChannelPage.swift */; };
|
||||||
|
37C7B21529ABD9F20013C196 /* ChannelPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7B21329ABD9F20013C196 /* ChannelPage.swift */; };
|
||||||
|
37C7B21629ABD9F20013C196 /* ChannelPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7B21329ABD9F20013C196 /* ChannelPage.swift */; };
|
||||||
37C89322294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
|
37C89322294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
|
||||||
37C89323294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
|
37C89323294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
|
||||||
37C89324294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
|
37C89324294532220032AFD3 /* PlayerOverlayModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */; };
|
||||||
@ -1432,6 +1435,7 @@
|
|||||||
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = "<group>"; };
|
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = "<group>"; };
|
37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = "<group>"; };
|
||||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
|
||||||
|
37C7B21329ABD9F20013C196 /* ChannelPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPage.swift; sourceTree = "<group>"; };
|
||||||
37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerOverlayModifier.swift; sourceTree = "<group>"; };
|
37C89321294532220032AFD3 /* PlayerOverlayModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerOverlayModifier.swift; sourceTree = "<group>"; };
|
||||||
37C8E700294FC97D00EEAB14 /* QueueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueView.swift; sourceTree = "<group>"; };
|
37C8E700294FC97D00EEAB14 /* QueueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueView.swift; sourceTree = "<group>"; };
|
||||||
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = "<group>"; };
|
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = "<group>"; };
|
||||||
@ -2380,6 +2384,7 @@
|
|||||||
377F9F79294403DC0043F856 /* Cache */,
|
377F9F79294403DC0043F856 /* Cache */,
|
||||||
3776ADD5287381240078EBC4 /* Captions.swift */,
|
3776ADD5287381240078EBC4 /* Captions.swift */,
|
||||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||||
|
37C7B21329ABD9F20013C196 /* ChannelPage.swift */,
|
||||||
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
||||||
37520698285E8DD300CA655F /* Chapter.swift */,
|
37520698285E8DD300CA655F /* Chapter.swift */,
|
||||||
371B7E5B27596B8400D21217 /* Comment.swift */,
|
371B7E5B27596B8400D21217 /* Comment.swift */,
|
||||||
@ -3237,6 +3242,7 @@
|
|||||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||||
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
|
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
|
||||||
37520699285E8DD300CA655F /* Chapter.swift in Sources */,
|
37520699285E8DD300CA655F /* Chapter.swift in Sources */,
|
||||||
|
37C7B21429ABD9F20013C196 /* ChannelPage.swift in Sources */,
|
||||||
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||||
@ -3462,6 +3468,7 @@
|
|||||||
37A362BB2953707F00BDF328 /* ClearQueueButton.swift in Sources */,
|
37A362BB2953707F00BDF328 /* ClearQueueButton.swift in Sources */,
|
||||||
3752069E285E910600CA655F /* ChapterView.swift in Sources */,
|
3752069E285E910600CA655F /* ChapterView.swift in Sources */,
|
||||||
37030FF827B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
|
37030FF827B0347C00ECDDAA /* MPVPlayerView.swift in Sources */,
|
||||||
|
37C7B21529ABD9F20013C196 /* ChannelPage.swift in Sources */,
|
||||||
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||||
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
|
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||||
@ -3882,6 +3889,7 @@
|
|||||||
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||||
37C8E703294FC97D00EEAB14 /* QueueView.swift in Sources */,
|
37C8E703294FC97D00EEAB14 /* QueueView.swift in Sources */,
|
||||||
|
37C7B21629ABD9F20013C196 /* ChannelPage.swift in Sources */,
|
||||||
3754B01728B7F84D009717C8 /* Constants.swift in Sources */,
|
3754B01728B7F84D009717C8 /* Constants.swift in Sources */,
|
||||||
37BC50AE2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
37BC50AE2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||||
37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||||
|
Loading…
Reference in New Issue
Block a user