mirror of
https://github.com/yattee/yattee.git
synced 2025-10-17 04:48:17 +00:00
Minor UI changes
This commit is contained in:
@@ -6,8 +6,8 @@ struct ChaptersView: View {
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
|
||||
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")) {
|
||||
ForEach(chapters) { chapter in
|
||||
Button {
|
||||
@@ -18,18 +18,17 @@ struct ChaptersView: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(player.currentVideo?.title ?? "")
|
||||
}
|
||||
}
|
||||
.id(UUID())
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
.listStyle(.inset)
|
||||
#elseif os(iOS)
|
||||
#elseif os(iOS)
|
||||
.listStyle(.grouped)
|
||||
#else
|
||||
#else
|
||||
.listStyle(.plain)
|
||||
#endif
|
||||
#endif
|
||||
} else {
|
||||
NoCommentsView(text: "No chapters information available", systemImage: "xmark.circle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func chapterButtonLabel(_ chapter: Chapter) -> some View {
|
||||
|
@@ -99,6 +99,7 @@ struct CommentView: View {
|
||||
#if os(tvOS)
|
||||
.padding(.horizontal, 20)
|
||||
#endif
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
|
||||
private var authorAvatar: some View {
|
||||
|
@@ -22,12 +22,7 @@ struct CommentsView: View {
|
||||
.onAppear {
|
||||
comments.loadNextPageIfNeeded(current: comment)
|
||||
}
|
||||
.padding(.bottom, comment == last ? 5 : 0)
|
||||
|
||||
if comment != last {
|
||||
Divider()
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.borderBottom(height: comment != last ? 0.5 : 0, color: Color("ControlsBorderColor"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct ControlsOverlay: View {
|
||||
@EnvironmentObject<NetworkStateModel> private var networkState
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlayerControlsModel> private var model
|
||||
|
||||
@@ -165,7 +166,7 @@ struct ControlsOverlay: View {
|
||||
Text("hw decoder: \(player.mpvBackend.hwDecoder)")
|
||||
Text("dropped: \(player.mpvBackend.frameDropCount)")
|
||||
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))")
|
||||
}
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
|
@@ -6,7 +6,11 @@ struct NetworkState: View {
|
||||
|
||||
var body: some View {
|
||||
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?
|
||||
#endif
|
||||
|
||||
@Default(.controlsBarInPlayer) private var controlsBarInPlayer
|
||||
|
||||
init(player: PlayerModel, thumbnails: ThumbnailsModel) {
|
||||
self.player = player
|
||||
self.thumbnails = thumbnails
|
||||
@@ -191,12 +189,13 @@ struct PlayerControls: View {
|
||||
HStack(spacing: 20) {
|
||||
#if !os(tvOS)
|
||||
fullscreenButton
|
||||
pipButton
|
||||
|
||||
#if os(iOS)
|
||||
pipButton
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
|
||||
button("overlay", systemImage: "info.circle") {}
|
||||
|
||||
button("settings", systemImage: "gearshape", active: model.presentingControlsOverlay) {
|
||||
withAnimation(Self.animation) {
|
||||
model.presentingControlsOverlay.toggle()
|
||||
|
@@ -114,12 +114,13 @@ struct TimelineView: View {
|
||||
ZStack(alignment: .leading) {
|
||||
ZStack(alignment: .leading) {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.1))
|
||||
.fill(Color.white.opacity(0.2))
|
||||
.frame(maxHeight: height)
|
||||
.offset(x: current * oneUnitWidth)
|
||||
.zIndex(1)
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.5))
|
||||
.fill(Color.white.opacity(0.6))
|
||||
.frame(maxHeight: height)
|
||||
.frame(width: current * oneUnitWidth)
|
||||
.zIndex(1)
|
||||
@@ -187,7 +188,7 @@ struct TimelineView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
.background(GeometryReader { proxy in
|
||||
.overlay(GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
self.size = proxy.size
|
||||
@@ -265,7 +266,6 @@ struct TimelineView: View {
|
||||
}
|
||||
|
||||
var segments: [Segment] {
|
||||
// [.init(category: "outro", segment: [25,30], uuid: UUID().uuidString, videoDuration: 100)] ??
|
||||
player.sponsorBlock.segments
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ struct TimelineView: View {
|
||||
var chaptersLayers: some View {
|
||||
ForEach(chapters) { chapter in
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color("AppBlueColor"))
|
||||
.fill(Color.orange)
|
||||
.frame(maxWidth: 2, maxHeight: 12)
|
||||
.offset(x: (chapter.start * oneUnitWidth) - 1)
|
||||
}
|
||||
|
@@ -53,10 +53,81 @@ struct VideoDetails: View {
|
||||
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(
|
||||
_ label: String,
|
||||
_ symbolName: String,
|
||||
_ destination: DetailsPage,
|
||||
_ active: Bool = true,
|
||||
pageChangeAction: (() -> Void)? = nil
|
||||
) -> some View {
|
||||
Button(action: {
|
||||
@@ -69,25 +140,25 @@ struct VideoDetails: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: symbolName)
|
||||
|
||||
if playerDetailsPageButtonLabelStyle.text {
|
||||
if playerDetailsPageButtonLabelStyle.text && player.playerSize.width > 450 {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 15)
|
||||
.lineLimit(1)
|
||||
.padding(.vertical, 4)
|
||||
.foregroundColor(currentPage == destination ? .white : .accentColor)
|
||||
.foregroundColor(currentPage == destination ? .white : (active ? Color.accentColor : .gray))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.background(currentPage == destination ? Color.accentColor : .clear)
|
||||
.background(currentPage == destination ? (active ? Color.accentColor : .gray) : .clear)
|
||||
.buttonStyle(.plain)
|
||||
.font(.system(size: 10).bold())
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.accentColor, lineWidth: 2)
|
||||
.stroke(active ? Color.accentColor : .gray, lineWidth: 2)
|
||||
.foregroundColor(.clear)
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -119,123 +190,6 @@ struct VideoDetails: View {
|
||||
.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 {
|
||||
Group {
|
||||
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 {
|
||||
VStack(spacing: 4) {
|
||||
HStack(spacing: 2) {
|
||||
|
@@ -2,13 +2,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct VideoDetailsPaddingModifier: ViewModifier {
|
||||
static var defaultAdditionalDetailsPadding: Double {
|
||||
#if os(macOS)
|
||||
5
|
||||
#else
|
||||
10
|
||||
#endif
|
||||
}
|
||||
static var defaultAdditionalDetailsPadding = 0.0
|
||||
|
||||
let geometry: GeometryProxy
|
||||
let aspectRatio: Double?
|
||||
|
@@ -61,10 +61,12 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// TODO: remove
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
_ = Self._printChanges()
|
||||
}
|
||||
#if DEBUG
|
||||
// TODO: remove
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
_ = Self._printChanges()
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
return HSplitView {
|
||||
@@ -159,7 +161,7 @@ struct VideoPlayerView: View {
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: 0) {
|
||||
if player.playingInPictureInPicture {
|
||||
pictureInPicturePlaceholder(geometry: geometry)
|
||||
pictureInPicturePlaceholder
|
||||
} else {
|
||||
playerView
|
||||
#if !os(tvOS)
|
||||
@@ -170,7 +172,7 @@ struct VideoPlayerView: View {
|
||||
fullScreen: player.playingFullScreen
|
||||
)
|
||||
)
|
||||
// .overlay(playerPlaceholder(geometry: geometry))
|
||||
.overlay(playerPlaceholder)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -183,15 +185,11 @@ struct VideoPlayerView: View {
|
||||
.gesture(
|
||||
DragGesture(coordinateSpace: .global)
|
||||
.onChanged { value in
|
||||
guard player.presentingPlayer else {
|
||||
return // swiftlint:disable:this implicit_return
|
||||
}
|
||||
guard player.presentingPlayer else { return }
|
||||
|
||||
let drag = value.translation.height
|
||||
|
||||
guard drag > 0 else {
|
||||
return // swiftlint:disable:this implicit_return
|
||||
}
|
||||
guard drag > 0 else { return }
|
||||
|
||||
guard drag < 100 else {
|
||||
player.hide()
|
||||
@@ -231,6 +229,7 @@ struct VideoPlayerView: View {
|
||||
#if os(iOS)
|
||||
if verticalSizeClass == .regular {
|
||||
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: fullScreenDetails)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
}
|
||||
|
||||
#else
|
||||
@@ -248,12 +247,6 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
if !fullScreenLayout {
|
||||
ControlsBar()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
||||
#if os(macOS)
|
||||
@@ -273,7 +266,6 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.transition(.asymmetric(insertion: .slide, removal: .identity))
|
||||
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
|
||||
#if os(iOS)
|
||||
.statusBar(hidden: player.playingFullScreen)
|
||||
@@ -326,7 +318,7 @@ struct VideoPlayerView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder func playerPlaceholder(geometry: GeometryProxy) -> some View {
|
||||
@ViewBuilder var playerPlaceholder: some View {
|
||||
if player.currentItem.isNil {
|
||||
ZStack(alignment: .topLeading) {
|
||||
HStack {
|
||||
@@ -359,11 +351,11 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
.background(Color.black)
|
||||
.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 {
|
||||
Spacer()
|
||||
VStack {
|
||||
@@ -389,7 +381,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
}
|
||||
.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)
|
||||
|
Reference in New Issue
Block a user