mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Subscribe/unsubscribe channels
This commit is contained in:
parent
151121aa31
commit
1196a2a5e2
@ -11,9 +11,9 @@ extension Video {
|
||||
length: 582,
|
||||
published: "7 years ago",
|
||||
views: 21534,
|
||||
channelID: "AbCdEFgHI",
|
||||
description: "Some relaxing live piano music",
|
||||
genre: "Music",
|
||||
channel: Channel(id: "AbCdEFgHI", name: "The Channel", subscriptionsCount: "2.3K"),
|
||||
thumbnails: Thumbnail.fixturesForAllQualities(videoId: id),
|
||||
live: false,
|
||||
upcoming: false,
|
||||
|
@ -1,12 +1,22 @@
|
||||
import AVFoundation
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
struct Channel: Codable, Defaults.Serializable {
|
||||
var id: String
|
||||
var name: String
|
||||
var subscriptionsCount: String
|
||||
|
||||
static func from(video: Video) -> Channel {
|
||||
Channel(id: video.channelID, name: video.author)
|
||||
init(json: JSON) {
|
||||
id = json["authorId"].stringValue
|
||||
name = json["author"].stringValue
|
||||
subscriptionsCount = json["subCountText"].stringValue
|
||||
}
|
||||
|
||||
init(id: String, name: String, subscriptionsCount: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.subscriptionsCount = subscriptionsCount
|
||||
}
|
||||
}
|
||||
|
@ -61,15 +61,19 @@ final class InvidiousAPI: Service {
|
||||
|
||||
configureTransformer("/auth/feed", requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||
if let feedVideos = content.json.dictionaryValue["videos"] {
|
||||
return feedVideos.arrayValue.map { Video($0) }
|
||||
return feedVideos.arrayValue.map(Video.init)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
configureTransformer("/auth/subscriptions", requestMethods: [.get]) { (content: Entity<JSON>) -> [Channel] in
|
||||
content.json.arrayValue.map(Channel.init)
|
||||
}
|
||||
|
||||
configureTransformer("/channels/*", requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||
if let channelVideos = content.json.dictionaryValue["latestVideos"] {
|
||||
return channelVideos.arrayValue.map { Video($0) }
|
||||
return channelVideos.arrayValue.map(Video.init)
|
||||
}
|
||||
|
||||
return []
|
||||
@ -92,10 +96,18 @@ final class InvidiousAPI: Service {
|
||||
.withParam("region", country.rawValue)
|
||||
}
|
||||
|
||||
var subscriptions: Resource {
|
||||
var feed: Resource {
|
||||
resource("/auth/feed")
|
||||
}
|
||||
|
||||
var subscriptions: Resource {
|
||||
resource("/auth/subscriptions")
|
||||
}
|
||||
|
||||
func channelSubscription(_ id: String) -> Resource {
|
||||
resource("/auth/subscriptions").child(id)
|
||||
}
|
||||
|
||||
func channelVideos(_ id: String) -> Resource {
|
||||
resource("/channels/\(id)")
|
||||
}
|
||||
|
41
Model/Subscriptions.swift
Normal file
41
Model/Subscriptions.swift
Normal file
@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
final class Subscriptions: ObservableObject {
|
||||
@Published var channels = [Channel]()
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.subscriptions
|
||||
}
|
||||
|
||||
init() {
|
||||
load()
|
||||
}
|
||||
|
||||
func subscribe(_ channelID: String) {
|
||||
performChannelSubscriptionRequest(channelID, method: .post)
|
||||
}
|
||||
|
||||
func unsubscribe(_ channelID: String) {
|
||||
performChannelSubscriptionRequest(channelID, method: .delete)
|
||||
}
|
||||
|
||||
func subscribed(_ channelID: String) -> Bool {
|
||||
channels.contains { $0.id == channelID }
|
||||
}
|
||||
|
||||
fileprivate func load() {
|
||||
resource.load().onSuccess { resource in
|
||||
if let channels: [Channel] = resource.typedContent() {
|
||||
self.channels = channels
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func performChannelSubscriptionRequest(_ channelID: String, method: RequestMethod) {
|
||||
InvidiousAPI.shared.channelSubscription(channelID).request(method).onCompletion { _ in
|
||||
self.load()
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ struct Video: Identifiable, Equatable {
|
||||
var length: TimeInterval
|
||||
var published: String
|
||||
var views: Int
|
||||
var channelID: String
|
||||
var description: String
|
||||
var genre: String
|
||||
|
||||
@ -29,6 +28,8 @@ struct Video: Identifiable, Equatable {
|
||||
var dislikes: Int?
|
||||
var keywords = [String]()
|
||||
|
||||
var channel: Channel
|
||||
|
||||
init(
|
||||
id: String,
|
||||
title: String,
|
||||
@ -36,9 +37,9 @@ struct Video: Identifiable, Equatable {
|
||||
length: TimeInterval,
|
||||
published: String,
|
||||
views: Int,
|
||||
channelID: String,
|
||||
description: String,
|
||||
genre: String,
|
||||
channel: Channel,
|
||||
thumbnails: [Thumbnail] = [],
|
||||
indexID: String? = nil,
|
||||
live: Bool = false,
|
||||
@ -54,9 +55,9 @@ struct Video: Identifiable, Equatable {
|
||||
self.length = length
|
||||
self.published = published
|
||||
self.views = views
|
||||
self.channelID = channelID
|
||||
self.description = description
|
||||
self.genre = genre
|
||||
self.channel = channel
|
||||
self.thumbnails = thumbnails
|
||||
self.indexID = indexID
|
||||
self.live = live
|
||||
@ -83,7 +84,6 @@ struct Video: Identifiable, Equatable {
|
||||
length = json["lengthSeconds"].doubleValue
|
||||
published = json["publishedText"].stringValue
|
||||
views = json["viewCount"].intValue
|
||||
channelID = json["authorId"].stringValue
|
||||
description = json["description"].stringValue
|
||||
genre = json["genre"].stringValue
|
||||
|
||||
@ -105,6 +105,7 @@ struct Video: Identifiable, Equatable {
|
||||
streams.append(contentsOf: Video.extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue))
|
||||
|
||||
hlsUrl = json["hlsUrl"].url
|
||||
channel = Channel(json: json)
|
||||
}
|
||||
|
||||
var playTime: String? {
|
||||
|
@ -176,6 +176,9 @@
|
||||
37D4B19826717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
||||
37D4B19926717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; };
|
||||
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 37D4B19C2671817900C925CA /* SwiftyJSON */; };
|
||||
37E64DD126D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
||||
37E64DD226D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
||||
37E64DD326D597EB00C71877 /* Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* Subscriptions.swift */; };
|
||||
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
||||
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */; };
|
||||
@ -286,6 +289,7 @@
|
||||
37D4B18B26717B3800C925CA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = "<group>"; };
|
||||
37D4B19626717E1500C925CA /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; };
|
||||
37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
37E64DD026D597EB00C71877 /* Subscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscriptions.swift; sourceTree = "<group>"; };
|
||||
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
|
||||
37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
|
||||
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
|
||||
@ -579,6 +583,7 @@
|
||||
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
|
||||
3797758A2689345500DD52A8 /* Store.swift */,
|
||||
37CEE4C02677B697005A1EFE /* Stream.swift */,
|
||||
37E64DD026D597EB00C71877 /* Subscriptions.swift */,
|
||||
373CFADA269663F1003CB2C6 /* Thumbnail.swift */,
|
||||
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
||||
37D4B19626717E1500C925CA /* Video.swift */,
|
||||
@ -852,6 +857,7 @@
|
||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
37E64DD126D597EB00C71877 /* Subscriptions.swift in Sources */,
|
||||
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
37B81B0526D2CEDA00675966 /* PlaybackState.swift in Sources */,
|
||||
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
@ -928,6 +934,7 @@
|
||||
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
|
||||
37E64DD226D597EB00C71877 /* Subscriptions.swift in Sources */,
|
||||
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||
37C7A1DD267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||
@ -990,6 +997,7 @@
|
||||
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */,
|
||||
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
37E64DD326D597EB00C71877 /* Subscriptions.swift in Sources */,
|
||||
37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||
373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */,
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||
|
@ -49,7 +49,7 @@ struct AppSidebarNavigation: View {
|
||||
SubscriptionsView()
|
||||
}
|
||||
label: {
|
||||
Label("Subscriptions", systemImage: "play.rectangle.fill")
|
||||
Label("Subscriptions", systemImage: "star.circle.fill")
|
||||
.accessibility(label: Text("Subscriptions"))
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ struct AppTabNavigation: View {
|
||||
SubscriptionsView()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Subscriptions", systemImage: "play.rectangle.fill")
|
||||
Label("Subscriptions", systemImage: "star.circle.fill")
|
||||
.accessibility(label: Text("Subscriptions"))
|
||||
}
|
||||
.tag(TabSelection.subscriptions)
|
||||
|
@ -4,6 +4,7 @@ struct ContentView: View {
|
||||
@StateObject private var navigationState = NavigationState()
|
||||
@StateObject private var playbackState = PlaybackState()
|
||||
@StateObject private var searchState = SearchState()
|
||||
@StateObject private var subscriptions = Subscriptions()
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@ -40,6 +41,7 @@ struct ContentView: View {
|
||||
.environmentObject(navigationState)
|
||||
.environmentObject(playbackState)
|
||||
.environmentObject(searchState)
|
||||
.environmentObject(subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,16 +2,76 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct VideoDetails: View {
|
||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||
|
||||
@State private var subscribed = false
|
||||
@State private var confirmationShown = false
|
||||
|
||||
var video: Video
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(video.title)
|
||||
.font(.title2.bold())
|
||||
.padding(.bottom, 0)
|
||||
|
||||
Text(video.author)
|
||||
Divider()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
HStack(spacing: 4) {
|
||||
if subscribed {
|
||||
Image(systemName: "star.circle.fill")
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text(video.channel.name)
|
||||
.font(.system(size: 13))
|
||||
.bold()
|
||||
if !video.channel.subscriptionsCount.isEmpty {
|
||||
Text("\(video.channel.subscriptionsCount) subscribers")
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Section {
|
||||
if subscribed {
|
||||
Button("Unsubscribe") {
|
||||
confirmationShown = true
|
||||
}
|
||||
#if os(iOS)
|
||||
.tint(.gray)
|
||||
#endif
|
||||
.confirmationDialog("Are you you want to unsubscribe from \(video.channel.name)?", isPresented: $confirmationShown) {
|
||||
Button("Unsubscribe") {
|
||||
subscriptions.unsubscribe(video.channel.id)
|
||||
|
||||
withAnimation {
|
||||
subscribed.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
subscriptions.subscribe(video.channel.id)
|
||||
|
||||
withAnimation {
|
||||
subscribed.toggle()
|
||||
}
|
||||
}
|
||||
.tint(.blue)
|
||||
}
|
||||
}
|
||||
.font(.system(size: 13))
|
||||
.buttonStyle(.borderless)
|
||||
.buttonBorderShape(.roundedRectangle)
|
||||
}
|
||||
.padding(.bottom, -1)
|
||||
|
||||
Divider()
|
||||
|
||||
HStack(spacing: 4) {
|
||||
if let published = video.publishedDate {
|
||||
Text(published)
|
||||
@ -26,25 +86,37 @@ struct VideoDetails: View {
|
||||
Text(publishedAt.formatted(date: .abbreviated, time: .omitted))
|
||||
}
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.font(.system(size: 12))
|
||||
.padding(.bottom, -1)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
if let views = video.viewsCount {
|
||||
VideoDetail(title: "Views", detail: views)
|
||||
videoDetail(label: "Views", value: views, symbol: "eye.fill")
|
||||
}
|
||||
|
||||
if let likes = video.likesCount {
|
||||
VideoDetail(title: "Likes", detail: likes, symbol: "hand.thumbsup.circle.fill", symbolColor: Color("VideoDetailLikesSymbolColor"))
|
||||
Divider()
|
||||
|
||||
videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup.circle.fill")
|
||||
}
|
||||
|
||||
if let dislikes = video.dislikesCount {
|
||||
VideoDetail(title: "Dislikes", detail: dislikes, symbol: "hand.thumbsdown.circle.fill", symbolColor: Color("VideoDetailDislikesSymbolColor"))
|
||||
Divider()
|
||||
|
||||
videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown.circle.fill")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 1)
|
||||
.padding(.vertical, 4)
|
||||
.frame(maxHeight: 35)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Divider()
|
||||
|
||||
#if os(macOS)
|
||||
ScrollView(.vertical) {
|
||||
@ -81,6 +153,25 @@ struct VideoDetails: View {
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.padding([.horizontal, .bottom])
|
||||
.onAppear {
|
||||
subscribed = subscriptions.subscribed(video.channel.id)
|
||||
}
|
||||
}
|
||||
|
||||
func videoDetail(label: String, value: String, symbol: String) -> some View {
|
||||
VStack(spacing: 4) {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: symbol)
|
||||
|
||||
Text(label.uppercased())
|
||||
}
|
||||
.font(.system(size: 9))
|
||||
.opacity(0.6)
|
||||
|
||||
Text(value)
|
||||
}
|
||||
|
||||
.frame(maxWidth: 100)
|
||||
}
|
||||
|
||||
var showScrollIndicators: Bool {
|
||||
@ -92,41 +183,9 @@ struct VideoDetails: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct VideoDetail: View {
|
||||
var title: String
|
||||
var detail: String
|
||||
var symbol = "eye.fill"
|
||||
var symbolColor = Color.white
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
Image(systemName: symbol)
|
||||
.foregroundColor(symbolColor)
|
||||
|
||||
Text(title.uppercased())
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.font(.caption2)
|
||||
.padding([.leading, .top], 4)
|
||||
.frame(alignment: .leading)
|
||||
|
||||
Divider()
|
||||
.background(.gray)
|
||||
.padding(.vertical, 4)
|
||||
|
||||
Text(detail)
|
||||
.shadow(radius: 1.0)
|
||||
.font(.title3.bold())
|
||||
}
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.background(Color("VideoDetailBackgroundColor"))
|
||||
.cornerRadius(6)
|
||||
.overlay(RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(Color("VideoDetailBorderColor"), lineWidth: 1))
|
||||
.frame(maxWidth: 90)
|
||||
struct VideoDetails_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VideoDetails(video: Video.fixture)
|
||||
.environmentObject(Subscriptions())
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import SwiftUI
|
||||
struct SubscriptionsView: View {
|
||||
@ObservedObject private var store = Store<[Video]>()
|
||||
|
||||
var resource = InvidiousAPI.shared.subscriptions
|
||||
var resource = InvidiousAPI.shared.feed
|
||||
|
||||
init() {
|
||||
resource.addObserver(store)
|
||||
|
@ -3,14 +3,21 @@ import SwiftUI
|
||||
|
||||
struct VideoContextMenuView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||
|
||||
let video: Video
|
||||
|
||||
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
||||
@Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist
|
||||
|
||||
@State private var subscribed = false
|
||||
|
||||
var body: some View {
|
||||
openChannelButton(from: video)
|
||||
Section {
|
||||
openChannelButton
|
||||
|
||||
subscriptionButton
|
||||
.opacity(subscribed ? 1 : 1)
|
||||
|
||||
openVideoDetailsButton
|
||||
|
||||
@ -20,16 +27,25 @@ struct VideoContextMenuView: View {
|
||||
addToPlaylistButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openChannelButton(from video: Video) -> some View {
|
||||
var openChannelButton: some View {
|
||||
Button("\(video.author) Channel") {
|
||||
navigationState.openChannel(Channel.from(video: video))
|
||||
navigationState.openChannel(video.channel)
|
||||
}
|
||||
}
|
||||
|
||||
func closeChannelButton(from video: Video) -> some View {
|
||||
Button("Close \(Channel.from(video: video).name) Channel") {
|
||||
navigationState.closeChannel()
|
||||
var subscriptionButton: some View {
|
||||
Group {
|
||||
if subscriptions.subscribed(video.channel.id) {
|
||||
Button("Unsubscribe", role: .destructive) {
|
||||
subscriptions.unsubscribe(video.channel.id)
|
||||
}
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
subscriptions.subscribe(video.channel.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,8 +20,7 @@ final class PlayerViewController: NSViewController {
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
playerState = PlayerState()
|
||||
playerState.playbackState = playbackState
|
||||
playerState = PlayerState(playbackState: playbackState)
|
||||
|
||||
guard playerState.player == nil else {
|
||||
return
|
||||
|
@ -101,7 +101,7 @@ struct VideoDetailsView: View {
|
||||
}
|
||||
|
||||
var openChannelButton: some View {
|
||||
let channel = Channel.from(video: store.item!)
|
||||
let channel = video.channel
|
||||
|
||||
return Button("Open \(channel.name) channel") {
|
||||
navigationState.openChannel(channel)
|
||||
|
Loading…
Reference in New Issue
Block a user