1
0
mirror of https://github.com/yattee/yattee.git synced 2024-12-13 22:00:31 +05:30
yattee/Shared/Views/ControlsBar.swift

343 lines
13 KiB
Swift
Raw Normal View History

import Defaults
import SDWebImageSwiftUI
import SwiftUI
struct ControlsBar: View {
enum ExpansionState {
case mini
case full
}
2022-06-30 13:35:32 +05:30
@Binding var fullScreen: Bool
2022-06-30 13:35:32 +05:30
@State private var presentingShareSheet = false
@State private var shareURL: URL?
@Binding var expansionState: ExpansionState
2022-09-02 18:41:20 +05:30
2022-12-21 04:11:21 +05:30
@State internal var gestureThrottle = Throttle(interval: 0.25)
2022-06-25 05:09:29 +05:30
var presentingControls = true
var backgroundEnabled = true
var detailsTogglePlayer = true
2022-06-25 22:03:35 +05:30
var detailsToggleFullScreen = false
var playerBar = false
2022-07-10 23:21:46 +05:30
var titleLineLimit = 2
2022-06-25 22:03:35 +05:30
@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
2022-09-02 04:35:31 +05:30
private let controlsOverlayModel = ControlOverlaysModel.shared
@Default(.playerButtonShowsControlButtonsWhenMinimized) private var controlsWhenMinimized
@Default(.playerButtonSingleTapGesture) private var playerButtonSingleTapGesture
@Default(.playerButtonDoubleTapGesture) private var playerButtonDoubleTapGesture
var body: some View {
2022-06-25 05:09:29 +05:30
HStack(spacing: 0) {
detailsButton
if presentingControls, expansionState == .full || (controlsWhenMinimized && model.currentItem != nil) {
if expansionState == .full {
Spacer()
}
2022-09-02 18:41:20 +05:30
controlsView
2022-06-25 05:09:29 +05:30
.frame(maxWidth: 120)
}
}
.buttonStyle(.plain)
.labelStyle(.iconOnly)
.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)
2022-12-18 17:41:06 +05:30
.stroke(Color("ControlsBorderColor"), lineWidth: 0.5)
)
2022-06-25 04:51:05 +05:30
#if os(iOS)
.background(
EmptyView().sheet(isPresented: $presentingShareSheet) {
if let shareURL {
ShareSheet(activityItems: [shareURL])
2022-06-25 04:51:05 +05:30
}
}
)
2022-06-25 04:51:05 +05:30
#endif
}
2022-06-25 05:09:29 +05:30
@ViewBuilder var detailsButton: some View {
if detailsTogglePlayer {
Button {
model.togglePlayer()
} label: {
details
.contentShape(Rectangle())
}
2022-06-25 22:03:35 +05:30
} else if detailsToggleFullScreen {
Button {
2022-11-18 03:17:45 +05:30
controlsOverlayModel.hide()
2022-09-02 18:41:20 +05:30
controls.presentingControls = false
2022-06-25 22:03:35 +05:30
withAnimation {
fullScreen.toggle()
}
} label: {
details
.contentShape(Rectangle())
}
2022-07-10 23:21:46 +05:30
#if !os(tvOS)
2022-06-25 22:03:35 +05:30
.keyboardShortcut("t")
2022-07-10 23:21:46 +05:30
#endif
2022-06-25 05:09:29 +05:30
} else {
details
}
}
2022-09-02 18:41:20 +05:30
var controlsView: some View {
HStack(spacing: 4) {
Group {
2022-09-02 18:41:20 +05:30
if controls.isPlaying {
Button(action: {
model.pause()
}) {
Label("Pause", systemImage: "pause.fill")
2022-06-25 05:09:29 +05:30
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
} else {
Button(action: {
model.play()
}) {
Label("Play", systemImage: "play.fill")
2022-06-25 05:09:29 +05:30
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
}
}
2022-09-02 18:41:20 +05:30
.disabled(controls.isLoadingVideo || model.currentItem.isNil)
Button(action: { model.advanceToNextItem() }) {
Label("Next", systemImage: "forward.fill")
2022-06-25 05:09:29 +05:30
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
2022-07-11 03:54:56 +05:30
.disabled(!model.isAdvanceToNextItemAvailable)
2022-07-02 04:22:27 +05:30
Button {
model.closeCurrentItem()
} label: {
Label("Close Video", systemImage: "xmark")
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
.disabled(model.currentItem.isNil)
}
2022-07-02 04:22:27 +05:30
.imageScale(.small)
.font(.system(size: 24))
}
var barHeight: Double {
2022-06-25 05:09:29 +05:30
55
}
var details: some View {
HStack {
HStack(spacing: 8) {
if !playerBar {
Button {
2022-12-18 17:41:06 +05:30
if let video = model.videoForDisplay, !video.isLocal {
navigation.openChannel(
video.channel,
navigationStyle: navigationStyle
)
}
} label: {
ChannelAvatarView(
2022-12-18 17:41:06 +05:30
channel: model.videoForDisplay?.channel,
video: model.videoForDisplay
2022-11-13 23:22:15 +05:30
)
.frame(width: barHeight - 10, height: barHeight - 10)
2022-11-13 23:22:15 +05:30
}
.contextMenu { contextMenu }
.zIndex(3)
} else {
ChannelAvatarView(
2022-12-18 17:41:06 +05:30
channel: model.videoForDisplay?.channel,
video: model.videoForDisplay
)
#if !os(tvOS)
.highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil)
.gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil)
#endif
.frame(width: barHeight - 10, height: barHeight - 10)
.contextMenu { contextMenu }
2022-06-25 04:51:05 +05:30
}
if expansionState == .full {
VStack(alignment: .leading, spacing: 0) {
let notPlaying = "Not Playing".localized()
2022-12-18 17:41:06 +05:30
Text(model.videoForDisplay?.displayTitle ?? notPlaying)
.font(.system(size: 14))
.fontWeight(.semibold)
2022-12-18 17:41:06 +05:30
.foregroundColor(model.videoForDisplay.isNil ? .secondary : .accentColor)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(titleLineLimit)
.multilineTextAlignment(.leading)
2022-06-25 04:51:05 +05:30
2022-12-18 17:41:06 +05:30
if let video = model.videoForDisplay, !video.localStreamIsFile {
HStack(spacing: 2) {
Text(video.displayAuthor)
.font(.system(size: 12))
2022-06-25 05:09:29 +05:30
if !presentingControls && !video.isLocal {
HStack(spacing: 2) {
Image(systemName: "person.2.fill")
2022-12-18 17:41:06 +05:30
if let channel = model.videoForDisplay?.channel {
if let subscriptions = channel.subscriptionsString {
Text(subscriptions)
2022-11-10 22:41:28 +05:30
} else {
Text("1234").redacted(reason: .placeholder)
2022-06-25 04:51:05 +05:30
}
}
}
.padding(.leading, 4)
.font(.system(size: 9))
}
}
.lineLimit(1)
.foregroundColor(.secondary)
}
}
.zIndex(0)
.transition(.opacity)
if !playerBar {
Spacer()
}
}
}
.buttonStyle(.plain)
.padding(.vertical)
}
}
2022-06-25 05:09:29 +05:30
#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) {
2022-12-21 04:11:21 +05:30
gestureThrottle.execute {
switch action {
case .togglePlayer:
self.model.togglePlayer()
case .openChannel:
guard let channel = self.model.videoForDisplay?.channel else { return }
self.navigation.openChannel(channel, navigationStyle: self.navigationStyle)
case .togglePlayerVisibility:
withAnimation(.spring(response: 0.25)) {
self.expansionState = self.expansionState == .full ? .mini : .full
}
default:
return
}
}
}
#endif
@ViewBuilder var contextMenu: some View {
2022-12-18 17:41:06 +05:30
if let video = model.videoForDisplay {
Group {
Section {
if accounts.app.supportsUserPlaylists && accounts.signedIn, !video.isLocal {
Section {
2022-06-25 05:09:29 +05:30
Button {
navigation.presentAddToPlaylist(video)
2022-06-25 05:09:29 +05:30
} label: {
Label("Add to Playlist...", systemImage: "text.badge.plus")
}
2022-12-18 17:41:06 +05:30
if let playlist = playlists.lastUsed, let video = model.videoForDisplay {
Button {
playlists.addVideo(playlistID: playlist.id, videoID: video.videoID)
} label: {
Label("Add to \(playlist.title)", systemImage: "text.badge.star")
}
2022-06-25 05:09:29 +05:30
}
}
}
#if !os(tvOS)
2022-12-18 17:41:06 +05:30
ShareButton(contentItem: .init(video: model.videoForDisplay))
#endif
Section {
if !video.isLocal {
Button {
navigation.openChannel(
video.channel,
navigationStyle: navigationStyle
)
} label: {
Label("\(video.author) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
}
2022-06-25 22:24:05 +05:30
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()
2022-07-11 23:14:25 +05:30
}
} label: {
Label("Subscribe", systemImage: "star.circle")
2022-07-11 23:14:25 +05:30
}
2022-07-02 04:22:27 +05:30
}
2022-06-25 22:24:05 +05:30
}
}
}
}
Button {
model.closeCurrentItem()
} label: {
Label("Close Video", systemImage: "xmark")
}
}
.labelStyle(.automatic)
}
}
}
struct ControlsBar_Previews: PreviewProvider {
static var previews: some View {
ControlsBar(fullScreen: .constant(false), expansionState: .constant(.full))
.injectFixtureEnvironmentObjects()
}
}