Player bar visibility modes and settings

This commit is contained in:
Arkadiusz Fal 2022-12-17 19:35:07 +01:00
parent 8e5bafba58
commit fcf527fa87
20 changed files with 320 additions and 168 deletions

View File

@ -574,7 +574,9 @@ final class PlayerModel: ObservableObject {
closePiP() closePiP()
prepareCurrentItemForHistory(finished: finished) prepareCurrentItemForHistory(finished: finished)
currentItem = nil withAnimation {
currentItem = nil
}
updateNowPlayingInfo() updateNowPlayingInfo()
backend.closeItem() backend.closeItem()

View File

@ -48,7 +48,9 @@ extension PlayerModel {
comments.reset() comments.reset()
stream = nil stream = nil
currentItem = item withAnimation {
currentItem = item
}
if !time.isNil { if !time.isNil {
currentItem.playbackTime = time currentItem.playbackTime = time
@ -204,7 +206,9 @@ extension PlayerModel {
let item = PlayerQueueItem(video, playbackTime: atTime) let item = PlayerQueueItem(video, playbackTime: atTime)
if play { if play {
currentItem = item withAnimation {
currentItem = item
}
videoBeingOpened = video videoBeingOpened = video
} }

View File

@ -20,7 +20,7 @@ struct ChannelCell: View {
} }
var navigationLink: some View { var navigationLink: some View {
NavigationLink(destination: ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) { NavigationLink(destination: ChannelVideosView(channel: channel)) {
labelContent labelContent
} }
} }

View File

@ -41,6 +41,7 @@ struct ChannelLinkView<ChannelLabel: View>: View {
@ViewBuilder private var channelNavigationLink: some View { @ViewBuilder private var channelNavigationLink: some View {
NavigationLink(destination: ChannelVideosView(channel: channel)) { NavigationLink(destination: ChannelVideosView(channel: channel)) {
channelLabel channelLabel
.lineLimit(1)
} }
} }

View File

@ -32,6 +32,10 @@ extension Defaults.Keys {
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10) static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10)
static let favorites = Key<[FavoriteItem]>("favorites", default: []) static let favorites = Key<[FavoriteItem]>("favorites", default: [])
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .togglePlayerVisibility)
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
#if !os(tvOS) #if !os(tvOS)
#if os(macOS) #if os(macOS)
static let accountPickerDisplaysUsernameDefault = true static let accountPickerDisplaysUsernameDefault = true
@ -363,3 +367,23 @@ enum DetailsToolbarPositionSetting: String, CaseIterable, Defaults.Serializable
self == .center || self == .left self == .center || self == .left
} }
} }
enum PlayerTapGestureAction: String, CaseIterable, Defaults.Serializable {
case togglePlayerVisibility
case togglePlayer
case openChannel
case nothing
var label: String {
switch self {
case .togglePlayerVisibility:
return "Toggle size"
case .togglePlayer:
return "Toggle player"
case .openChannel:
return "Open channel"
case .nothing:
return "Do nothing"
}
}
}

View File

