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