mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 12:41:57 +00:00 
			
		
		
		
	Player bar visibility modes and settings
This commit is contained in:
		| @@ -574,7 +574,9 @@ final class PlayerModel: ObservableObject { | ||||
|         closePiP() | ||||
|  | ||||
|         prepareCurrentItemForHistory(finished: finished) | ||||
|         currentItem = nil | ||||
|         withAnimation { | ||||
|             currentItem = nil | ||||
|         } | ||||
|         updateNowPlayingInfo() | ||||
|  | ||||
|         backend.closeItem() | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ struct ChannelCell: View { | ||||
|     } | ||||
|  | ||||
|     var navigationLink: some View { | ||||
|         NavigationLink(destination: ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) { | ||||
|         NavigationLink(destination: ChannelVideosView(channel: channel)) { | ||||
|             labelContent | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -41,6 +41,7 @@ struct ChannelLinkView<ChannelLabel: View>: View { | ||||
|     @ViewBuilder private var channelNavigationLink: some View { | ||||
|         NavigationLink(destination: ChannelVideosView(channel: channel)) { | ||||
|             channelLabel | ||||
|                 .lineLimit(1) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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()) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -52,6 +52,7 @@ struct AppSidebarNavigation: View { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .modifier(PlayerOverlayModifier()) | ||||
|         .environment(\.navigationStyle, .sidebar) | ||||
|     } | ||||
|  | ||||
| @@ -75,7 +76,7 @@ struct AppSidebarNavigation: View { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             ToolbarItemGroup(placement: accountsMenuToolbarItemPlacement) { | ||||
|             ToolbarItemGroup { | ||||
|                 AccountViewButton() | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|                 } | ||||
|   | ||||
| @@ -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!)) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -47,7 +47,7 @@ struct AppTabNavigation: View { | ||||
|                     searchNavigationView | ||||
|                 } | ||||
|             } | ||||
|             .overlay(ControlsBar(fullScreen: .constant(false)), alignment: .bottom) | ||||
|             .modifier(PlayerOverlayModifier()) | ||||
|         } | ||||
|         .onAppear { | ||||
|             feed.calculateUnwatchedFeed() | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|                                             } | ||||
|   | ||||
| @@ -39,6 +39,7 @@ struct VideoDetails: View { | ||||
|         VStack(alignment: .leading, spacing: 0) { | ||||
|             ControlsBar( | ||||
|                 fullScreen: $fullScreen, | ||||
|                 expansionState: .constant(.full), | ||||
|                 presentingControls: false, | ||||
|                 backgroundEnabled: false, | ||||
|                 borderTop: false, | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Arkadiusz Fal
					Arkadiusz Fal