@ -170,23 +170,20 @@ struct FavoriteItemView: View {
} }
@ViewBuilder var itemNavigationLinkDestination: some View { @ViewBuilder var itemNavigationLinkDestination: some View {
Group { switch item.section {
switch item.section { case let .channel(_, id, name):
case let .channel(_, id, name): ChannelVideosView(channel: .init(app: .invidious, id: id, name: name))
ChannelVideosView(channel: .init(app: .invidious, id: id, name: name)) case let .channelPlaylist(_, id, title):
case let .channelPlaylist(_, id, title): ChannelPlaylistView(playlist: .init(id: id, title: title))
ChannelPlaylistView(playlist: .init(id: id, title: title)) case let .playlist(_, id):
case let .playlist(_, id): ChannelPlaylistView(playlist: .init(id: id, title: label))
ChannelPlaylistView(playlist: .init(id: id, title: label)) case .subscriptions:
case .subscriptions: SubscriptionsView()
SubscriptionsView() case .popular:
case .popular: PopularView()
PopularView() default:
default: EmptyView()
EmptyView()
}
} }
.modifier(PlayerOverlayModifier())
} }
func itemButtonAction() { func itemButtonAction() {

View File

@ -1,11 +1,38 @@
import Defaults
import Foundation import Foundation
import SwiftUI import SwiftUI
struct PlayerOverlayModifier: ViewModifier { struct PlayerOverlayModifier: ViewModifier {
@ObservedObject private var player = PlayerModel.shared
@State private var expansionState = ControlsBar.ExpansionState.mini
@Environment(\.navigationStyle) private var navigationStyle
@Default(.playerButtonShowsControlButtonsWhenMinimized) private var controlsWhenMinimized
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
#if !os(tvOS) #if !os(tvOS)
.overlay(ControlsBar(fullScreen: .constant(false)), alignment: .bottom) .overlay(overlay, alignment: .bottomTrailing)
#endif #endif
} }
@ViewBuilder var overlay: some View {
Group {
if player.currentItem != nil {
ControlsBar(fullScreen: .constant(false), expansionState: $expansionState, playerBar: true)
.offset(x: expansionState == .mini && !controlsWhenMinimized ? 10 : 0, y: 0)
.transition(.opacity)
}
}
.animation(.default, value: player.currentItem)
}
}
struct PlayerOverlayModifier_Previews: PreviewProvider {
static var previews: some View {
HStack {}
.frame(maxWidth: .infinity, maxHeight: 100)
.modifier(PlayerOverlayModifier())
}
} }

View File

@ -52,6 +52,7 @@ struct AppSidebarNavigation: View {
} }
} }
} }
.modifier(PlayerOverlayModifier())
.environment(\.navigationStyle, .sidebar) .environment(\.navigationStyle, .sidebar)
} }
@ -75,7 +76,7 @@ struct AppSidebarNavigation: View {
} }
} }
ToolbarItemGroup(placement: accountsMenuToolbarItemPlacement) { ToolbarItemGroup {
AccountViewButton() AccountViewButton()
} }

View File

@ -10,7 +10,7 @@ struct AppSidebarPlaylists: View {
Section(header: Text("Playlists")) { Section(header: Text("Playlists")) {
ForEach(playlists.playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }) { playlist in ForEach(playlists.playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }) { playlist in
NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) { NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) {
LazyView(PlaylistVideosView(playlist).modifier(PlayerOverlayModifier())) LazyView(PlaylistVideosView(playlist))
} label: { } label: {
playlistLabel(playlist) playlistLabel(playlist)
} }

View File

@ -16,17 +16,17 @@ struct AppSidebarRecents: View {
switch recent.type { switch recent.type {
case .channel: case .channel:
RecentNavigationLink(recent: recent) { RecentNavigationLink(recent: recent) {
LazyView(ChannelVideosView(channel: recent.channel!).modifier(PlayerOverlayModifier())) LazyView(ChannelVideosView(channel: recent.channel!))
} }
case .playlist: case .playlist:
RecentNavigationLink(recent: recent, systemImage: "list.and.film") { RecentNavigationLink(recent: recent, systemImage: "list.and.film") {
LazyView(ChannelPlaylistView(playlist: recent.playlist!).modifier(PlayerOverlayModifier())) LazyView(ChannelPlaylistView(playlist: recent.playlist!))
} }
case .query: case .query:
RecentNavigationLink(recent: recent, systemImage: "magnifyingglass") { RecentNavigationLink(recent: recent, systemImage: "magnifyingglass") {
LazyView(SearchView(recent.query!).modifier(PlayerOverlayModifier())) LazyView(SearchView(recent.query!))
} }
} }
} }

View File

