Channels layout improvements, other UI fixes

This commit is contained in:
Arkadiusz Fal 2021-08-31 23:17:50 +02:00
parent 1651110a5d
commit b00b54ad2a
28 changed files with 633 additions and 192 deletions

View File

@ -0,0 +1,29 @@
import Foundation
extension Int {
func formattedAsAbbreviation() -> String {
typealias Abbrevation = (threshold: Double, divisor: Double, suffix: String)
let abbreviations: [Abbrevation] = [
(0, 1, ""), (1000.0, 1000.0, "K"),
(999_999.0, 1_000_000.0, "M"), (999_999_999.0, 1_000_000_000.0, "B")
]
let startValue = Double(abs(self))
guard let nextAbbreviationIndex = abbreviations.firstIndex(where: { startValue < $0.threshold }) else {
return String(self)
}
let abbreviation = abbreviations[abbreviations.index(before: nextAbbreviationIndex)]
let formatter = NumberFormatter()
formatter.positiveSuffix = abbreviation.suffix
formatter.negativeSuffix = abbreviation.suffix
formatter.allowsFloats = true
formatter.minimumIntegerDigits = 1
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 1
return formatter.string(from: NSNumber(value: Double(self) / abbreviation.divisor))!
}
}

View File

@ -13,7 +13,7 @@ extension Video {
views: 21534,
description: "Some relaxing live piano music",
genre: "Music",
channel: Channel(id: "AbCdEFgHI", name: "The Channel", subscriptionsCount: "2.3K"),
channel: Channel(id: "AbCdEFgHI", name: "The Channel", subscriptionsCount: 2300, videos: []),
thumbnails: Thumbnail.fixturesForAllQualities(videoId: id),
live: false,
upcoming: false,

View File

@ -3,20 +3,41 @@ import Defaults
import Foundation
import SwiftyJSON
struct Channel: Codable, Identifiable, Defaults.Serializable {
struct Channel: Identifiable, Hashable {
var id: String
var name: String
var subscriptionsCount: String
var videos = [Video]()
private var subscriptionsCount: Int?
private var subscriptionsText: String?
init(json: JSON) {
id = json["authorId"].stringValue
name = json["author"].stringValue
subscriptionsCount = json["subCountText"].stringValue
subscriptionsCount = json["subCount"].int
subscriptionsText = json["subCountText"].string
if let channelVideos = json.dictionaryValue["latestVideos"] {
videos = channelVideos.arrayValue.map(Video.init)
}
}
init(id: String, name: String, subscriptionsCount: String) {
init(id: String, name: String, subscriptionsCount: Int? = nil, videos: [Video] = []) {
self.id = id
self.name = name
self.subscriptionsCount = subscriptionsCount
self.videos = videos
}
var subscriptionsString: String? {
if subscriptionsCount != nil {
return subscriptionsCount!.formattedAsAbbreviation()
}
return subscriptionsText
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

View File

@ -75,12 +75,8 @@ final class InvidiousAPI: Service {
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.init)
}
return []
configureTransformer("/channels/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Channel in
Channel(json: content.json)
}
configureTransformer("/videos/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
@ -112,7 +108,7 @@ final class InvidiousAPI: Service {
resource("/auth/subscriptions").child(id)
}
func channelVideos(_ id: String) -> Resource {
func channel(_ id: String) -> Resource {
resource("/channels/\(id)")
}

View File

@ -8,9 +8,6 @@ final class NavigationState: ObservableObject {
@Published var tabSelection: TabSelection = .subscriptions
@Published var showingChannel = false
@Published var channel: Channel?
@Published var showingVideoDetails = false
@Published var showingVideo = false
@Published var video: Video?
@ -23,15 +20,40 @@ final class NavigationState: ObservableObject {
@Published var presentingUnsubscribeAlert = false
@Published var channelToUnsubscribe: Channel!
@Published var openChannels = Set<Channel>()
@Published var isChannelOpen = false
@Published var sidebarSectionChanged = false
func openChannel(_ channel: Channel) {
returnToDetails = false
self.channel = channel
showingChannel = true
openChannels.insert(channel)
isChannelOpen = true
tabSelection = .channel(channel.id)
}
func closeChannel() {
showingChannel = false
channel = nil
func closeChannel(_ channel: Channel) {
guard openChannels.remove(channel) != nil else {
return
}
isChannelOpen = !openChannels.isEmpty
if tabSelection == .channel(channel.id) {
tabSelection = .subscriptions
}
}
func closeAllChannels() {
isChannelOpen = false
openChannels.removeAll()
}
func showOpenChannel(_ id: Channel.ID) -> Bool {
if case .channel = tabSelection {
return false
} else {
return !openChannels.contains { $0.id == id }
}
}
func openVideoDetails(_ video: Video) {
@ -59,8 +81,10 @@ final class NavigationState: ObservableObject {
get: {
self.tabSelection
},
set: {
self.tabSelection = $0 ?? .subscriptions
set: { newValue in
if newValue != nil {
self.tabSelection = newValue!
}
}
)
}

View File

@ -17,29 +17,30 @@ final class Subscriptions: ObservableObject {
channels.sorted { $0.name.lowercased() < $1.name.lowercased() }
}
func subscribe(_ channelID: String) {
performChannelSubscriptionRequest(channelID, method: .post)
func subscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
performChannelSubscriptionRequest(channelID, method: .post, onSuccess: onSuccess)
}
func unsubscribe(_ channelID: String) {
performChannelSubscriptionRequest(channelID, method: .delete)
func unsubscribe(_ channelID: String, onSuccess: @escaping () -> Void = {}) {
performChannelSubscriptionRequest(channelID, method: .delete, onSuccess: onSuccess)
}
func subscribed(_ channelID: String) -> Bool {
func isSubscribing(_ channelID: String) -> Bool {
channels.contains { $0.id == channelID }
}
fileprivate func load() {
fileprivate func load(onSuccess: @escaping () -> Void = {}) {
resource.load().onSuccess { resource in
if let channels: [Channel] = resource.typedContent() {
self.channels = channels
onSuccess()
}
}
}
fileprivate func performChannelSubscriptionRequest(_ channelID: String, method: RequestMethod) {
fileprivate func performChannelSubscriptionRequest(_ channelID: String, method: RequestMethod, onSuccess: @escaping () -> Void = {}) {
InvidiousAPI.shared.channelSubscription(channelID).request(method).onCompletion { _ in
self.load()
self.load(onSuccess: onSuccess)
}
}
}

View File

@ -127,40 +127,15 @@ struct Video: Identifiable, Equatable {
}
var viewsCount: String? {
views != 0 ? formattedCount(views) : nil
views != 0 ? views.formattedAsAbbreviation() : nil
}
var likesCount: String? {
formattedCount(likes)
likes?.formattedAsAbbreviation()
}
var dislikesCount: String? {
formattedCount(dislikes)
}
func formattedCount(_ count: Int!) -> String? {
guard count != nil else {
return nil
}
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 1
var number: NSNumber
var unit: String
if count < 1000 {
return "\(count!)"
} else if count < 1_000_000 {
number = NSNumber(value: Double(count) / 1000.0)
unit = "K"
} else {
number = NSNumber(value: Double(count) / 1_000_000.0)
unit = "M"
}
return "\(formatter.string(from: number)!)\(unit)"
dislikes?.formattedAsAbbreviation()
}
var selectableStreams: [Stream] {

View File

@ -54,6 +54,8 @@
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
3748187026A769D60084E870 /* DetailBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186D26A769D60084E870 /* DetailBadge.swift */; };
3763495126DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */; };
3763495226DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */; };
376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
@ -73,8 +75,6 @@
377FC7DB267A080300A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7DA267A080300A6BBAF /* Logging */; };
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; };
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; };
377FC7E0267A082600A6BBAF /* ChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2892673AB89007FC770 /* ChannelView.swift */; };
377FC7E1267A082600A6BBAF /* ChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2892673AB89007FC770 /* ChannelView.swift */; };
377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoView.swift */; };
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoView.swift */; };
377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
@ -91,9 +91,14 @@
379775932689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
379775942689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
379775952689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
379DDFEE26DEDB0E00EA08E7 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DDFED26DEDB0E00EA08E7 /* EnvironmentValues.swift */; };
379DDFEF26DEDB0E00EA08E7 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DDFED26DEDB0E00EA08E7 /* EnvironmentValues.swift */; };
379DDFF026DEDB0E00EA08E7 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DDFED26DEDB0E00EA08E7 /* EnvironmentValues.swift */; };
379DDFF326DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DDFF226DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift */; };
379DDFF426DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DDFF226DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift */; };
379DDFF526DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DDFF226DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift */; };
37AAF27E26737323007FC770 /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; };
37AAF28026737550007FC770 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; };
37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2892673AB89007FC770 /* ChannelView.swift */; };
37AAF29026740715007FC770 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; };
37AAF29126740715007FC770 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; };
37AAF29226740715007FC770 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; };
@ -133,6 +138,11 @@
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */; };
37BA794B26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */; };
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */; };
37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; };
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; };
37BA795126DC3E0E002A0235 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; };
37BA796F26DC412E002A0235 /* Int+FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */; };
37BA797026DC426B002A0235 /* Int+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794E26DC3E0E002A0235 /* Int+Format.swift */; };
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */; };
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA42699FB72009BE4FB /* Alamofire */; };
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA6269A552E009BE4FB /* Alamofire */; };
@ -207,6 +217,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
37BA796726DC40CB002A0235 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 37D4B0BD2671614700C925CA /* Project object */;
proxyType = 1;
remoteGlobalIDString = 37D4B0CE2671614900C925CA;
remoteInfo = "Pearvidious (macOS)";
};
37D4B0D52671614900C925CA /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 37D4B0BD2671614700C925CA /* Project object */;
@ -248,6 +265,7 @@
3748186526A7627F0084E870 /* Video+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+Fixtures.swift"; sourceTree = "<group>"; };
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail+Fixtures.swift"; sourceTree = "<group>"; };
3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = "<group>"; };
3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecentlyOpened.swift; sourceTree = "<group>"; };
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; };
@ -257,9 +275,10 @@
3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = "<group>"; };
37992DC726CC50BC003D4C27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
379DDFED26DEDB0E00EA08E7 /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
379DDFF226DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = "<group>"; };
37AAF27D26737323007FC770 /* PopularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularView.swift; sourceTree = "<group>"; };
37AAF27F26737550007FC770 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
37AAF2892673AB89007FC770 /* ChannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelView.swift; sourceTree = "<group>"; };
37AAF28F26740715007FC770 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
37AAF29926740A01007FC770 /* VideosListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosListView.swift; sourceTree = "<group>"; };
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
@ -277,6 +296,9 @@
37BA794226DBA973002A0235 /* Playlists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlists.swift; sourceTree = "<group>"; };
37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarSubscriptions.swift; sourceTree = "<group>"; };
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarPlaylists.swift; sourceTree = "<group>"; };
37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = "<group>"; };
37BA796326DC40CB002A0235 /* Shared Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Shared Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
37BA796D26DC412E002A0235 /* Int+FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+FormatTests.swift"; sourceTree = "<group>"; };
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = "<group>"; };
37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarNavigation.swift; sourceTree = "<group>"; };
@ -314,6 +336,13 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
37BA796026DC40CB002A0235 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
37D4B0C62671614900C925CA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -380,6 +409,7 @@
children = (
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */,
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */,
3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */,
37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */,
37D4B0C32671614700C925CA /* AppTabNavigation.swift */,
37BD07B42698AA4D003EBB87 /* ContentView.swift */,
@ -480,6 +510,30 @@
path = iOS;
sourceTree = "<group>";
};
379DDFF126DEE2A800EA08E7 /* Modifiers */ = {
isa = PBXGroup;
children = (
379DDFF226DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift */,
);
path = Modifiers;
sourceTree = "<group>";
};
37BA796426DC40CB002A0235 /* Shared Tests */ = {
isa = PBXGroup;
children = (
37BA796C26DC4105002A0235 /* Extensions */,
);
path = "Shared Tests";
sourceTree = "<group>";
};
37BA796C26DC4105002A0235 /* Extensions */ = {
isa = PBXGroup;
children = (
37BA796D26DC412E002A0235 /* Int+FormatTests.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
37BE0BD826A214500092E2DB /* macOS */ = {
isa = PBXGroup;
children = (
@ -494,6 +548,7 @@
children = (
379775922689365600DD52A8 /* Array+Next.swift */,
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
);
path = Extensions;
@ -509,6 +564,7 @@
37D4B1B72672CFE300C925CA /* Model */,
37C7A9022679058300E721B4 /* Extensions */,
3748186426A762300084E870 /* Fixtures */,
37BA796426DC40CB002A0235 /* Shared Tests */,
377FC7D1267A080300A6BBAF /* Frameworks */,
37D4B0CA2671614900C925CA /* Products */,
37D4B174267164B000C925CA /* Tests Apple TV */,
@ -526,7 +582,9 @@
371AAE2526CEBF0B00901972 /* Trending */,
371AAE2726CEBF4700901972 /* Videos */,
371AAE2826CEC7D900901972 /* Views */,
379DDFF126DEE2A800EA08E7 /* Modifiers */,
372915E52687E3B900F5A35B /* Defaults.swift */,
379DDFED26DEDB0E00EA08E7 /* EnvironmentValues.swift */,
37D4B0C22671614700C925CA /* PearvidiousApp.swift */,
37D4B0C42671614800C925CA /* Assets.xcassets */,
37BD07C42698ADEE003EBB87 /* Pearvidious.entitlements */,
@ -543,6 +601,7 @@
37D4B0DE2671614900C925CA /* Tests macOS.xctest */,
37D4B158267164AE00C925CA /* Pearvidious (Apple TV).app */,
37D4B171267164B000C925CA /* Tests Apple TV.xctest */,
37BA796326DC40CB002A0235 /* Shared Tests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -568,7 +627,6 @@
children = (
371AAE2926CF143200901972 /* Options */,
373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */,
37AAF2892673AB89007FC770 /* ChannelView.swift */,
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */,
37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */,
37D4B15E267164AF00C925CA /* Assets.xcassets */,
@ -616,6 +674,24 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
37BA796226DC40CB002A0235 /* Shared Tests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 37BA796926DC40CB002A0235 /* Build configuration list for PBXNativeTarget "Shared Tests" */;
buildPhases = (
37BA795F26DC40CB002A0235 /* Sources */,
37BA796026DC40CB002A0235 /* Frameworks */,
37BA796126DC40CB002A0235 /* Resources */,
);
buildRules = (
);
dependencies = (
37BA796826DC40CB002A0235 /* PBXTargetDependency */,
);
name = "Shared Tests";
productName = "Shared Tests";
productReference = 37BA796326DC40CB002A0235 /* Shared Tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
37D4B0C82671614900C925CA /* Pearvidious (iOS) */ = {
isa = PBXNativeTarget;
buildConfigurationList = 37D4B0EC2671614900C925CA /* Build configuration list for PBXNativeTarget "Pearvidious (iOS)" */;
@ -753,6 +829,10 @@
LastSwiftUpdateCheck = 1300;
LastUpgradeCheck = 1300;
TargetAttributes = {
37BA796226DC40CB002A0235 = {
CreatedOnToolsVersion = 13.0;
TestTargetID = 37D4B0CE2671614900C925CA;
};
37D4B0C82671614900C925CA = {
CreatedOnToolsVersion = 13.0;
};
@ -803,11 +883,19 @@
37D4B0D32671614900C925CA /* Tests iOS */,
37D4B0DD2671614900C925CA /* Tests macOS */,
37D4B170267164B000C925CA /* Tests Apple TV */,
37BA796226DC40CB002A0235 /* Shared Tests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
37BA796126DC40CB002A0235 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
37D4B0C72671614900C925CA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -857,6 +945,15 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
37BA795F26DC40CB002A0235 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
37BA797026DC426B002A0235 /* Int+Format.swift in Sources */,
37BA796F26DC412E002A0235 /* Int+FormatTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
37D4B0C52671614900C925CA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -864,6 +961,7 @@
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
3763495126DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */,
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
@ -888,6 +986,7 @@
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */,
373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */,
379DDFF326DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift in Sources */,
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
@ -900,9 +999,9 @@
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
379775932689365600DD52A8 /* Array+Next.swift in Sources */,
377FC7E1267A082600A6BBAF /* ChannelView.swift in Sources */,
37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */,
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
37BA794B26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
@ -916,6 +1015,7 @@
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
37D4B19726717E1500C925CA /* Video.swift in Sources */,
371F2F1A269B43D300E4A7AB /* NavigationState.swift in Sources */,
379DDFEE26DEDB0E00EA08E7 /* EnvironmentValues.swift in Sources */,
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
@ -934,6 +1034,7 @@
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */,
37BA795026DC3E0E002A0235 /* Int+Format.swift in Sources */,
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
@ -959,7 +1060,7 @@
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
37F4AE7326828F0900BD60EA /* VideosCellsView.swift in Sources */,
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
377FC7E0267A082600A6BBAF /* ChannelView.swift in Sources */,
379DDFEF26DEDB0E00EA08E7 /* EnvironmentValues.swift in Sources */,
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
@ -975,6 +1076,7 @@
37D4B19826717E1500C925CA /* Video.swift in Sources */,
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
379DDFF426DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift in Sources */,
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
3711404026B206A6005B3555 /* SearchState.swift in Sources */,
37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
@ -984,6 +1086,7 @@
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
3763495226DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */,
37BA794426DBA973002A0235 /* Playlists.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1025,6 +1128,7 @@
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
37AAF29226740715007FC770 /* Channel.swift in Sources */,
379DDFF526DEE2BA00EA08E7 /* UnsubscribeAlertModifier.swift in Sources */,
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */,
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
@ -1040,6 +1144,7 @@
37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */,
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
37BA795126DC3E0E002A0235 /* Int+Format.swift in Sources */,
3748186C26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */,
@ -1052,7 +1157,7 @@
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
3711404126B206A6005B3555 /* SearchState.swift in Sources */,
379775952689365600DD52A8 /* Array+Next.swift in Sources */,
37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */,
379DDFF026DEDB0E00EA08E7 /* EnvironmentValues.swift in Sources */,
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
37141675267A8E10006CA35D /* Country.swift in Sources */,
@ -1079,6 +1184,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
37BA796826DC40CB002A0235 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 37D4B0CE2671614900C925CA /* Pearvidious (macOS) */;
targetProxy = 37BA796726DC40CB002A0235 /* PBXContainerItemProxy */;
};
37D4B0D62671614900C925CA /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 37D4B0C82671614900C925CA /* Pearvidious (iOS) */;
@ -1097,6 +1207,58 @@
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
37BA796A26DC40CB002A0235 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "net.arekf.Shared-Tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Pearvidious.app/Contents/MacOS/Pearvidious";
};
name = Debug;
};
37BA796B26DC40CB002A0235 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 78Z5H3M6RJ;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "net.arekf.Shared-Tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Pearvidious.app/Contents/MacOS/Pearvidious";
};
name = Release;
};
37D4B0EA2671614900C925CA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -1555,6 +1717,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
37BA796926DC40CB002A0235 /* Build configuration list for PBXNativeTarget "Shared Tests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
37BA796A26DC40CB002A0235 /* Debug */,
37BA796B26DC40CB002A0235 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
37D4B0C02671614700C925CA /* Build configuration list for PBXProject "Pearvidious" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -38,6 +38,16 @@
ReferencedContainer = "container:Pearvidious.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "37BA796226DC40CB002A0235"
BuildableName = "Shared Tests.xctest"
BlueprintName = "Shared Tests"
ReferencedContainer = "container:Pearvidious.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -0,0 +1,19 @@
import XCTest
final class IntFormatTests: XCTestCase {
func testFormattedAsAbbreviation() throws {
let samples: [Int: String] = [
1: "1",
999: "999",
1000: "1K",
1101: "1,1K",
12345: "12,3K",
123_456: "123,5K",
123_626_789: "123,6M"
]
samples.forEach { value, formatted in
XCTAssertEqual(value.formattedAsAbbreviation(), formatted)
}
}
}

View File

@ -0,0 +1,13 @@
import Foundation
import SwiftUI
private struct InNavigationViewKey: EnvironmentKey {
static let defaultValue = false
}
extension EnvironmentValues {
var inNavigationView: Bool {
get { self[InNavigationViewKey.self] }
set { self[InNavigationViewKey.self] = newValue }
}
}

View File

@ -0,0 +1,29 @@
import Foundation
import SwiftUI
struct UnsubscribeAlertModifier: ViewModifier {
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Subscriptions> private var subscriptions
func body(content: Content) -> some View {
content
.alert(unsubscribeAlertTitle, isPresented: $navigationState.presentingUnsubscribeAlert) {
if let channel = navigationState.channelToUnsubscribe {
Button("Unsubscribe", role: .destructive) {
subscriptions.unsubscribe(channel.id) {
navigationState.openChannel(channel)
navigationState.sidebarSectionChanged.toggle()
}
}
}
}
}
var unsubscribeAlertTitle: String {
if let channel = navigationState.channelToUnsubscribe {
return "Unsubscribe from \(channel.name)"
}
return "Unknown channel"
}
}

View File

@ -38,16 +38,27 @@ struct AppSidebarNavigation: View {
NavigationView {
sidebar
.frame(minWidth: 180)
Text("Select section")
}
}
var sidebar: some View {
List {
mainNavigationLinks
ScrollViewReader { scrollView in
List {
mainNavigationLinks
AppSidebarSubscriptions(selection: selection)
AppSidebarPlaylists(selection: selection)
Group {
AppSidebarRecentlyOpened(selection: selection)
.id("recentlyOpened")
AppSidebarSubscriptions(selection: selection)
AppSidebarPlaylists(selection: selection)
}
.onChange(of: navigationState.sidebarSectionChanged) { _ in
scrollScrollViewToItem(scrollView: scrollView, for: navigationState.tabSelection)
}
}
.listStyle(.sidebar)
}
#if os(macOS)
@ -61,48 +72,51 @@ struct AppSidebarNavigation: View {
var mainNavigationLinks: some View {
Group {
NavigationLink(tag: TabSelection.subscriptions, selection: selection) {
SubscriptionsView()
}
label: {
NavigationLink(destination: SubscriptionsView(), tag: TabSelection.subscriptions, selection: selection) {
Label("Subscriptions", systemImage: "star.circle.fill")
.accessibility(label: Text("Subscriptions"))
}
NavigationLink(tag: TabSelection.popular, selection: selection) {
PopularView()
}
label: {
NavigationLink(destination: PopularView(), tag: TabSelection.popular, selection: selection) {
Label("Popular", systemImage: "chart.bar")
.accessibility(label: Text("Popular"))
}
NavigationLink(tag: TabSelection.trending, selection: selection) {
TrendingView()
}
label: {
NavigationLink(destination: TrendingView(), tag: TabSelection.trending, selection: selection) {
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
.accessibility(label: Text("Trending"))
}
NavigationLink(tag: TabSelection.playlists, selection: selection) {
PlaylistsView()
}
label: {
NavigationLink(destination: PlaylistsView(), tag: TabSelection.playlists, selection: selection) {
Label("Playlists", systemImage: "list.and.film")
.accessibility(label: Text("Playlists"))
}
NavigationLink(tag: TabSelection.search, selection: selection) {
SearchView()
}
label: {
NavigationLink(destination: SearchView(), tag: TabSelection.search, selection: selection) {
Label("Search", systemImage: "magnifyingglass")
.accessibility(label: Text("Search"))
}
}
}
func scrollScrollViewToItem(scrollView: ScrollViewProxy, for selection: TabSelection) {
if case let .channel(id) = selection {
if subscriptions.isSubscribing(id) {
scrollView.scrollTo(id)
} else {
scrollView.scrollTo("recentlyOpened")
}
} else if case let .playlist(id) = selection {
scrollView.scrollTo(id)
}
}
#if os(macOS)
private func toggleSidebar() {
NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}
#endif
static func symbolSystemImage(_ name: String) -> String {
let firstLetter = name.first?.lowercased()
let regex = #"^[a-z0-9]$"#
@ -111,10 +125,4 @@ struct AppSidebarNavigation: View {
return "\(symbolName).square"
}
#if os(macOS)
private func toggleSidebar() {
NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}
#endif
}

View File

@ -15,6 +15,7 @@ struct AppSidebarPlaylists: View {
Label(playlist.title, systemImage: AppSidebarNavigation.symbolSystemImage(playlist.title))
.badge(Text("\(playlist.videos.count)"))
}
.id(playlist.id)
.contextMenu {
Button("Edit") {
navigationState.presentEditPlaylistForm(playlists.find(id: playlist.id))

View File

@ -0,0 +1,54 @@
import SwiftUI
struct AppSidebarRecentlyOpened: View {
@Binding var selection: TabSelection?
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Subscriptions> private var subscriptions
@State private var subscriptionsChanged = false
var body: some View {
Group {
if !recentlyOpened.isEmpty {
Section(header: Text("Recently Opened")) {
ForEach(recentlyOpened) { channel in
NavigationLink(tag: TabSelection.channel(channel.id), selection: $selection) {
ChannelVideosView(channel)
} label: {
HStack {
Label(channel.name, systemImage: AppSidebarNavigation.symbolSystemImage(channel.name))
Spacer()
Button(action: { navigationState.closeChannel(channel) }) {
Image(systemName: "xmark.circle.fill")
}
.foregroundColor(.secondary)
.buttonStyle(.plain)
}
}
// force recalculating the view on change of subscriptions
.opacity(subscriptionsChanged ? 1 : 1)
.id(channel.id)
.contextMenu {
Button("Subscribe") {
subscriptions.subscribe(channel.id) {
navigationState.sidebarSectionChanged.toggle()
}
}
}
}
}
.onChange(of: subscriptions.all) { _ in
subscriptionsChanged.toggle()
}
}
}
}
var recentlyOpened: [Channel] {
navigationState.openChannels.filter { !subscriptions.all.contains($0) }
}
}

View File

@ -19,13 +19,7 @@ struct AppSidebarSubscriptions: View {
navigationState.presentUnsubscribeAlert(channel)
}
}
.alert(unsubscribeAlertTitle, isPresented: $navigationState.presentingUnsubscribeAlert) {
if let channel = navigationState.channelToUnsubscribe {
Button("Unsubscribe", role: .destructive) {
subscriptions.unsubscribe(channel.id)
}
}
}
.modifier(UnsubscribeAlertModifier())
}
}
}

View File

@ -2,10 +2,10 @@ import Defaults
import SwiftUI
struct AppTabNavigation: View {
@State private var tabSelection: TabSelection = .subscriptions
@EnvironmentObject<NavigationState> private var navigationState
var body: some View {
TabView(selection: $tabSelection) {
TabView(selection: $navigationState.tabSelection) {
NavigationView {
SubscriptionsView()
}
@ -51,5 +51,19 @@ struct AppTabNavigation: View {
}
.tag(TabSelection.search)
}
.sheet(isPresented: $navigationState.isChannelOpen, onDismiss: {
navigationState.closeChannel(presentedChannel)
}) {
if presentedChannel != nil {
NavigationView {
ChannelVideosView(presentedChannel)
.environment(\.inNavigationView, true)
}
}
}
}
fileprivate var presentedChannel: Channel! {
navigationState.openChannels.first
}
}

View File

@ -59,7 +59,7 @@ struct PlaybackBar: View {
Image(systemName: "chevron.down.circle.fill")
}
.accessibilityLabel(Text("Close"))
.buttonStyle(BorderlessButtonStyle())
.buttonStyle(.borderless)
.foregroundColor(.gray)
.keyboardShortcut(.cancelAction)
}

View File

@ -26,8 +26,8 @@ struct VideoDetails: View {
Text(video.channel.name)
.font(.system(size: 13))
.bold()
if !video.channel.subscriptionsCount.isEmpty {
Text("\(video.channel.subscriptionsCount) subscribers")
if let subscribers = video.channel.subscriptionsString {
Text("\(subscribers) subscribers")
.font(.caption2)
}
}
@ -154,7 +154,7 @@ struct VideoDetails: View {
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding([.horizontal, .bottom])
.onAppear {
subscribed = subscriptions.subscribed(video.channel.id)
subscribed = subscriptions.isSubscribing(video.channel.id)
}
}

View File

@ -89,10 +89,9 @@ struct VideoPlayerView: View {
navigationState.showingVideoDetails = navigationState.returnToDetails
}
#if os(macOS)
.navigationTitle(video.title)
.frame(maxWidth: 1000, minHeight: 700)
#elseif os(iOS)
.navigationBarTitle(video.title, displayMode: .inline)
.navigationBarHidden(true)
#endif
}

View File

@ -44,7 +44,7 @@ struct PlaylistFormView: View {
Text(visibility.name)
}
}
.pickerStyle(SegmentedPickerStyle())
.pickerStyle(.segmented)
}
Divider()
.padding(.vertical, 4)

View File

@ -8,36 +8,50 @@ struct VideoView: View {
@Environment(\.verticalSizeClass) private var verticalSizeClass
#endif
@Environment(\.inNavigationView) private var inNavigationView
var video: Video
var layout: ListingLayout
var body: some View {
Button(action: { navigationState.playVideo(video) }) {
VStack {
if layout == .cells {
#if os(iOS)
if verticalSizeClass == .compact {
horizontalRow
.padding(.vertical, 4)
} else {
verticalRow
}
#else
verticalRow
#endif
} else {
horizontalRow
Group {
if inNavigationView {
NavigationLink(destination: VideoPlayerView(video)) {
content
}
} else {
Button(action: { navigationState.playVideo(video) }) {
content
}
}
#if os(macOS)
.background()
#endif
}
.modifier(ButtonStyleModifier(layout: layout))
.contentShape(RoundedRectangle(cornerRadius: 12))
.contextMenu { VideoContextMenuView(video: video) }
}
var content: some View {
VStack {
if layout == .cells {
#if os(iOS)
if verticalSizeClass == .compact {
horizontalRow
.padding(.vertical, 4)
} else {
verticalRow
}
#else
verticalRow
#endif
} else {
horizontalRow
}
}
#if os(macOS)
.background()
#endif
}
var horizontalRow: some View {
HStack(alignment: .top, spacing: 2) {
Section {
@ -228,7 +242,7 @@ struct VideoListRowPreview: PreviewProvider {
VideoView(video: video, layout: .list)
}
}
.listStyle(GroupedListStyle())
.listStyle(.grouped)
HStack {
ForEach(Video.allFixtures) { video in

View File

@ -24,7 +24,7 @@ struct VideosListView: View {
}
}
}
.listStyle(GroupedListStyle())
.listStyle(.grouped)
}
}
}

View File

@ -4,8 +4,6 @@ import SwiftUI
struct VideosView: View {
@EnvironmentObject<NavigationState> private var navigationState
@State private var profile = Profile()
#if os(tvOS)
@Default(.layout) private var layout
#endif

View File

@ -2,26 +2,119 @@ import Siesta
import SwiftUI
struct ChannelVideosView: View {
@ObservedObject private var store = Store<[Video]>()
let channel: Channel
var resource: Resource {
InvidiousAPI.shared.channelVideos(channel.id)
}
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Subscriptions> private var subscriptions
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.dismiss) private var dismiss
#if os(iOS)
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
#endif
@ObservedObject private var store = Store<Channel>()
@Namespace private var focusNamespace
init(_ channel: Channel) {
self.channel = channel
resource.addObserver(store)
}
var body: some View {
VideosView(videos: store.collection)
#if !os(tvOS)
.navigationTitle("\(channel.name) Channel")
VStack {
#if os(tvOS)
HStack {
Text(navigationTitle)
.font(.title2)
.frame(alignment: .leading)
Spacer()
if let subscribers = store.item?.subscriptionsString {
Text("**\(subscribers)** subscribers")
.foregroundColor(.secondary)
}
subscriptionToggleButton
}
.frame(maxWidth: .infinity)
#endif
VideosView(videos: store.item?.videos ?? [])
#if !os(iOS)
.prefersDefaultFocus(in: focusNamespace)
#endif
}
#if !os(iOS)
.focusScope(focusNamespace)
#endif
.onAppear {
resource.loadIfNeeded()
#if !os(tvOS)
.toolbar {
ToolbarItem(placement: subscriptionToolbarItemPlacement) {
HStack {
if let channel = store.item, let subscribers = channel.subscriptionsString {
Text("**\(subscribers)** subscribers")
.foregroundColor(.secondary)
}
subscriptionToggleButton
}
}
ToolbarItem(placement: .cancellationAction) {
if inNavigationView {
Button("Done") {
dismiss()
}
}
}
}
#endif
.modifier(UnsubscribeAlertModifier())
.onAppear {
resource.loadIfNeeded()
}
.navigationTitle(navigationTitle)
}
var resource: Resource {
InvidiousAPI.shared.channel(channel.id)
}
#if !os(tvOS)
var subscriptionToolbarItemPlacement: ToolbarItemPlacement {
#if os(iOS)
if horizontalSizeClass == .regular {
return .primaryAction
}
#endif
return .status
}
#endif
var subscriptionToggleButton: some View {
Group {
if subscriptions.isSubscribing(channel.id) {
Button("Unsubscribe") {
navigationState.presentUnsubscribeAlert(channel)
}
} else {
Button("Subscribe") {
subscriptions.subscribe(channel.id) {
navigationState.sidebarSectionChanged.toggle()
}
}
}
}
}
var navigationTitle: String {
store.item?.name ?? channel.name
}
}

View File

@ -14,7 +14,9 @@ struct VideoContextMenuView: View {
var body: some View {
Section {
openChannelButton
if navigationState.showOpenChannel(video.channel.id) {
openChannelButton
}
subscriptionButton
.opacity(subscribed ? 1 : 1)
@ -32,18 +34,25 @@ struct VideoContextMenuView: View {
var openChannelButton: some View {
Button("\(video.author) Channel") {
navigationState.openChannel(video.channel)
navigationState.sidebarSectionChanged.toggle()
}
}
var subscriptionButton: some View {
Group {
if subscriptions.subscribed(video.channel.id) {
if subscriptions.isSubscribing(video.channel.id) {
Button("Unsubscribe", role: .destructive) {
subscriptions.unsubscribe(video.channel.id)
#if os(tvOS)
subscriptions.unsubscribe(video.channel.id)
#else
navigationState.presentUnsubscribeAlert(video.channel)
#endif
}
} else {
Button("Subscribe") {
subscriptions.subscribe(video.channel.id)
subscriptions.subscribe(video.channel.id) {
navigationState.sidebarSectionChanged.toggle()
}
}
}
}

View File

@ -1,36 +0,0 @@
import Siesta
import SwiftUI
struct ChannelView: View {
@ObservedObject private var store = Store<[Video]>()
var id: String
var resource: Resource {
InvidiousAPI.shared.channelVideos(id)
}
init(id: String) {
self.id = id
resource.addObserver(store)
}
var body: some View {
HStack {
Spacer()
VStack {
Spacer()
VideosView(videos: store.collection)
.onAppear {
resource.loadIfNeeded()
}
Spacer()
}
Spacer()
}
.edgesIgnoringSafeArea(.all)
.background(.ultraThickMaterial)
}
}

View File

@ -38,21 +38,26 @@ struct TVNavigationView: View {
VideoDetailsView(video)
}
}
.fullScreenCover(isPresented: $navigationState.showingChannel, onDismiss: {
navigationState.showVideoDetailsIfNeeded()
}) {
if let channel = navigationState.channel {
ChannelView(id: channel.id)
}
}
.fullScreenCover(isPresented: $navigationState.showingVideo) {
if let video = navigationState.video {
VideoPlayerView(video)
.environmentObject(playbackState)
}
}
.fullScreenCover(isPresented: $navigationState.isChannelOpen, onDismiss: {
navigationState.closeChannel(presentedChannel)
}) {
if presentedChannel != nil {
ChannelVideosView(presentedChannel)
.background(.thickMaterial)
}
}
.onPlayPauseCommand { showingOptions.toggle() }
}
fileprivate var presentedChannel: Channel! {
navigationState.openChannels.first
}
}
struct TVNavigationView_Previews: PreviewProvider {