diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 71b6fcfc..6ee2db54 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -574,7 +574,9 @@ final class PlayerModel: ObservableObject { closePiP() prepareCurrentItemForHistory(finished: finished) - currentItem = nil + withAnimation { + currentItem = nil + } updateNowPlayingInfo() backend.closeItem() diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index 66861c09..c3186186 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -48,7 +48,9 @@ extension PlayerModel { comments.reset() stream = nil - currentItem = item + withAnimation { + currentItem = item + } if !time.isNil { currentItem.playbackTime = time @@ -204,7 +206,9 @@ extension PlayerModel { let item = PlayerQueueItem(video, playbackTime: atTime) if play { - currentItem = item + withAnimation { + currentItem = item + } videoBeingOpened = video } diff --git a/Shared/Channels/ChannelCell.swift b/Shared/Channels/ChannelCell.swift index 4d611973..3e42bc91 100644 --- a/Shared/Channels/ChannelCell.swift +++ b/Shared/Channels/ChannelCell.swift @@ -20,7 +20,7 @@ struct ChannelCell: View { } var navigationLink: some View { - NavigationLink(destination: ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) { + NavigationLink(destination: ChannelVideosView(channel: channel)) { labelContent } } diff --git a/Shared/Channels/ChannelLinkView.swift b/Shared/Channels/ChannelLinkView.swift index 30f92ae6..30a18400 100644 --- a/Shared/Channels/ChannelLinkView.swift +++ b/Shared/Channels/ChannelLinkView.swift @@ -41,6 +41,7 @@ struct ChannelLinkView: View { @ViewBuilder private var channelNavigationLink: some View { NavigationLink(destination: ChannelVideosView(channel: channel)) { channelLabel + .lineLimit(1) } } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 00b41cea..8b200342 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -32,6 +32,10 @@ extension Defaults.Keys { static let homeHistoryItems = Key("homeHistoryItems", default: 10) static let favorites = Key<[FavoriteItem]>("favorites", default: []) + static let playerButtonSingleTapGesture = Key("playerButtonSingleTapGesture", default: .togglePlayer) + static let playerButtonDoubleTapGesture = Key("playerButtonDoubleTapGesture", default: .togglePlayerVisibility) + static let playerButtonShowsControlButtonsWhenMinimized = Key("playerButtonShowsControlButtonsWhenMinimized", default: false) + #if !os(tvOS) #if os(macOS) static let accountPickerDisplaysUsernameDefault = true @@ -363,3 +367,23 @@ enum DetailsToolbarPositionSetting: String, CaseIterable, Defaults.Serializable 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" + } + } +} diff --git a/Shared/Home/FavoriteItemView.swift b/Shared/Home/FavoriteItemView.swift index 46974da5..6cf6ed91 100644 --- a/Shared/Home/FavoriteItemView.swift +++ b/Shared/Home/FavoriteItemView.swift @@ -170,23 +170,20 @@ struct FavoriteItemView: View { } @ViewBuilder var itemNavigationLinkDestination: some View { - Group { - switch item.section { - case let .channel(_, id, name): - ChannelVideosView(channel: .init(app: .invidious, id: id, name: name)) - case let .channelPlaylist(_, id, title): - ChannelPlaylistView(playlist: .init(id: id, title: title)) - case let .playlist(_, id): - ChannelPlaylistView(playlist: .init(id: id, title: label)) - case .subscriptions: - SubscriptionsView() - case .popular: - PopularView() - default: - EmptyView() - } + switch item.section { + case let .channel(_, id, name): + ChannelVideosView(channel: .init(app: .invidious, id: id, name: name)) + case let .channelPlaylist(_, id, title): + ChannelPlaylistView(playlist: .init(id: id, title: title)) + case let .playlist(_, id): + ChannelPlaylistView(playlist: .init(id: id, title: label)) + case .subscriptions: + SubscriptionsView() + case .popular: + PopularView() + default: + EmptyView() } - .modifier(PlayerOverlayModifier()) } func itemButtonAction() { diff --git a/Shared/Modifiers/PlayerOverlayModifier.swift b/Shared/Modifiers/PlayerOverlayModifier.swift index e541d126..f8ef95ba 100644 --- a/Shared/Modifiers/PlayerOverlayModifier.swift +++ b/Shared/Modifiers/PlayerOverlayModifier.swift @@ -1,11 +1,38 @@ +import Defaults import Foundation import SwiftUI 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 { content #if !os(tvOS) - .overlay(ControlsBar(fullScreen: .constant(false)), alignment: .bottom) + .overlay(overlay, alignment: .bottomTrailing) #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()) + } } diff --git a/Shared/Navigation/AppSidebarNavigation.swift b/Shared/Navigation/AppSidebarNavigation.swift index 72f07526..30af65c2 100644 --- a/Shared/Navigation/AppSidebarNavigation.swift +++ b/Shared/Navigation/AppSidebarNavigation.swift @@ -52,6 +52,7 @@ struct AppSidebarNavigation: View { } } } + .modifier(PlayerOverlayModifier()) .environment(\.navigationStyle, .sidebar) } @@ -75,7 +76,7 @@ struct AppSidebarNavigation: View { } } - ToolbarItemGroup(placement: accountsMenuToolbarItemPlacement) { + ToolbarItemGroup { AccountViewButton() } diff --git a/Shared/Navigation/AppSidebarPlaylists.swift b/Shared/Navigation/AppSidebarPlaylists.swift index 3298c5eb..ced46dd6 100644 --- a/Shared/Navigation/AppSidebarPlaylists.swift +++ b/Shared/Navigation/AppSidebarPlaylists.swift @@ -10,7 +10,7 @@ struct AppSidebarPlaylists: View { Section(header: Text("Playlists")) { ForEach(playlists.playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }) { playlist in NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $navigation.tabSelection) { - LazyView(PlaylistVideosView(playlist).modifier(PlayerOverlayModifier())) + LazyView(PlaylistVideosView(playlist)) } label: { playlistLabel(playlist) } diff --git a/Shared/Navigation/AppSidebarRecents.swift b/Shared/Navigation/AppSidebarRecents.swift index 777f2782..41ec437a 100644 --- a/Shared/Navigation/AppSidebarRecents.swift +++ b/Shared/Navigation/AppSidebarRecents.swift @@ -16,17 +16,17 @@ struct AppSidebarRecents: View { switch recent.type { case .channel: RecentNavigationLink(recent: recent) { - LazyView(ChannelVideosView(channel: recent.channel!).modifier(PlayerOverlayModifier())) + LazyView(ChannelVideosView(channel: recent.channel!)) } case .playlist: RecentNavigationLink(recent: recent, systemImage: "list.and.film") { - LazyView(ChannelPlaylistView(playlist: recent.playlist!).modifier(PlayerOverlayModifier())) + LazyView(ChannelPlaylistView(playlist: recent.playlist!)) } case .query: RecentNavigationLink(recent: recent, systemImage: "magnifyingglass") { - LazyView(SearchView(recent.query!).modifier(PlayerOverlayModifier())) + LazyView(SearchView(recent.query!)) } } } diff --git a/Shared/Navigation/AppSidebarSubscriptions.swift b/Shared/Navigation/AppSidebarSubscriptions.swift index 02a84a5f..48d3c245 100644 --- a/Shared/Navigation/AppSidebarSubscriptions.swift +++ b/Shared/Navigation/AppSidebarSubscriptions.swift @@ -12,7 +12,7 @@ struct AppSidebarSubscriptions: View { Section(header: Text("Subscriptions")) { ForEach(subscriptions.all) { channel in NavigationLink(tag: TabSelection.channel(channel.id), selection: $navigation.tabSelection) { - LazyView(ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) + LazyView(ChannelVideosView(channel: channel)) } label: { HStack { if channel.thumbnailURL != nil { diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index d375e325..d527b32b 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -47,7 +47,7 @@ struct AppTabNavigation: View { searchNavigationView } } - .overlay(ControlsBar(fullScreen: .constant(false)), alignment: .bottom) + .modifier(PlayerOverlayModifier()) } .onAppear { feed.calculateUnwatchedFeed() diff --git a/Shared/Navigation/Sidebar.swift b/Shared/Navigation/Sidebar.swift index c91f5a9d..99a54760 100644 --- a/Shared/Navigation/Sidebar.swift +++ b/Shared/Navigation/Sidebar.swift @@ -53,7 +53,7 @@ struct Sidebar: View { var mainNavigationLinks: some View { Section(header: Text("Videos")) { 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") .accessibility(label: Text("Home")) } @@ -62,7 +62,7 @@ struct Sidebar: View { #if os(iOS) 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") .accessibility(label: Text("Documents")) } @@ -74,7 +74,7 @@ struct Sidebar: View { if visibleSections.contains(.subscriptions), 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") .accessibility(label: Text("Subscriptions")) } @@ -88,7 +88,7 @@ struct Sidebar: View { } 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") .accessibility(label: Text("Popular")) } @@ -96,14 +96,14 @@ struct Sidebar: View { } 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") .accessibility(label: Text("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") .accessibility(label: Text("Search")) } @@ -159,3 +159,9 @@ struct Sidebar: View { scrollView.scrollTo(selection.stringValue) } } + +struct Sidebar_Previews: PreviewProvider { + static var previews: some View { + Sidebar() + } +} diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 6943d195..189c33fb 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -92,7 +92,7 @@ struct PlayerControls: View { model.presentingDetailsOverlay = true } } 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)) .frame(maxWidth: 300, alignment: .leading) } diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index bd518b67..05dc9e07 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -39,6 +39,7 @@ struct VideoDetails: View { VStack(alignment: .leading, spacing: 0) { ControlsBar( fullScreen: $fullScreen, + expansionState: .constant(.full), presentingControls: false, backgroundEnabled: false, borderTop: false, diff --git a/Shared/Settings/BrowsingSettings.swift b/Shared/Settings/BrowsingSettings.swift index a62f8d24..666d2490 100644 --- a/Shared/Settings/BrowsingSettings.swift +++ b/Shared/Settings/BrowsingSettings.swift @@ -21,6 +21,9 @@ struct BrowsingSettings: View { @Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem @Default(.homeHistoryItems) private var homeHistoryItems @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 @@ -65,6 +68,7 @@ struct BrowsingSettings: View { interface } #else + playerBarSettings interface #endif 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) -> 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 { Section(header: SettingsHeader(text: "Interface".localized())) { #if !os(tvOS) diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index 1e7b2c27..3c95552e 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -10,7 +10,7 @@ struct SettingsView: View { case browsing, player, quality, history, sponsorBlock, locations, advanced, help } - @State private var selection: Tabs? + @State private var selection: Tabs = .browsing #endif @Environment(\.colorScheme) private var colorScheme @@ -224,10 +224,8 @@ struct SettingsView: View { #if os(macOS) private var windowHeight: Double { switch selection { - case nil: - return accounts.isEmpty ? 680 : 580 case .browsing: - return 580 + return 680 case .player: return 900 case .quality: diff --git a/Shared/Subscriptions/ChannelsView.swift b/Shared/Subscriptions/ChannelsView.swift index 49b4ce3d..4e2f1311 100644 --- a/Shared/Subscriptions/ChannelsView.swift +++ b/Shared/Subscriptions/ChannelsView.swift @@ -14,7 +14,7 @@ struct ChannelsView: View { List { Section(header: header) { ForEach(subscriptions.all) { channel in - NavigationLink(destination: ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) { + NavigationLink(destination: ChannelVideosView(channel: channel)) { HStack { if let url = channel.thumbnailURLOrCached { ThumbnailView(url: url) diff --git a/Shared/Views/ControlsBar.swift b/Shared/Views/ControlsBar.swift index 63ad7937..64fe6516 100644 --- a/Shared/Views/ControlsBar.swift +++ b/Shared/Views/ControlsBar.swift @@ -3,20 +3,15 @@ import SDWebImageSwiftUI import SwiftUI 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 shareURL: URL? - - @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 + @Binding var expansionState: ExpansionState var presentingControls = true var backgroundEnabled = true @@ -24,34 +19,57 @@ struct ControlsBar: View { var borderBottom = true var detailsTogglePlayer = true var detailsToggleFullScreen = false + var playerBar = false 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 + @Default(.playerButtonShowsControlButtonsWhenMinimized) private var controlsWhenMinimized + @Default(.playerButtonSingleTapGesture) private var playerButtonSingleTapGesture + @Default(.playerButtonDoubleTapGesture) private var playerButtonDoubleTapGesture + var body: some View { HStack(spacing: 0) { detailsButton - if presentingControls { + if presentingControls, expansionState == .full || (controlsWhenMinimized && model.currentItem != nil) { + if expansionState == .full { + Spacer() + } controlsView .frame(maxWidth: 120) } } + .buttonStyle(.plain) .labelStyle(.iconOnly) - .padding(.horizontal) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight) - .borderTop(height: borderTop ? 0.5 : 0, color: Color("ControlsBorderColor")) - .borderBottom(height: borderBottom ? 0.5 : 0, color: Color("ControlsBorderColor")) - .modifier(ControlBackgroundModifier(enabled: backgroundEnabled, edgesIgnoringSafeArea: .bottom)) + .padding(.horizontal, 10) + .padding(.vertical, 2) + .frame(maxHeight: barHeight) + .padding(.trailing, expansionState == .mini && !controlsWhenMinimized ? 8 : 0) + .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) - .background( - EmptyView().sheet(isPresented: $presentingShareSheet) { - if let shareURL { - ShareSheet(activityItems: [shareURL]) - } + .background( + EmptyView().sheet(isPresented: $presentingShareSheet) { + if let shareURL { + ShareSheet(activityItems: [shareURL]) } - ) + } + ) #endif } @@ -136,139 +154,187 @@ struct ControlsBar: View { var details: some View { HStack { HStack(spacing: 8) { - Button { - if let video = model.currentVideo, !video.isLocal { - navigation.openChannel( - video.channel, - navigationStyle: navigationStyle + if !playerBar { + Button { + if let video = model.currentVideo, !video.isLocal { + navigation.openChannel( + 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( channel: model.currentVideo?.channel, video: model.currentVideo ) + #if !os(tvOS) + .highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil) + .gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil) + #endif .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 { - Button { - playlists.addVideo(playlistID: playlist.id, videoID: video.videoID) - } label: { - Label("Add to \(playlist.title)", systemImage: "text.badge.star") - } - } - } - } + if expansionState == .full { + VStack(alignment: .leading, spacing: 0) { + let notPlaying = "Not Playing".localized() + 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 !os(tvOS) - ShareButton(contentItem: .init(video: model.currentVideo)) - #endif + if let video = model.currentVideo, !video.localStreamIsFile { + HStack(spacing: 2) { + Text(video.displayAuthor) + .font(.system(size: 12)) - Section { - if !video.isLocal { - Button { - navigation.openChannel( - video.channel, - navigationStyle: navigationStyle - ) - } label: { - Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop") - } + if !presentingControls && !video.isLocal { + HStack(spacing: 2) { + Image(systemName: "person.2.fill") - 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") - } + if let channel = model.currentVideo?.channel { + if let subscriptions = channel.subscriptionsString { + Text(subscriptions) } else { - Button { - subscriptions.subscribe(video.channel.id) { - navigation.sidebarSectionChanged.toggle() - } - } label: { - Label("Subscribe", systemImage: "star.circle") - } + Text("1234").redacted(reason: .placeholder) } } } + .padding(.leading, 4) + .font(.system(size: 9)) } } - - Button { - model.closeCurrentItem() - } label: { - Label("Close Video", systemImage: "xmark") - } + .lineLimit(1) + .foregroundColor(.secondary) } - .labelStyle(.automatic) } - } + .zIndex(0) + .transition(.opacity) - VStack(alignment: .leading, spacing: 0) { - let notPlaying = "Not Playing".localized() - 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) + if !playerBar { + Spacer() } } } .buttonStyle(.plain) .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 { static var previews: some View { - ControlsBar(fullScreen: .constant(false)) + ControlsBar(fullScreen: .constant(false), expansionState: .constant(.full)) .injectFixtureEnvironmentObjects() } } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 93a2d07c..e61cafe5 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -15,11 +15,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { #if os(iOS) UIViewController.swizzleHomeIndicatorProperty() - UITabBar.appearance().shadowImage = UIImage() - UITabBar.appearance().backgroundImage = UIImage() - UITabBar.appearance().isTranslucent = true - UITabBar.appearance().backgroundColor = .clear - OrientationTracker.shared.startDeviceOrientationTracking() #endif return true