@ -12,7 +12,7 @@ struct AppSidebarSubscriptions: View {
Section(header: Text("Subscriptions")) { Section(header: Text("Subscriptions")) {
ForEach(subscriptions.all) { channel in ForEach(subscriptions.all) { channel in
NavigationLink(tag: TabSelection.channel(channel.id), selection: $navigation.tabSelection) { NavigationLink(tag: TabSelection.channel(channel.id), selection: $navigation.tabSelection) {
LazyView(ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) LazyView(ChannelVideosView(channel: channel))
} label: { } label: {
HStack { HStack {
if channel.thumbnailURL != nil { if channel.thumbnailURL != nil {

View File

@ -47,7 +47,7 @@ struct AppTabNavigation: View {
searchNavigationView searchNavigationView
} }
} }
.overlay(ControlsBar(fullScreen: .constant(false)), alignment: .bottom) .modifier(PlayerOverlayModifier())
} }
.onAppear { .onAppear {
feed.calculateUnwatchedFeed() feed.calculateUnwatchedFeed()

View File

@ -53,7 +53,7 @@ struct Sidebar: View {
var mainNavigationLinks: some View { var mainNavigationLinks: some View {
Section(header: Text("Videos")) { Section(header: Text("Videos")) {
if showHome { if showHome {
NavigationLink(destination: LazyView(HomeView().modifier(PlayerOverlayModifier())), tag: TabSelection.home, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(HomeView()), tag: TabSelection.home, selection: $navigation.tabSelection) {
Label("Home", systemImage: "house") Label("Home", systemImage: "house")
.accessibility(label: Text("Home")) .accessibility(label: Text("Home"))
} }
@ -62,7 +62,7 @@ struct Sidebar: View {
#if os(iOS) #if os(iOS)
if showDocuments { if showDocuments {
NavigationLink(destination: LazyView(DocumentsView().modifier(PlayerOverlayModifier())), tag: TabSelection.documents, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(DocumentsView()), tag: TabSelection.documents, selection: $navigation.tabSelection) {
Label("Documents", systemImage: "folder") Label("Documents", systemImage: "folder")
.accessibility(label: Text("Documents")) .accessibility(label: Text("Documents"))
} }
@ -74,7 +74,7 @@ struct Sidebar: View {
if visibleSections.contains(.subscriptions), if visibleSections.contains(.subscriptions),
accounts.app.supportsSubscriptions && accounts.signedIn accounts.app.supportsSubscriptions && accounts.signedIn
{ {
NavigationLink(destination: LazyView(SubscriptionsView().modifier(PlayerOverlayModifier())), tag: TabSelection.subscriptions, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: $navigation.tabSelection) {
Label("Subscriptions", systemImage: "star.circle") Label("Subscriptions", systemImage: "star.circle")
.accessibility(label: Text("Subscriptions")) .accessibility(label: Text("Subscriptions"))
} }
@ -88,7 +88,7 @@ struct Sidebar: View {
} }
if visibleSections.contains(.popular), accounts.app.supportsPopular { if visibleSections.contains(.popular), accounts.app.supportsPopular {
NavigationLink(destination: LazyView(PopularView().modifier(PlayerOverlayModifier())), tag: TabSelection.popular, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: $navigation.tabSelection) {
Label("Popular", systemImage: "arrow.up.right.circle") Label("Popular", systemImage: "arrow.up.right.circle")
.accessibility(label: Text("Popular")) .accessibility(label: Text("Popular"))
} }
@ -96,14 +96,14 @@ struct Sidebar: View {
} }
if visibleSections.contains(.trending) { if visibleSections.contains(.trending) {
NavigationLink(destination: LazyView(TrendingView().modifier(PlayerOverlayModifier())), tag: TabSelection.trending, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) {
Label("Trending", systemImage: "chart.bar") Label("Trending", systemImage: "chart.bar")
.accessibility(label: Text("Trending")) .accessibility(label: Text("Trending"))
} }
.id("trending") .id("trending")
} }
NavigationLink(destination: LazyView(SearchView().modifier(PlayerOverlayModifier())), tag: TabSelection.search, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(SearchView()), tag: TabSelection.search, selection: $navigation.tabSelection) {
Label("Search", systemImage: "magnifyingglass") Label("Search", systemImage: "magnifyingglass")
.accessibility(label: Text("Search")) .accessibility(label: Text("Search"))
} }
@ -159,3 +159,9 @@ struct Sidebar: View {
scrollView.scrollTo(selection.stringValue) scrollView.scrollTo(selection.stringValue)
} }
} }
struct Sidebar_Previews: PreviewProvider {
static var previews: some View {
Sidebar()
}
}

View File

@ -92,7 +92,7 @@ struct PlayerControls: View {
model.presentingDetailsOverlay = true model.presentingDetailsOverlay = true
} }
} label: { } label: {
ControlsBar(fullScreen: $model.presentingDetailsOverlay, presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false) ControlsBar(fullScreen: $model.presentingDetailsOverlay, expansionState: .constant(.full), presentingControls: false, detailsTogglePlayer: false, detailsToggleFullScreen: false)
.clipShape(RoundedRectangle(cornerRadius: 4)) .clipShape(RoundedRectangle(cornerRadius: 4))
.frame(maxWidth: 300, alignment: .leading) .frame(maxWidth: 300, alignment: .leading)
} }

View File

@ -39,6 +39,7 @@ struct VideoDetails: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
ControlsBar( ControlsBar(
fullScreen: $fullScreen, fullScreen: $fullScreen,
expansionState: .constant(.full),
presentingControls: false, presentingControls: false,
backgroundEnabled: false, backgroundEnabled: false,
borderTop: false, borderTop: false,

View File

@ -21,6 +21,9 @@ struct BrowsingSettings: View {
@Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem @Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem
@Default(.homeHistoryItems) private var homeHistoryItems @Default(.homeHistoryItems) private var homeHistoryItems
@Default(.visibleSections) private var visibleSections @Default(.visibleSections) private var visibleSections
@Default(.playerButtonSingleTapGesture) private var playerButtonSingleTapGesture
@Default(.playerButtonDoubleTapGesture) private var playerButtonDoubleTapGesture
@Default(.playerButtonShowsControlButtonsWhenMinimized) private var playerButtonShowsControlButtonsWhenMinimized
@ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var accounts = AccountsModel.shared
@ -65,6 +68,7 @@ struct BrowsingSettings: View {
interface interface
} }
#else #else
playerBarSettings
interface interface
#endif #endif
if !accounts.isEmpty { if !accounts.isEmpty {
@ -150,6 +154,32 @@ struct BrowsingSettings: View {
} }
} }
#if !os(tvOS)
private var playerBarSettings: some View {
Section(header: SettingsHeader(text: "Player Bar".localized()), footer: playerBarFooter) {
Toggle("Always show controls buttons", isOn: $playerButtonShowsControlButtonsWhenMinimized)
playerBarGesturePicker("Single tap gesture", selection: $playerButtonSingleTapGesture)
playerBarGesturePicker("Double tap gesture", selection: $playerButtonDoubleTapGesture)
}
}
func playerBarGesturePicker(_ label: String, selection: Binding<PlayerTapGestureAction>) -> some View {
Picker(label, selection: selection) {
ForEach(PlayerTapGestureAction.allCases, id: \.rawValue) { action in
Text(action.label).tag(action)
}
}
}
var playerBarFooter: some View {
#if os(iOS)
Text("Tap and hold channel thumbnail to open context menu with more actions")
#elseif os(macOS)
Text("Right click channel thumbnail to open context menu with more actions")
#endif
}
#endif
private var interfaceSettings: some View { private var interfaceSettings: some View {
Section(header: SettingsHeader(text: "Interface".localized())) { Section(header: SettingsHeader(text: "Interface".localized())) {
#if !os(tvOS) #if !os(tvOS)

View File

@ -10,7 +10,7 @@ struct SettingsView: View {
case browsing, player, quality, history, sponsorBlock, locations, advanced, help case browsing, player, quality, history, sponsorBlock, locations, advanced, help
} }
@State private var selection: Tabs? @State private var selection: Tabs = .browsing
#endif #endif
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@ -224,10 +224,8 @@ struct SettingsView: View {
#if os(macOS) #if os(macOS)
private var windowHeight: Double { private var windowHeight: Double {
switch selection { switch selection {
case nil:
return accounts.isEmpty ? 680 : 580
case .browsing: case .browsing:
return 580 return 680
case .player: case .player:
return 900 return 900
case .quality: case .quality:

View File

@ -14,7 +14,7 @@ struct ChannelsView: View {
List { List {
Section(header: header) { Section(header: header) {
ForEach(subscriptions.all) { channel in ForEach(subscriptions.all) { channel in
NavigationLink(destination: ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) { NavigationLink(destination: ChannelVideosView(channel: channel)) {
HStack { HStack {
if let url = channel.thumbnailURLOrCached { if let url = channel.thumbnailURLOrCached {
ThumbnailView(url: url) ThumbnailView(url: url)

View File

@ -3,20 +3,15 @@ import SDWebImageSwiftUI
import SwiftUI import SwiftUI
struct ControlsBar: View { struct ControlsBar: View {
@Binding var fullScreen: Bool enum ExpansionState {
case mini
case full
}
@Binding var fullScreen: Bool
@State private var presentingShareSheet = false @State private var presentingShareSheet = false
@State private var shareURL: URL? @State private var shareURL: URL?
@Binding var expansionState: ExpansionState
@Environment(\.navigationStyle) private var navigationStyle
@ObservedObject private var accounts = AccountsModel.shared
var navigation = NavigationModel.shared
@ObservedObject private var model = PlayerModel.shared
@ObservedObject private var playlists = PlaylistsModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@ObservedObject private var controls = PlayerControlsModel.shared
var presentingControls = true var presentingControls = true
var backgroundEnabled = true var backgroundEnabled = true
@ -24,34 +19,57 @@ struct ControlsBar: View {
var borderBottom = true var borderBottom = true
var detailsTogglePlayer = true var detailsTogglePlayer = true
var detailsToggleFullScreen = false var detailsToggleFullScreen = false
var playerBar = false
var titleLineLimit = 2 var titleLineLimit = 2
@ObservedObject private var accounts = AccountsModel.shared
@ObservedObject private var model = PlayerModel.shared
@ObservedObject private var playlists = PlaylistsModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@ObservedObject private var controls = PlayerControlsModel.shared
@Environment(\.navigationStyle) private var navigationStyle
private let navigation = NavigationModel.shared
private let controlsOverlayModel = ControlOverlaysModel.shared private let controlsOverlayModel = ControlOverlaysModel.shared
@Default(.playerButtonShowsControlButtonsWhenMinimized) private var controlsWhenMinimized
@Default(.playerButtonSingleTapGesture) private var playerButtonSingleTapGesture
@Default(.playerButtonDoubleTapGesture) private var playerButtonDoubleTapGesture
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
detailsButton detailsButton
if presentingControls { if presentingControls, expansionState == .full || (controlsWhenMinimized && model.currentItem != nil) {
if expansionState == .full {
Spacer()
}
controlsView controlsView
.frame(maxWidth: 120) .frame(maxWidth: 120)
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.padding(.horizontal) .padding(.horizontal, 10)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight) .padding(.vertical, 2)
.borderTop(height: borderTop ? 0.5 : 0, color: Color("ControlsBorderColor")) .frame(maxHeight: barHeight)
.borderBottom(height: borderBottom ? 0.5 : 0, color: Color("ControlsBorderColor")) .padding(.trailing, expansionState == .mini && !controlsWhenMinimized ? 8 : 0)
.modifier(ControlBackgroundModifier(enabled: backgroundEnabled, edgesIgnoringSafeArea: .bottom)) .modifier(ControlBackgroundModifier(enabled: backgroundEnabled))
.clipShape(RoundedRectangle(cornerRadius: expansionState == .full || !playerBar ? 0 : 6))
.overlay(
RoundedRectangle(cornerRadius: expansionState == .full || !playerBar ? 0 : 6)
.stroke(Color("ControlsBorderColor"), lineWidth: playerBar ? 0 : 0.5)
)
#if os(iOS) #if os(iOS)
.background( .background(
EmptyView().sheet(isPresented: $presentingShareSheet) { EmptyView().sheet(isPresented: $presentingShareSheet) {
if let shareURL { if let shareURL {
ShareSheet(activityItems: [shareURL]) ShareSheet(activityItems: [shareURL])
}
} }
) }
)
#endif #endif
} }
@ -136,139 +154,187 @@ struct ControlsBar: View {
var details: some View { var details: some View {
HStack { HStack {
HStack(spacing: 8) { HStack(spacing: 8) {
Button { if !playerBar {
if let video = model.currentVideo, !video.isLocal { Button {
navigation.openChannel( if let video = model.currentVideo, !video.isLocal {
video.channel, navigation.openChannel(
navigationStyle: navigationStyle video.channel,
navigationStyle: navigationStyle
)
}
} label: {
ChannelAvatarView(
channel: model.currentVideo?.channel,
video: model.currentVideo
) )
.frame(width: barHeight - 10, height: barHeight - 10)
} }
} label: { .contextMenu { contextMenu }
.zIndex(3)
} else {
ChannelAvatarView( ChannelAvatarView(
channel: model.currentVideo?.channel, channel: model.currentVideo?.channel,
video: model.currentVideo video: model.currentVideo
) )
#if !os(tvOS)
.highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil)
.gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil)
#endif
.frame(width: barHeight - 10, height: barHeight - 10) .frame(width: barHeight - 10, height: barHeight - 10)
.contextMenu { contextMenu }
} }
.contextMenu {
if let video = model.currentVideo {
Group {
Section {
if accounts.app.supportsUserPlaylists && accounts.signedIn, !video.isLocal {
Section {
Button {
navigation.presentAddToPlaylist(video)
} label: {
Label("Add to Playlist...", systemImage: "text.badge.plus")
}
if let playlist = playlists.lastUsed, let video = model.currentVideo { if expansionState == .full {
Button { VStack(alignment: .leading, spacing: 0) {
playlists.addVideo(playlistID: playlist.id, videoID: video.videoID) let notPlaying = "Not Playing".localized()
} label: { Text(model.currentVideo?.displayTitle ?? notPlaying)
Label("Add to \(playlist.title)", systemImage: "text.badge.star") .font(.system(size: 14))
} .fontWeight(.semibold)
} .foregroundColor(model.currentVideo.isNil ? .secondary : .accentColor)
} .fixedSize(horizontal: false, vertical: true)
} .lineLimit(titleLineLimit)
.multilineTextAlignment(.leading)
#if !os(tvOS) if let video = model.currentVideo, !video.localStreamIsFile {
ShareButton(contentItem: .init(video: model.currentVideo)) HStack(spacing: 2) {
#endif Text(video.displayAuthor)
.font(.system(size: 12))
Section { if !presentingControls && !video.isLocal {
if !video.isLocal { HStack(spacing: 2) {
Button { Image(systemName: "person.2.fill")
navigation.openChannel(
video.channel,
navigationStyle: navigationStyle
)
} label: {
Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
}
if accounts.app.supportsSubscriptions, accounts.signedIn { if let channel = model.currentVideo?.channel {
if subscriptions.isSubscribing(video.channel.id) { if let subscriptions = channel.subscriptionsString {
Button { Text(subscriptions)
#if os(tvOS)
subscriptions.unsubscribe(video.channel.id)
#else
navigation.presentUnsubscribeAlert(video.channel, subscriptions: subscriptions)
#endif
} label: {
Label("Unsubscribe", systemImage: "star.circle")
}
} else { } else {
Button { Text("1234").redacted(reason: .placeholder)
subscriptions.subscribe(video.channel.id) {
navigation.sidebarSectionChanged.toggle()
}
} label: {
Label("Subscribe", systemImage: "star.circle")
}
} }
} }
} }
.padding(.leading, 4)
.font(.system(size: 9))
} }
} }
.lineLimit(1)
Button { .foregroundColor(.secondary)
model.closeCurrentItem()
} label: {
Label("Close Video", systemImage: "xmark")
}
} }
.labelStyle(.automatic)
} }
} .zIndex(0)
.transition(.opacity)
VStack(alignment: .leading, spacing: 0) { if !playerBar {
let notPlaying = "Not Playing".localized() Spacer()
Text(model.currentVideo?.displayTitle ?? notPlaying)
.font(.system(size: 14))
.fontWeight(.semibold)
.foregroundColor(model.currentVideo.isNil ? .secondary : .accentColor)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(titleLineLimit)
.multilineTextAlignment(.leading)
if let video = model.currentVideo, !video.localStreamIsFile {
HStack(spacing: 2) {
Text(video.displayAuthor)
.font(.system(size: 12))
if !presentingControls && !video.isLocal {
HStack(spacing: 2) {
Image(systemName: "person.2.fill")
if let channel = model.currentVideo?.channel {
if let subscriptions = channel.subscriptionsString {
Text(subscriptions)
} else {
Text("1234").redacted(reason: .placeholder)
}
}
}
.padding(.leading, 4)
.font(.system(size: 9))
}
}
.lineLimit(1)
.foregroundColor(.secondary)
} }
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(.vertical) .padding(.vertical)
}
}
Spacer() #if !os(tvOS)
var singleTapGesture: some Gesture {
TapGesture(count: 1).onEnded { gestureAction(playerButtonSingleTapGesture) }
}
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded { gestureAction(playerButtonDoubleTapGesture) }
}
func gestureAction(_ action: PlayerTapGestureAction) {
switch action {
case .togglePlayer:
model.togglePlayer()
case .openChannel:
guard let channel = model.currentVideo?.channel else { return }
navigation.openChannel(channel, navigationStyle: navigationStyle)
case .togglePlayerVisibility:
withAnimation(.spring(response: 0.25)) {
expansionState = expansionState == .full ? .mini : .full
}
default:
return
}
}
#endif
@ViewBuilder var contextMenu: some View {
if let video = model.currentVideo {
Group {
Section {
if accounts.app.supportsUserPlaylists && accounts.signedIn, !video.isLocal {
Section {
Button {
navigation.presentAddToPlaylist(video)
} label: {
Label("Add to Playlist...", systemImage: "text.badge.plus")
}
if let playlist = playlists.lastUsed, let video = model.currentVideo {
Button {
playlists.addVideo(playlistID: playlist.id, videoID: video.videoID)
} label: {
Label("Add to \(playlist.title)", systemImage: "text.badge.star")
}
}
}
}
#if !os(tvOS)
ShareButton(contentItem: .init(video: model.currentVideo))
#endif
Section {
if !video.isLocal {
Button {
navigation.openChannel(
video.channel,
navigationStyle: navigationStyle
)
} label: {
Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
}
if accounts.app.supportsSubscriptions, accounts.signedIn {
if subscriptions.isSubscribing(video.channel.id) {
Button {
#if os(tvOS)
subscriptions.unsubscribe(video.channel.id)
#else
navigation.presentUnsubscribeAlert(video.channel, subscriptions: subscriptions)
#endif
} label: {
Label("Unsubscribe", systemImage: "star.circle")
}
} else {
Button {
subscriptions.subscribe(video.channel.id) {
navigation.sidebarSectionChanged.toggle()
}
} label: {
Label("Subscribe", systemImage: "star.circle")
}
}
}
}
}
}
Button {
model.closeCurrentItem()
} label: {
Label("Close Video", systemImage: "xmark")
}
}
.labelStyle(.automatic)
} }
} }
} }
struct ControlsBar_Previews: PreviewProvider { struct ControlsBar_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ControlsBar(fullScreen: .constant(false)) ControlsBar(fullScreen: .constant(false), expansionState: .constant(.full))
.injectFixtureEnvironmentObjects() .injectFixtureEnvironmentObjects()
} }
} }

View File

@ -15,11 +15,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
#if os(iOS) #if os(iOS)
UIViewController.swizzleHomeIndicatorProperty() UIViewController.swizzleHomeIndicatorProperty()
UITabBar.appearance().shadowImage = UIImage()
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().isTranslucent = true
UITabBar.appearance().backgroundColor = .clear
OrientationTracker.shared.startDeviceOrientationTracking() OrientationTracker.shared.startDeviceOrientationTracking()
#endif #endif
return true return true