mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Minor UI changes
This commit is contained in:
parent
7b09805b81
commit
c940fb3198
@ -5,7 +5,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
|||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.environmentObject(AccountsModel())
|
.environmentObject(AccountsModel())
|
||||||
.environmentObject(CommentsModel())
|
.environmentObject(comments)
|
||||||
.environmentObject(InstancesModel())
|
.environmentObject(InstancesModel())
|
||||||
.environmentObject(invidious)
|
.environmentObject(invidious)
|
||||||
.environmentObject(NavigationModel())
|
.environmentObject(NavigationModel())
|
||||||
@ -21,6 +21,14 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
|||||||
.environmentObject(ThumbnailsModel())
|
.environmentObject(ThumbnailsModel())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var comments: CommentsModel {
|
||||||
|
let comments = CommentsModel()
|
||||||
|
comments.loaded = true
|
||||||
|
comments.all = [.fixture]
|
||||||
|
|
||||||
|
return comments
|
||||||
|
}
|
||||||
|
|
||||||
private var invidious: InvidiousAPI {
|
private var invidious: InvidiousAPI {
|
||||||
let api = InvidiousAPI()
|
let api = InvidiousAPI()
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ final class NetworkStateModel: ObservableObject {
|
|||||||
|
|
||||||
var needsUpdates: Bool {
|
var needsUpdates: Bool {
|
||||||
if let player = player {
|
if let player = player {
|
||||||
return pausedForCache || player.isSeeking || player.isLoadingVideo
|
return pausedForCache || player.isSeeking || player.isLoadingVideo || player.controls.presentingControlsOverlay
|
||||||
}
|
}
|
||||||
|
|
||||||
return pausedForCache
|
return pausedForCache
|
||||||
|
@ -589,5 +589,5 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
func stopControlsUpdates() {}
|
func stopControlsUpdates() {}
|
||||||
func setNeedsDrawing(_: Bool) {}
|
func setNeedsDrawing(_: Bool) {}
|
||||||
func setSize(_: Double, _: Double) {}
|
func setSize(_: Double, _: Double) {}
|
||||||
func setNeedsNetworkStateUpdates() {}
|
func setNeedsNetworkStateUpdates(_: Bool) {}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.controls?.isLoadingVideo = self.isLoadingVideo
|
self.controls?.isLoadingVideo = self.isLoadingVideo
|
||||||
self.setNeedsNetworkStateUpdates()
|
self.setNeedsNetworkStateUpdates(true)
|
||||||
|
|
||||||
if !self.isLoadingVideo {
|
if !self.isLoadingVideo {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
||||||
@ -476,7 +476,11 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setNeedsNetworkStateUpdates() {
|
func setNeedsNetworkStateUpdates(_ needsUpdates: Bool) {
|
||||||
networkStateTimer.resume()
|
if needsUpdates {
|
||||||
|
networkStateTimer.resume()
|
||||||
|
} else {
|
||||||
|
networkStateTimer.suspend()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ protocol PlayerBackend {
|
|||||||
func startControlsUpdates()
|
func startControlsUpdates()
|
||||||
func stopControlsUpdates()
|
func stopControlsUpdates()
|
||||||
|
|
||||||
func setNeedsNetworkStateUpdates()
|
func setNeedsNetworkStateUpdates(_ needsUpdates: Bool)
|
||||||
|
|
||||||
func setNeedsDrawing(_ needsDrawing: Bool)
|
func setNeedsDrawing(_ needsDrawing: Bool)
|
||||||
func setSize(_ width: Double, _ height: Double)
|
func setSize(_ width: Double, _ height: Double)
|
||||||
|
@ -6,7 +6,7 @@ final class PlayerControlsModel: ObservableObject {
|
|||||||
@Published var isLoadingVideo = false
|
@Published var isLoadingVideo = false
|
||||||
@Published var isPlaying = true
|
@Published var isPlaying = true
|
||||||
@Published var presentingControls = false { didSet { handlePresentationChange() } }
|
@Published var presentingControls = false { didSet { handlePresentationChange() } }
|
||||||
@Published var presentingControlsOverlay = false
|
@Published var presentingControlsOverlay = false { didSet { handleOverlayPresentationChange() } }
|
||||||
@Published var timer: Timer?
|
@Published var timer: Timer?
|
||||||
|
|
||||||
var player: PlayerModel!
|
var player: PlayerModel!
|
||||||
@ -40,6 +40,10 @@ final class PlayerControlsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleOverlayPresentationChange() {
|
||||||
|
player?.backend.setNeedsNetworkStateUpdates(presentingControlsOverlay)
|
||||||
|
}
|
||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
guard !(player?.currentItem.isNil ?? true) else {
|
guard !(player?.currentItem.isNil ?? true) else {
|
||||||
return
|
return
|
||||||
|
@ -67,7 +67,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
||||||
|
|
||||||
@Published var isSeeking = false { didSet {
|
@Published var isSeeking = false { didSet {
|
||||||
backend.setNeedsNetworkStateUpdates()
|
backend.setNeedsNetworkStateUpdates(true)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
@ -6,8 +6,8 @@ struct ChaptersView: View {
|
|||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
if let chapters = player.currentVideo?.chapters, !chapters.isEmpty {
|
||||||
if let chapters = player.currentVideo?.chapters, !chapters.isEmpty {
|
List {
|
||||||
Section(header: Text("Chapters")) {
|
Section(header: Text("Chapters")) {
|
||||||
ForEach(chapters) { chapter in
|
ForEach(chapters) { chapter in
|
||||||
Button {
|
Button {
|
||||||
@ -18,18 +18,17 @@ struct ChaptersView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Text(player.currentVideo?.title ?? "")
|
|
||||||
}
|
}
|
||||||
}
|
#if os(macOS)
|
||||||
.id(UUID())
|
|
||||||
#if os(macOS)
|
|
||||||
.listStyle(.inset)
|
.listStyle(.inset)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
.listStyle(.grouped)
|
.listStyle(.grouped)
|
||||||
#else
|
#else
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
#endif
|
#endif
|
||||||
|
} else {
|
||||||
|
NoCommentsView(text: "No chapters information available", systemImage: "xmark.circle.fill")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func chapterButtonLabel(_ chapter: Chapter) -> some View {
|
@ViewBuilder func chapterButtonLabel(_ chapter: Chapter) -> some View {
|
||||||
|
@ -99,6 +99,7 @@ struct CommentView: View {
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
#endif
|
#endif
|
||||||
|
.padding(.bottom, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var authorAvatar: some View {
|
private var authorAvatar: some View {
|
||||||
|
@ -22,12 +22,7 @@ struct CommentsView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
comments.loadNextPageIfNeeded(current: comment)
|
comments.loadNextPageIfNeeded(current: comment)
|
||||||
}
|
}
|
||||||
.padding(.bottom, comment == last ? 5 : 0)
|
.borderBottom(height: comment != last ? 0.5 : 0, color: Color("ControlsBorderColor"))
|
||||||
|
|
||||||
if comment != last {
|
|
||||||
Divider()
|
|
||||||
.padding(.vertical, 5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ControlsOverlay: View {
|
struct ControlsOverlay: View {
|
||||||
|
@EnvironmentObject<NetworkStateModel> private var networkState
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<PlayerControlsModel> private var model
|
@EnvironmentObject<PlayerControlsModel> private var model
|
||||||
|
|
||||||
@ -165,7 +166,7 @@ struct ControlsOverlay: View {
|
|||||||
Text("hw decoder: \(player.mpvBackend.hwDecoder)")
|
Text("hw decoder: \(player.mpvBackend.hwDecoder)")
|
||||||
Text("dropped: \(player.mpvBackend.frameDropCount)")
|
Text("dropped: \(player.mpvBackend.frameDropCount)")
|
||||||
Text("video: \(String(format: "%.2ffps", player.mpvBackend.outputFps))")
|
Text("video: \(String(format: "%.2ffps", player.mpvBackend.outputFps))")
|
||||||
Text("buffering: \(String(format: "%.0f%%", player.mpvBackend.bufferingState))")
|
Text("buffering: \(String(format: "%.0f%%", networkState.bufferingState))")
|
||||||
Text("cache: \(String(format: "%.2fs", player.mpvBackend.cacheDuration))")
|
Text("cache: \(String(format: "%.2fs", player.mpvBackend.cacheDuration))")
|
||||||
}
|
}
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
|
@ -6,7 +6,11 @@ struct NetworkState: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Buffering(state: model.fullStateText)
|
Buffering(state: model.fullStateText)
|
||||||
.opacity(model.pausedForCache || player.isSeeking ? 1 : 0)
|
.opacity(visible ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var visible: Bool {
|
||||||
|
player.isPlaying && (model.pausedForCache || player.isSeeking)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,8 +23,6 @@ struct PlayerControls: View {
|
|||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@Default(.controlsBarInPlayer) private var controlsBarInPlayer
|
|
||||||
|
|
||||||
init(player: PlayerModel, thumbnails: ThumbnailsModel) {
|
init(player: PlayerModel, thumbnails: ThumbnailsModel) {
|
||||||
self.player = player
|
self.player = player
|
||||||
self.thumbnails = thumbnails
|
self.thumbnails = thumbnails
|
||||||
@ -191,12 +189,13 @@ struct PlayerControls: View {
|
|||||||
HStack(spacing: 20) {
|
HStack(spacing: 20) {
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
fullscreenButton
|
fullscreenButton
|
||||||
pipButton
|
|
||||||
|
#if os(iOS)
|
||||||
|
pipButton
|
||||||
|
#endif
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
button("overlay", systemImage: "info.circle") {}
|
|
||||||
|
|
||||||
button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) {
|
button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) {
|
||||||
withAnimation(Self.animation) {
|
withAnimation(Self.animation) {
|
||||||
model.presentingControlsOverlay.toggle()
|
model.presentingControlsOverlay.toggle()
|
||||||
|
@ -114,12 +114,13 @@ struct TimelineView: View {
|
|||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.gray.opacity(0.1))
|
.fill(Color.white.opacity(0.2))
|
||||||
.frame(maxHeight: height)
|
.frame(maxHeight: height)
|
||||||
|
.offset(x: current * oneUnitWidth)
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
|
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.gray.opacity(0.5))
|
.fill(Color.white.opacity(0.6))
|
||||||
.frame(maxHeight: height)
|
.frame(maxHeight: height)
|
||||||
.frame(width: current * oneUnitWidth)
|
.frame(width: current * oneUnitWidth)
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
@ -187,7 +188,7 @@ struct TimelineView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
.background(GeometryReader { proxy in
|
.overlay(GeometryReader { proxy in
|
||||||
Color.clear
|
Color.clear
|
||||||
.onAppear {
|
.onAppear {
|
||||||
self.size = proxy.size
|
self.size = proxy.size
|
||||||
@ -265,7 +266,6 @@ struct TimelineView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var segments: [Segment] {
|
var segments: [Segment] {
|
||||||
// [.init(category: "outro", segment: [25,30], uuid: UUID().uuidString, videoDuration: 100)] ??
|
|
||||||
player.sponsorBlock.segments
|
player.sponsorBlock.segments
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ struct TimelineView: View {
|
|||||||
var chaptersLayers: some View {
|
var chaptersLayers: some View {
|
||||||
ForEach(chapters) { chapter in
|
ForEach(chapters) { chapter in
|
||||||
RoundedRectangle(cornerRadius: 4)
|
RoundedRectangle(cornerRadius: 4)
|
||||||
.fill(Color("AppBlueColor"))
|
.fill(Color.orange)
|
||||||
.frame(maxWidth: 2, maxHeight: 12)
|
.frame(maxWidth: 2, maxHeight: 12)
|
||||||
.offset(x: (chapter.start * oneUnitWidth) - 1)
|
.offset(x: (chapter.start * oneUnitWidth) - 1)
|
||||||
}
|
}
|
||||||
|
@ -53,10 +53,81 @@ struct VideoDetails: View {
|
|||||||
player.currentVideo
|
player.currentVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
ControlsBar(
|
||||||
|
presentingControls: false,
|
||||||
|
backgroundEnabled: false,
|
||||||
|
borderTop: false,
|
||||||
|
detailsTogglePlayer: false
|
||||||
|
)
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
pageButton("Info", "info.circle", .info, !video.isNil)
|
||||||
|
pageButton("Chapters", "bookmark", .chapters, !(video?.chapters.isEmpty ?? true))
|
||||||
|
pageButton("Comments", "text.bubble", .comments, !video.isNil) { comments.load() }
|
||||||
|
pageButton("Related", "rectangle.stack.fill", .related, !video.isNil)
|
||||||
|
pageButton("Queue", "list.number", .queue, !video.isNil)
|
||||||
|
}
|
||||||
|
.onChange(of: player.currentItem) { _ in
|
||||||
|
page.update(.moveToFirst)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
|
||||||
|
Pager(page: page, data: DetailsPage.allCases, id: \.self) {
|
||||||
|
detailsByPage($0)
|
||||||
|
}
|
||||||
|
.onPageWillChange { pageIndex in
|
||||||
|
if pageIndex == DetailsPage.comments.index {
|
||||||
|
comments.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if video.isNil && !sidebarQueue {
|
||||||
|
page.update(.new(index: DetailsPage.queue.index))
|
||||||
|
}
|
||||||
|
|
||||||
|
guard video != nil, accounts.app.supportsSubscriptions else {
|
||||||
|
subscribed = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: sidebarQueue) { queue in
|
||||||
|
if queue {
|
||||||
|
if currentPage == .related || currentPage == .queue {
|
||||||
|
page.update(.moveToFirst)
|
||||||
|
}
|
||||||
|
} else if video.isNil {
|
||||||
|
page.update(.moveToLast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishedDateSection: some View {
|
||||||
|
Group {
|
||||||
|
if let video = player.currentVideo {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
if let published = video.publishedDate {
|
||||||
|
Text(published)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contentItem: ContentItem {
|
||||||
|
ContentItem(video: player.currentVideo!)
|
||||||
|
}
|
||||||
|
|
||||||
func pageButton(
|
func pageButton(
|
||||||
_ label: String,
|
_ label: String,
|
||||||
_ symbolName: String,
|
_ symbolName: String,
|
||||||
_ destination: DetailsPage,
|
_ destination: DetailsPage,
|
||||||
|
_ active: Bool = true,
|
||||||
pageChangeAction: (() -> Void)? = nil
|
pageChangeAction: (() -> Void)? = nil
|
||||||
) -> some View {
|
) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@ -69,25 +140,25 @@ struct VideoDetails: View {
|
|||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: symbolName)
|
Image(systemName: symbolName)
|
||||||
|
|
||||||
if playerDetailsPageButtonLabelStyle.text {
|
if playerDetailsPageButtonLabelStyle.text && player.playerSize.width > 450 {
|
||||||
Text(label)
|
Text(label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: 15)
|
.frame(minHeight: 15)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.foregroundColor(currentPage == destination ? .white : .accentColor)
|
.foregroundColor(currentPage == destination ? .white : (active ? Color.accentColor : .gray))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.background(currentPage == destination ? Color.accentColor : .clear)
|
.background(currentPage == destination ? (active ? Color.accentColor : .gray) : .clear)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.font(.system(size: 10).bold())
|
.font(.system(size: 10).bold())
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 2)
|
RoundedRectangle(cornerRadius: 2)
|
||||||
.stroke(Color.accentColor, lineWidth: 2)
|
.stroke(active ? Color.accentColor : .gray, lineWidth: 2)
|
||||||
.foregroundColor(.clear)
|
.foregroundColor(.clear)
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@ -119,123 +190,6 @@ struct VideoDetails: View {
|
|||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Group {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
pageButton("Info", "info.circle", .info)
|
|
||||||
pageButton("Chapters", "bookmark", .chapters)
|
|
||||||
pageButton("Comments", "text.bubble", .comments) { comments.load() }
|
|
||||||
pageButton("Related", "rectangle.stack.fill", .related)
|
|
||||||
pageButton("Queue", "list.number", .queue)
|
|
||||||
}
|
|
||||||
.onChange(of: player.currentItem) { _ in
|
|
||||||
page.update(.moveToFirst)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
|
|
||||||
Pager(page: page, data: DetailsPage.allCases, id: \.self) {
|
|
||||||
detailsByPage($0)
|
|
||||||
}
|
|
||||||
.onPageWillChange { pageIndex in
|
|
||||||
if pageIndex == DetailsPage.comments.index {
|
|
||||||
comments.load()
|
|
||||||
} else {
|
|
||||||
print("comments not loading")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if video.isNil && !sidebarQueue {
|
|
||||||
page.update(.new(index: DetailsPage.queue.index))
|
|
||||||
}
|
|
||||||
|
|
||||||
guard video != nil, accounts.app.supportsSubscriptions else {
|
|
||||||
subscribed = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: sidebarQueue) { queue in
|
|
||||||
if queue {
|
|
||||||
if currentPage == .related || currentPage == .queue {
|
|
||||||
page.update(.moveToFirst)
|
|
||||||
}
|
|
||||||
} else if video.isNil {
|
|
||||||
page.update(.moveToLast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showAddToPlaylistButton: Bool {
|
|
||||||
accounts.app.supportsUserPlaylists && accounts.signedIn
|
|
||||||
}
|
|
||||||
|
|
||||||
var publishedDateSection: some View {
|
|
||||||
Group {
|
|
||||||
if let video = player.currentVideo {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
if let published = video.publishedDate {
|
|
||||||
Text(published)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var contentItem: ContentItem {
|
|
||||||
ContentItem(video: player.currentVideo!)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var authorAvatar: some View {
|
|
||||||
Group {
|
|
||||||
if let video = video, let url = video.channel.thumbnailURL {
|
|
||||||
WebImage(url: url)
|
|
||||||
.resizable()
|
|
||||||
.placeholder {
|
|
||||||
Rectangle().fill(Color("PlaceholderColor"))
|
|
||||||
}
|
|
||||||
.retryOnAppear(true)
|
|
||||||
.indicator(.activity)
|
|
||||||
.clipShape(Circle())
|
|
||||||
.frame(width: 35, height: 35, alignment: .leading)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoProperties: some View {
|
|
||||||
HStack(spacing: 2) {
|
|
||||||
publishedDateSection
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
if let views = video?.viewsCount {
|
|
||||||
Image(systemName: "eye")
|
|
||||||
|
|
||||||
Text(views)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let likes = video?.likesCount {
|
|
||||||
Image(systemName: "hand.thumbsup")
|
|
||||||
|
|
||||||
Text(likes)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let likes = video?.dislikesCount {
|
|
||||||
Image(systemName: "hand.thumbsdown")
|
|
||||||
|
|
||||||
Text(likes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
var detailsPage: some View {
|
var detailsPage: some View {
|
||||||
Group {
|
Group {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
@ -316,6 +270,35 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var videoProperties: some View {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
publishedDateSection
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
if let views = video?.viewsCount {
|
||||||
|
Image(systemName: "eye")
|
||||||
|
|
||||||
|
Text(views)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let likes = video?.likesCount {
|
||||||
|
Image(systemName: "hand.thumbsup")
|
||||||
|
|
||||||
|
Text(likes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let likes = video?.dislikesCount {
|
||||||
|
Image(systemName: "hand.thumbsdown")
|
||||||
|
|
||||||
|
Text(likes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
func videoDetail(label: String, value: String, symbol: String) -> some View {
|
func videoDetail(label: String, value: String, symbol: String) -> some View {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
|
@ -2,13 +2,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoDetailsPaddingModifier: ViewModifier {
|
struct VideoDetailsPaddingModifier: ViewModifier {
|
||||||
static var defaultAdditionalDetailsPadding: Double {
|
static var defaultAdditionalDetailsPadding = 0.0
|
||||||
#if os(macOS)
|
|
||||||
5
|
|
||||||
#else
|
|
||||||
10
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
let geometry: GeometryProxy
|
let geometry: GeometryProxy
|
||||||
let aspectRatio: Double?
|
let aspectRatio: Double?
|
||||||
|
@ -61,10 +61,12 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// TODO: remove
|
#if DEBUG
|
||||||
if #available(iOS 15.0, macOS 12.0, *) {
|
// TODO: remove
|
||||||
_ = Self._printChanges()
|
if #available(iOS 15.0, macOS 12.0, *) {
|
||||||
}
|
_ = Self._printChanges()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
return HSplitView {
|
return HSplitView {
|
||||||
@ -159,7 +161,7 @@ struct VideoPlayerView: View {
|
|||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if player.playingInPictureInPicture {
|
if player.playingInPictureInPicture {
|
||||||
pictureInPicturePlaceholder(geometry: geometry)
|
pictureInPicturePlaceholder
|
||||||
} else {
|
} else {
|
||||||
playerView
|
playerView
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@ -170,7 +172,7 @@ struct VideoPlayerView: View {
|
|||||||
fullScreen: player.playingFullScreen
|
fullScreen: player.playingFullScreen
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// .overlay(playerPlaceholder(geometry: geometry))
|
.overlay(playerPlaceholder)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,15 +185,11 @@ struct VideoPlayerView: View {
|
|||||||
.gesture(
|
.gesture(
|
||||||
DragGesture(coordinateSpace: .global)
|
DragGesture(coordinateSpace: .global)
|
||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
guard player.presentingPlayer else {
|
guard player.presentingPlayer else { return }
|
||||||
return // swiftlint:disable:this implicit_return
|
|
||||||
}
|
|
||||||
|
|
||||||
let drag = value.translation.height
|
let drag = value.translation.height
|
||||||
|
|
||||||
guard drag > 0 else {
|
guard drag > 0 else { return }
|
||||||
return // swiftlint:disable:this implicit_return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard drag < 100 else {
|
guard drag < 100 else {
|
||||||
player.hide()
|
player.hide()
|
||||||
@ -231,6 +229,7 @@ struct VideoPlayerView: View {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if verticalSizeClass == .regular {
|
if verticalSizeClass == .regular {
|
||||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails)
|
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails)
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
@ -248,12 +247,6 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !os(tvOS)
|
|
||||||
if !fullScreenLayout {
|
|
||||||
ControlsBar()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@ -273,7 +266,6 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition(.asymmetric(insertion: .slide, removal: .identity))
|
|
||||||
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.statusBar(hidden: player.playingFullScreen)
|
.statusBar(hidden: player.playingFullScreen)
|
||||||
@ -326,7 +318,7 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func playerPlaceholder(geometry: GeometryProxy) -> some View {
|
@ViewBuilder var playerPlaceholder: some View {
|
||||||
if player.currentItem.isNil {
|
if player.currentItem.isNil {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
HStack {
|
HStack {
|
||||||
@ -359,11 +351,11 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio)
|
.frame(width: player.playerSize.width, height: player.playerSize.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pictureInPicturePlaceholder(geometry: GeometryProxy) -> some View {
|
var pictureInPicturePlaceholder: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack {
|
VStack {
|
||||||
@ -389,7 +381,7 @@ struct VideoPlayerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio)
|
.frame(width: player.playerSize.width, height: player.playerSize.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
@ -8,6 +8,7 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let content: Content
|
let content: Content
|
||||||
|
let toolbar: Toolbar?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context _: Context? = nil,
|
context _: Context? = nil,
|
||||||
@ -15,7 +16,7 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
|||||||
@ViewBuilder content: @escaping () -> Content
|
@ViewBuilder content: @escaping () -> Content
|
||||||
) {
|
) {
|
||||||
self.content = content()
|
self.content = content()
|
||||||
toolbar()
|
self.toolbar = toolbar()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@ -26,195 +27,29 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 15.0, macOS 12.0, *) {
|
// TODO: remove
|
||||||
_ = Self._printChanges()
|
#if DEBUG
|
||||||
}
|
if #available(iOS 15.0, macOS 12.0, *) {
|
||||||
|
Self._printChanges()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return VStack(spacing: 0) {
|
return ZStack(alignment: .bottomLeading) {
|
||||||
content
|
content
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
ControlsBar()
|
VStack(spacing: 0) {
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
toolbar
|
||||||
|
.borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
||||||
|
.modifier(ControlBackgroundModifier())
|
||||||
|
ControlsBar()
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
|
||||||
// enum Context {
|
|
||||||
// case browser, player
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let context: Context
|
|
||||||
// let content: Content
|
|
||||||
// let toolbar: Toolbar?
|
|
||||||
//
|
|
||||||
// @Environment(\.navigationStyle) private var navigationStyle
|
|
||||||
// @EnvironmentObject<PlayerControlsModel> private var playerControls
|
|
||||||
// @EnvironmentObject<PlayerModel> private var model
|
|
||||||
//
|
|
||||||
// var barHeight: Double {
|
|
||||||
// 75
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// init(
|
|
||||||
// context: Context? = nil,
|
|
||||||
// @ViewBuilder toolbar: @escaping () -> Toolbar? = { nil },
|
|
||||||
// @ViewBuilder content: @escaping () -> Content
|
|
||||||
// ) {
|
|
||||||
// self.context = context ?? .browser
|
|
||||||
// self.content = content()
|
|
||||||
// self.toolbar = toolbar()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// init(
|
|
||||||
// context: Context? = nil,
|
|
||||||
// @ViewBuilder content: @escaping () -> Content
|
|
||||||
// ) where Toolbar == EmptyView {
|
|
||||||
// self.init(context: context, toolbar: { EmptyView() }, content: content)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var body: some View {
|
|
||||||
// ZStack(alignment: .bottomLeading) {
|
|
||||||
// VStack(spacing: 0) {
|
|
||||||
// content
|
|
||||||
//
|
|
||||||
// Color.clear.frame(height: barHeight)
|
|
||||||
// }
|
|
||||||
// #if !os(tvOS)
|
|
||||||
// .frame(minHeight: 0, maxHeight: .infinity)
|
|
||||||
// #endif
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// VStack {
|
|
||||||
// #if !os(tvOS)
|
|
||||||
// #if !os(macOS)
|
|
||||||
// toolbar
|
|
||||||
// .frame(height: 100)
|
|
||||||
// .offset(x: 0, y: -28)
|
|
||||||
// #endif
|
|
||||||
//
|
|
||||||
// if context != .player || !playerControls.playingFullscreen {
|
|
||||||
// controls
|
|
||||||
// }
|
|
||||||
// #endif
|
|
||||||
// }
|
|
||||||
// .borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
|
||||||
// #if os(macOS)
|
|
||||||
// .background(VisualEffectBlur(material: .sidebar))
|
|
||||||
// #elseif os(iOS)
|
|
||||||
// .background(VisualEffectBlur(blurStyle: .systemThinMaterial).edgesIgnoringSafeArea(.all))
|
|
||||||
// #endif
|
|
||||||
// }
|
|
||||||
// .background(Color.debug)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private var controls: some View {
|
|
||||||
// VStack(spacing: 0) {
|
|
||||||
// TimelineView(duration: playerControls.durationBinding, current: playerControls.currentTimeBinding)
|
|
||||||
// .foregroundColor(.secondary)
|
|
||||||
//
|
|
||||||
// Button(action: {
|
|
||||||
// model.togglePlayer()
|
|
||||||
// }) {
|
|
||||||
// HStack(spacing: 8) {
|
|
||||||
// authorAvatar
|
|
||||||
//
|
|
||||||
// VStack(alignment: .leading, spacing: 5) {
|
|
||||||
// Text(model.currentVideo?.title ?? "Not playing")
|
|
||||||
// .font(.headline)
|
|
||||||
// .foregroundColor(model.currentVideo.isNil ? .secondary : .accentColor)
|
|
||||||
// .lineLimit(1)
|
|
||||||
//
|
|
||||||
// Text(model.currentVideo?.author ?? "")
|
|
||||||
// .font(.subheadline)
|
|
||||||
// .foregroundColor(.secondary)
|
|
||||||
// .lineLimit(1)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Spacer()
|
|
||||||
//
|
|
||||||
// HStack {
|
|
||||||
// Group {
|
|
||||||
// if !model.currentItem.isNil {
|
|
||||||
// Button {
|
|
||||||
// model.closeCurrentItem()
|
|
||||||
// model.closePiP()
|
|
||||||
// } label: {
|
|
||||||
// Label("Close Video", systemImage: "xmark")
|
|
||||||
// .padding(.horizontal, 4)
|
|
||||||
// .contentShape(Rectangle())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if playerControls.isPlaying {
|
|
||||||
// Button(action: {
|
|
||||||
// model.pause()
|
|
||||||
// }) {
|
|
||||||
// Label("Pause", systemImage: "pause.fill")
|
|
||||||
// .padding(.horizontal, 4)
|
|
||||||
// .contentShape(Rectangle())
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// Button(action: {
|
|
||||||
// model.play()
|
|
||||||
// }) {
|
|
||||||
// Label("Play", systemImage: "play.fill")
|
|
||||||
// .padding(.horizontal, 4)
|
|
||||||
// .contentShape(Rectangle())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
|
|
||||||
// .font(.system(size: 30))
|
|
||||||
// .frame(minWidth: 30)
|
|
||||||
//
|
|
||||||
// Button(action: { model.advanceToNextItem() }) {
|
|
||||||
// Label("Next", systemImage: "forward.fill")
|
|
||||||
// .padding(.vertical)
|
|
||||||
// .contentShape(Rectangle())
|
|
||||||
// }
|
|
||||||
// .disabled(model.queue.isEmpty)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .buttonStyle(.plain)
|
|
||||||
// .contentShape(Rectangle())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .buttonStyle(.plain)
|
|
||||||
// .labelStyle(.iconOnly)
|
|
||||||
// .padding(.horizontal)
|
|
||||||
// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight)
|
|
||||||
// .borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
|
||||||
// .borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor"))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private var authorAvatar: some View {
|
|
||||||
// Group {
|
|
||||||
// if let video = model.currentItem?.video, let url = video.channel.thumbnailURL {
|
|
||||||
// WebImage(url: url)
|
|
||||||
// .resizable()
|
|
||||||
// .placeholder {
|
|
||||||
// Rectangle().fill(Color("PlaceholderColor"))
|
|
||||||
// }
|
|
||||||
// .retryOnAppear(true)
|
|
||||||
// .indicator(.activity)
|
|
||||||
// .clipShape(Circle())
|
|
||||||
// .frame(width: 44, height: 44, alignment: .leading)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private var progressViewValue: Double {
|
|
||||||
// [model.time?.seconds, model.videoDuration].compactMap { $0 }.min() ?? 0
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private var progressViewTotal: Double {
|
|
||||||
// model.videoDuration ?? 100
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
struct PlayerControlsView_Previews: PreviewProvider {
|
struct PlayerControlsView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
BrowserPlayerControls(context: .player) {
|
BrowserPlayerControls(context: .player) {
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import Defaults
|
import Defaults
|
||||||
import SDWebImageSwiftUI
|
import SDWebImageSwiftUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUIPager
|
|
||||||
|
|
||||||
struct ControlsBar: View {
|
struct ControlsBar: View {
|
||||||
enum Pages: CaseIterable {
|
|
||||||
case details, controls
|
|
||||||
}
|
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<NavigationModel> private var navigation
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||||
@ -19,27 +14,28 @@ struct ControlsBar: View {
|
|||||||
@State private var presentingShareSheet = false
|
@State private var presentingShareSheet = false
|
||||||
@State private var shareURL: URL?
|
@State private var shareURL: URL?
|
||||||
|
|
||||||
@StateObject private var controlsPage = Page.first()
|
var presentingControls = true
|
||||||
|
var backgroundEnabled = true
|
||||||
|
var borderTop = true
|
||||||
|
var borderBottom = true
|
||||||
|
var detailsTogglePlayer = true
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Pager(page: controlsPage, data: Pages.allCases, id: \.self) { index in
|
detailsButton
|
||||||
switch index {
|
|
||||||
case .details:
|
if presentingControls {
|
||||||
details
|
controls
|
||||||
default:
|
.frame(maxWidth: 120)
|
||||||
controls
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.pagingPriority(.simultaneous)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: barHeight)
|
||||||
.borderTop(height: 0.4, color: Color("ControlsBorderColor"))
|
.borderTop(height: borderTop ? 0.4 : 0, color: Color("ControlsBorderColor"))
|
||||||
.borderBottom(height: 0.4, color: Color("ControlsBorderColor"))
|
.borderBottom(height: borderBottom ? 0.4 : 0, color: Color("ControlsBorderColor"))
|
||||||
.modifier(ControlBackgroundModifier(edgesIgnoringSafeArea: .bottom))
|
.modifier(ControlBackgroundModifier(enabled: backgroundEnabled, edgesIgnoringSafeArea: .bottom))
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.background(
|
.background(
|
||||||
EmptyView().sheet(isPresented: $presentingShareSheet) {
|
EmptyView().sheet(isPresented: $presentingShareSheet) {
|
||||||
@ -51,6 +47,19 @@ struct ControlsBar: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var detailsButton: some View {
|
||||||
|
if detailsTogglePlayer {
|
||||||
|
Button {
|
||||||
|
model.togglePlayer()
|
||||||
|
} label: {
|
||||||
|
details
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
details
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var controls: some View {
|
var controls: some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Group {
|
Group {
|
||||||
@ -59,32 +68,18 @@ struct ControlsBar: View {
|
|||||||
model.closePiP()
|
model.closePiP()
|
||||||
} label: {
|
} label: {
|
||||||
Label("Close Video", systemImage: "xmark")
|
Label("Close Video", systemImage: "xmark")
|
||||||
.padding(.horizontal, 4)
|
.padding(.vertical, 10)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: { model.backend.seek(to: 0) }) {
|
|
||||||
Label("Restart", systemImage: "backward.end.fill")
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button {
|
|
||||||
model.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
|
||||||
} label: {
|
|
||||||
Label("Backward", systemImage: "gobackward.10")
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if playerControls.isPlaying {
|
if playerControls.isPlaying {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
model.pause()
|
model.pause()
|
||||||
}) {
|
}) {
|
||||||
Label("Pause", systemImage: "pause.fill")
|
Label("Pause", systemImage: "pause.fill")
|
||||||
.padding(.horizontal, 4)
|
.padding(.vertical, 10)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -92,38 +87,27 @@ struct ControlsBar: View {
|
|||||||
model.play()
|
model.play()
|
||||||
}) {
|
}) {
|
||||||
Label("Play", systemImage: "play.fill")
|
Label("Play", systemImage: "play.fill")
|
||||||
.padding(.horizontal, 4)
|
.padding(.vertical, 10)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button {
|
|
||||||
model.backend.seek(relative: .secondsInDefaultTimescale(10))
|
|
||||||
} label: {
|
|
||||||
Label("Forward", systemImage: "goforward.10")
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
|
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
|
||||||
|
|
||||||
Button(action: { model.advanceToNextItem() }) {
|
Button(action: { model.advanceToNextItem() }) {
|
||||||
Label("Next", systemImage: "forward.fill")
|
Label("Next", systemImage: "forward.fill")
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.disabled(model.queue.isEmpty)
|
.disabled(model.queue.isEmpty)
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.padding(.vertical)
|
|
||||||
|
|
||||||
.font(.system(size: 24))
|
.font(.system(size: 24))
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var barHeight: Double {
|
var barHeight: Double {
|
||||||
75
|
55
|
||||||
}
|
}
|
||||||
|
|
||||||
var details: some View {
|
var details: some View {
|
||||||
@ -147,8 +131,6 @@ struct ControlsBar: View {
|
|||||||
if let video = model.currentVideo {
|
if let video = model.currentVideo {
|
||||||
Group {
|
Group {
|
||||||
Section {
|
Section {
|
||||||
Text(video.title)
|
|
||||||
|
|
||||||
if accounts.app.supportsUserPlaylists && accounts.signedIn {
|
if accounts.app.supportsUserPlaylists && accounts.signedIn {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
@ -165,14 +147,14 @@ struct ControlsBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ShareButton(
|
|
||||||
contentItem: .init(video: model.currentVideo),
|
|
||||||
presentingShareSheet: $presentingShareSheet,
|
|
||||||
shareURL: $shareURL
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShareButton(
|
||||||
|
contentItem: .init(video: model.currentVideo),
|
||||||
|
presentingShareSheet: $presentingShareSheet,
|
||||||
|
shareURL: $shareURL
|
||||||
|
)
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
NavigationModel.openChannel(
|
NavigationModel.openChannel(
|
||||||
@ -208,6 +190,12 @@ struct ControlsBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
model.closeCurrentItem()
|
||||||
|
} label: {
|
||||||
|
Label("Close Video", systemImage: "xmark")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.labelStyle(.automatic)
|
.labelStyle(.automatic)
|
||||||
}
|
}
|
||||||
@ -234,9 +222,7 @@ struct ControlsBar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var authorAvatar: some View {
|
private var authorAvatar: some View {
|
||||||
Button {
|
Group {
|
||||||
model.togglePlayer()
|
|
||||||
} label: {
|
|
||||||
if let video = model.currentItem?.video, let url = video.channel.thumbnailURL {
|
if let video = model.currentItem?.video, let url = video.channel.thumbnailURL {
|
||||||
WebImage(url: url)
|
WebImage(url: url)
|
||||||
.resizable()
|
.resizable()
|
||||||
@ -246,9 +232,15 @@ struct ControlsBar: View {
|
|||||||
.retryOnAppear(true)
|
.retryOnAppear(true)
|
||||||
.indicator(.activity)
|
.indicator(.activity)
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "play.rectangle")
|
ZStack {
|
||||||
.foregroundColor(.accentColor)
|
Color(white: 0.8)
|
||||||
.font(.system(size: 30))
|
.opacity(0.5)
|
||||||
|
|
||||||
|
Image(systemName: "play.rectangle")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 44, height: 44, alignment: .leading)
|
.frame(width: 44, height: 44, alignment: .leading)
|
||||||
|
@ -24,8 +24,8 @@ final class AppleAVPlayerViewController: NSViewController {
|
|||||||
playerView.player = playerModel.avPlayerBackend.avPlayer
|
playerView.player = playerModel.avPlayerBackend.avPlayer
|
||||||
pictureInPictureDelegate.playerModel = playerModel
|
pictureInPictureDelegate.playerModel = playerModel
|
||||||
|
|
||||||
|
playerView.controlsStyle = .none
|
||||||
playerView.allowsPictureInPicturePlayback = true
|
playerView.allowsPictureInPicturePlayback = true
|
||||||
playerView.showsFullScreenToggleButton = true
|
|
||||||
|
|
||||||
playerView.pictureInPictureDelegate = pictureInPictureDelegate
|
playerView.pictureInPictureDelegate = pictureInPictureDelegate
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user