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()
prepareCurrentItemForHistory(finished: finished)
withAnimation {
currentItem = nil
}
updateNowPlayingInfo()
backend.closeItem()

View File

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

View File

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

View File

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

View File

@ -32,6 +32,10 @@ extension Defaults.Keys {
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10)
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(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"
}
}
}

View File

@ -170,7 +170,6 @@ 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))
@ -186,8 +185,6 @@ struct FavoriteItemView: View {
EmptyView()
}
}
.modifier(PlayerOverlayModifier())
}
func itemButtonAction() {
switch item.section {

View File

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

View File

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

View File

@ -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)
}

View File

@ -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!))
}
}
}

View File

@ -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 {

View File

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

View File

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

View File

@ -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)
}

View File

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

View File

@ -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<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 {
Section(header: SettingsHeader(text: "Interface".localized())) {
#if !os(tvOS)

View File

@ -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:

View File

@ -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)

View File

@ -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,26 +19,49 @@ 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) {
@ -136,6 +154,7 @@ struct ControlsBar: View {
var details: some View {
HStack {
HStack(spacing: 8) {
if !playerBar {
Button {
if let video = model.currentVideo, !video.isLocal {
navigation.openChannel(
@ -150,7 +169,97 @@ struct ControlsBar: View {
)
.frame(width: barHeight - 10, height: barHeight - 10)
}
.contextMenu {
.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 }
}
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 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)
}
}
.zIndex(0)
.transition(.opacity)
if !playerBar {
Spacer()
}
}
}
.buttonStyle(.plain)
.padding(.vertical)
}
}
#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 {
@ -221,54 +330,11 @@ struct ControlsBar: View {
.labelStyle(.automatic)
}
}
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)
}
}
}
.buttonStyle(.plain)
.padding(.vertical)
Spacer()
}
}
}
struct ControlsBar_Previews: PreviewProvider {
static var previews: some View {
ControlsBar(fullScreen: .constant(false))
ControlsBar(fullScreen: .constant(false), expansionState: .constant(.full))
.injectFixtureEnvironmentObjects()
}
}

View File

@ -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