1
0
mirror of https://github.com/yattee/yattee.git synced 2024-12-13 05:40:32 +05:30

Model improvements

This commit is contained in:
Arkadiusz Fal 2022-08-31 21:24:46 +02:00
parent b220f212df
commit 0d3ccc00ce
23 changed files with 190 additions and 133 deletions

View File

@ -145,10 +145,10 @@ extension VideosAPI {
let startRange = line.range(withName: "start") let startRange = line.range(withName: "start")
guard let titleSubstringRange = Range(titleRange, in: description), guard let titleSubstringRange = Range(titleRange, in: description),
let startSubstringRange = Range(startRange, in: description), let startSubstringRange = Range(startRange, in: description) else { return nil }
let titleCapture = String(description[titleSubstringRange]),
let startCapture = String(description[startSubstringRange]) else { return nil }
let titleCapture = String(description[titleSubstringRange])
let startCapture = String(description[startSubstringRange])
let startComponents = startCapture.components(separatedBy: ":") let startComponents = startCapture.components(separatedBy: ":")
guard startComponents.count <= 3 else { return nil } guard startComponents.count <= 3 else { return nil }

View File

@ -79,7 +79,7 @@ final class AVPlayerBackend: PlayerBackend {
init(model: PlayerModel, controls: PlayerControlsModel?, playerTime: PlayerTimeModel?) { init(model: PlayerModel, controls: PlayerControlsModel?, playerTime: PlayerTimeModel?) {
self.model = model self.model = model
self.controls = controls self.controls = controls
self.playerTime = playerTime self.playerTime = playerTime ?? PlayerTimeModel.shared
addFrequentTimeObserver() addFrequentTimeObserver()
addInfrequentTimeObserver() addInfrequentTimeObserver()
@ -582,6 +582,7 @@ final class AVPlayerBackend: PlayerBackend {
} }
if player.timeControlStatus == .playing { if player.timeControlStatus == .playing {
self.model.objectWillChange.send()
if player.rate != self.model.currentRate { if player.rate != self.model.currentRate {
player.rate = self.model.currentRate player.rate = self.model.currentRate
} }

View File

@ -129,7 +129,7 @@ final class MPVBackend: PlayerBackend {
) { ) {
self.model = model self.model = model
self.controls = controls self.controls = controls
self.playerTime = playerTime self.playerTime = playerTime ?? PlayerTimeModel.shared
self.networkState = networkState self.networkState = networkState
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in

View File

@ -272,7 +272,11 @@ final class MPVClient: ObservableObject {
UIView.animate(withDuration: 0.2, animations: { UIView.animate(withDuration: 0.2, animations: {
let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio
let height = [self.backend.model.playerSize.height, self.backend.model.playerSize.width / aspectRatio].min()! let height = [self.backend.model.playerSize.height, self.backend.model.playerSize.width / aspectRatio].min()!
let offsetY = self.backend.model.playingFullScreen ? ((self.backend.model.playerSize.height / 2.0) - (height / 2)) : 0 var insets = 0.0
#if os(iOS)
insets = OrientationTracker.shared.currentInterfaceOrientation.isPortrait ? SafeArea.insets.bottom : 0
#endif
let offsetY = self.backend.model.playingFullScreen ? ((self.backend.model.playerSize.height / 2.0) - ((height + insets) / 2)) : 0
self.glView?.frame = CGRect(x: 0, y: offsetY, width: roundedWidth, height: height) self.glView?.frame = CGRect(x: 0, y: offsetY, width: roundedWidth, height: height)
}) { completion in }) { completion in
if completion { if completion {

View File

@ -130,8 +130,8 @@ extension PlayerBackend {
return return
} }
#endif #endif
self.playerTime.currentTime = self.currentTime ?? .zero PlayerTimeModel.shared.currentTime = self.currentTime ?? .zero
self.playerTime.duration = self.playerItemDuration ?? .zero PlayerTimeModel.shared.duration = self.playerItemDuration ?? .zero
completionHandler?() completionHandler?()
} }
} }

View File

@ -53,7 +53,7 @@ final class PlayerModel: ObservableObject {
var mpvPlayerView = MPVPlayerView() var mpvPlayerView = MPVPlayerView()
@Published var presentingPlayer = false @Published var presentingPlayer = false { didSet { handlePresentationChange() } }
@Published var activeBackend = PlayerBackendType.mpv @Published var activeBackend = PlayerBackendType.mpv
var avPlayerBackend: AVPlayerBackend! var avPlayerBackend: AVPlayerBackend!
@ -324,11 +324,7 @@ final class PlayerModel: ObservableObject {
} }
var playerItemDurationWithoutSponsorSegments: CMTime? { var playerItemDurationWithoutSponsorSegments: CMTime? {
guard let playerItemDuration = playerItemDuration, !playerItemDuration.seconds.isZero else { PlayerTimeModel.shared.duration - .secondsInDefaultTimescale(
return nil
}
return playerItemDuration - .secondsInDefaultTimescale(
sponsorBlock.segments.reduce(0) { $0 + $1.duration } sponsorBlock.segments.reduce(0) { $0 + $1.duration }
) )
} }
@ -496,18 +492,7 @@ final class PlayerModel: ObservableObject {
} }
private func handlePresentationChange() { private func handlePresentationChange() {
var delay = 0.0 backend.setNeedsDrawing(presentingPlayer)
#if os(iOS)
if presentingPlayer {
delay = 0.2
}
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self = self else { return }
self.backend.setNeedsDrawing(self.presentingPlayer)
}
controls.hide() controls.hide()

View File

@ -3,6 +3,7 @@ import Foundation
import SwiftUI import SwiftUI
final class PlayerTimeModel: ObservableObject { final class PlayerTimeModel: ObservableObject {
static let shared = PlayerTimeModel()
static let timePlaceholder = "--:--" static let timePlaceholder = "--:--"
@Published var currentTime = CMTime.zero @Published var currentTime = CMTime.zero

View File

@ -24,17 +24,12 @@ final class SearchModel: ObservableObject {
func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) { func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) {
changeHandler(query) changeHandler(query)
let newResource = accounts.api.search(query, page: nil)
guard newResource != resource else {
return
}
page = nil page = nil
resource = newResource
resource.addObserver(store)
if !query.isEmpty { if !query.isEmpty {
resource = accounts.api.search(query, page: nil)
resource.addObserver(store)
loadResource() loadResource()
} }
} }

View File

@ -278,6 +278,14 @@ enum VisibleSection: String, CaseIterable, Comparable, Defaults.Serializable {
enum WatchedVideoStyle: String, Defaults.Serializable { enum WatchedVideoStyle: String, Defaults.Serializable {
case nothing, badge, decreasedOpacity, both case nothing, badge, decreasedOpacity, both
var isShowingBadge: Bool {
self == .badge || self == .both
}
var isDecreasingOpacity: Bool {
self == .decreasedOpacity || self == .both
}
} }
enum WatchedVideoBadgeColor: String, Defaults.Serializable { enum WatchedVideoBadgeColor: String, Defaults.Serializable {

View File

@ -12,10 +12,8 @@ struct ContentView: View {
@EnvironmentObject<CommentsModel> private var comments @EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<InstancesModel> private var instances @EnvironmentObject<InstancesModel> private var instances
@EnvironmentObject<NavigationModel> private var navigation @EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<NetworkStateModel> private var networkState
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlayerControlsModel> private var playerControls @EnvironmentObject<PlayerControlsModel> private var playerControls
@EnvironmentObject<PlayerTimeModel> private var playerTime
@EnvironmentObject<PlaylistsModel> private var playlists @EnvironmentObject<PlaylistsModel> private var playlists
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var search @EnvironmentObject<SearchModel> private var search
@ -60,9 +58,7 @@ struct ContentView: View {
.environmentObject(comments) .environmentObject(comments)
.environmentObject(instances) .environmentObject(instances)
.environmentObject(navigation) .environmentObject(navigation)
.environmentObject(networkState)
.environmentObject(player) .environmentObject(player)
.environmentObject(playerTime)
.environmentObject(playlists) .environmentObject(playlists)
.environmentObject(recents) .environmentObject(recents)
.environmentObject(search) .environmentObject(search)

View File

@ -31,19 +31,28 @@ struct ChapterView: View {
} }
@ViewBuilder func smallImage(_ chapter: Chapter) -> some View { @ViewBuilder func smallImage(_ chapter: Chapter) -> some View {
WebImage(url: chapter.image) if #available(iOS 15, macOS 12, *) {
.resizable() AsyncImage(url: chapter.image) { image in
.placeholder { image
ProgressView() .resizable()
} placeholder: {
Rectangle().foregroundColor(Color("PlaceholderColor"))
} }
.indicator(.activity) } else {
#if os(tvOS) WebImage(url: chapter.image)
.frame(width: thumbnailWidth, height: 140) .resizable()
.mask(RoundedRectangle(cornerRadius: 12)) .placeholder {
#else ProgressView()
.frame(width: thumbnailWidth, height: 60) }
.mask(RoundedRectangle(cornerRadius: 6)) .indicator(.activity)
#endif #if os(tvOS)
.frame(width: thumbnailWidth, height: 140)
.mask(RoundedRectangle(cornerRadius: 12))
#else
.frame(width: thumbnailWidth, height: 60)
.mask(RoundedRectangle(cornerRadius: 6))
#endif
}
} }
private var thumbnailWidth: Double { private var thumbnailWidth: Double {

View File

@ -72,7 +72,7 @@ struct PlayerControls: View {
} }
.offset(y: playerControlsLayout.osdVerticalOffset + 5) .offset(y: playerControlsLayout.osdVerticalOffset + 5)
if model.presentingControls, !model.presentingOverlays { Section {
#if !os(tvOS) #if !os(tvOS)
HStack { HStack {
seekBackwardButton seekBackwardButton
@ -160,7 +160,8 @@ struct PlayerControls: View {
.offset(y: -playerControlsLayout.timelineHeight - 5) .offset(y: -playerControlsLayout.timelineHeight - 5)
#endif #endif
} }
} }.opacity(model.presentingControls && !model.presentingOverlays ? 1 : 0)
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -219,14 +220,23 @@ struct PlayerControls: View {
let video = item.video, let video = item.video,
let url = thumbnails.best(video) let url = thumbnails.best(video)
{ {
WebImage(url: url) if #available(iOS 15, macOS 12, *) {
.resizable() AsyncImage(url: url) { image in
.placeholder { image
Rectangle().fill(Color("PlaceholderColor")) .resizable()
} placeholder: {
Rectangle().foregroundColor(Color("PlaceholderColor"))
} }
.retryOnAppear(true) } else {
.indicator(.activity) WebImage(url: url)
.frame(maxWidth: .infinity, maxHeight: .infinity) .resizable()
.placeholder {
Rectangle().fill(Color("PlaceholderColor"))
}
.retryOnAppear(true)
.indicator(.activity)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
} }
} }

View File

@ -60,7 +60,7 @@ enum PlayerControlsLayout: String, CaseIterable, Defaults.Serializable {
case .veryLarge: case .veryLarge:
return 40 return 40
case .large: case .large:
return 30 return 25
case .medium: case .medium:
return 25 return 25
case .small: case .small:

View File

@ -44,9 +44,10 @@ struct TimelineView: View {
@Environment(\.verticalSizeClass) private var verticalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass
#endif #endif
@ObservedObject private var playerTime = PlayerTimeModel.shared
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlayerControlsModel> private var controls @EnvironmentObject<PlayerControlsModel> private var controls
@EnvironmentObject<PlayerTimeModel> private var playerTime
@Default(.playerControlsLayout) private var regularPlayerControlsLayout @Default(.playerControlsLayout) private var regularPlayerControlsLayout
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
@ -392,7 +393,7 @@ struct TimelineView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let playerModel = PlayerModel() let playerModel = PlayerModel()
playerModel.currentItem = .init(Video.fixture) playerModel.currentItem = .init(Video.fixture)
let playerTimeModel = PlayerTimeModel() let playerTimeModel = PlayerTimeModel.shared
playerTimeModel.player = playerModel playerTimeModel.player = playerModel
playerTimeModel.currentTime = .secondsInDefaultTimescale(33) playerTimeModel.currentTime = .secondsInDefaultTimescale(33)
playerTimeModel.duration = .secondsInDefaultTimescale(100) playerTimeModel.duration = .secondsInDefaultTimescale(100)
@ -400,7 +401,6 @@ struct TimelineView_Previews: PreviewProvider {
TimelineView() TimelineView()
} }
.environmentObject(playerModel) .environmentObject(playerModel)
.environmentObject(playerTimeModel)
.environmentObject(PlayerControlsModel()) .environmentObject(PlayerControlsModel())
.padding() .padding()
} }

View File

@ -50,14 +50,20 @@ extension VideoPlayerView {
return return
} }
if orientation.isLandscape { orientationDebouncer.callback = {
playerControls.presentingControls = false DispatchQueue.main.async {
player.enterFullScreen(showControls: false) if orientation.isLandscape {
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) playerControls.presentingControls = false
} else { player.enterFullScreen(showControls: false)
player.exitFullScreen(showControls: false) Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) } else {
player.exitFullScreen(showControls: false)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
}
}
} }
orientationDebouncer.call()
} }
} }
} }

View File

@ -3,6 +3,7 @@ import AVKit
import CoreMotion import CoreMotion
#endif #endif
import Defaults import Defaults
import Repeat
import Siesta import Siesta
import SwiftUI import SwiftUI
@ -41,6 +42,7 @@ struct VideoPlayerView: View {
@State internal var orientation = UIInterfaceOrientation.portrait @State internal var orientation = UIInterfaceOrientation.portrait
@State internal var lastOrientation: UIInterfaceOrientation? @State internal var lastOrientation: UIInterfaceOrientation?
@State internal var orientationDebouncer = Debouncer(.milliseconds(300))
#elseif os(macOS) #elseif os(macOS)
var hoverThrottle = Throttle(interval: 0.5) var hoverThrottle = Throttle(interval: 0.5)
var mouseLocation: CGPoint { NSEvent.mouseLocation } var mouseLocation: CGPoint { NSEvent.mouseLocation }

View File

@ -27,6 +27,7 @@ struct SearchView: View {
@EnvironmentObject<SearchModel> private var state @EnvironmentObject<SearchModel> private var state
private var favorites = FavoritesModel.shared private var favorites = FavoritesModel.shared
@Default(.recentlyOpened) private var recentlyOpened
@Default(.saveRecents) private var saveRecents @Default(.saveRecents) private var saveRecents
private var videos = [Video]() private var videos = [Video]()
@ -287,11 +288,11 @@ struct SearchView: View {
VStack { VStack {
List { List {
Section(header: Text("Recents")) { Section(header: Text("Recents")) {
if recentItems.isEmpty { if recentlyOpened.isEmpty {
Text("Search history is empty") Text("Search history is empty")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
ForEach(recentItems) { item in ForEach(recentlyOpened, id: \.tag) { item in
recentItemButton(item) recentItemButton(item)
} }
} }
@ -347,7 +348,6 @@ struct SearchView: View {
item.type == .channel ? RecentsModel.symbolSystemImage(item.title) : item.type == .channel ? RecentsModel.symbolSystemImage(item.title) :
"list.and.film" "list.and.film"
Label(item.title, systemImage: systemImage) Label(item.title, systemImage: systemImage)
.lineLimit(1)
} }
.contextMenu { .contextMenu {
removeButton(item) removeButton(item)
@ -391,10 +391,6 @@ struct SearchView: View {
searchDate != .any || searchDuration != .any searchDate != .any || searchDuration != .any
} }
private var recentItems: [RecentItem] {
Defaults[.recentlyOpened].reversed()
}
private var searchSortOrderPicker: some View { private var searchSortOrderPicker: some View {
Picker("Sort", selection: $searchSortOrder) { Picker("Sort", selection: $searchSortOrder) {
ForEach(SearchQuery.SortOrder.allCases) { sortOrder in ForEach(SearchQuery.SortOrder.allCases) { sortOrder in

View File

@ -70,20 +70,37 @@ struct VideoBanner: View {
#endif #endif
} }
private var smallThumbnail: some View { @ViewBuilder private var smallThumbnail: some View {
WebImage(url: video?.thumbnailURL(quality: .medium)) let url = video?.thumbnailURL(quality: .medium)
.resizable() if #available(iOS 15, macOS 12, *) {
.placeholder { AsyncImage(url: url) { image in
ProgressView() image
.resizable()
} placeholder: {
Rectangle().foregroundColor(Color("PlaceholderColor"))
} }
.indicator(.activity) #if os(tvOS)
#if os(tvOS) .frame(width: thumbnailWidth, height: 140)
.frame(width: thumbnailWidth, height: 140) .mask(RoundedRectangle(cornerRadius: 12))
.mask(RoundedRectangle(cornerRadius: 12)) #else
#else .frame(width: thumbnailWidth, height: 60)
.frame(width: thumbnailWidth, height: 60) .mask(RoundedRectangle(cornerRadius: 6))
.mask(RoundedRectangle(cornerRadius: 6)) #endif
#endif } else {
WebImage(url: url)
.resizable()
.placeholder {
ProgressView()
}
.indicator(.activity)
#if os(tvOS)
.frame(width: thumbnailWidth, height: 140)
.mask(RoundedRectangle(cornerRadius: 12))
#else
.frame(width: thumbnailWidth, height: 60)
.mask(RoundedRectangle(cornerRadius: 6))
#endif
}
} }
private var thumbnailWidth: Double { private var thumbnailWidth: Double {

View File

@ -382,7 +382,7 @@ struct VideoCell: View {
HStack(alignment: .center) { HStack(alignment: .center) {
if saveHistory, if saveHistory,
watchedVideoStyle == .badge || watchedVideoStyle == .both, watchedVideoStyle.isShowingBadge,
watch?.finished ?? false watch?.finished ?? false
{ {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
@ -419,27 +419,32 @@ struct VideoCell: View {
private var thumbnailImage: some View { private var thumbnailImage: some View {
Group { Group {
if let url = thumbnails.best(video) { let url = thumbnails.best(video)
if #available(iOS 15, macOS 12, *) {
AsyncImage(url: url) { image in
image
.resizable()
} placeholder: {
Rectangle().foregroundColor(Color("PlaceholderColor"))
}
#if os(tvOS)
.frame(minHeight: 320)
#endif
} else {
WebImage(url: url) WebImage(url: url)
.resizable() .resizable()
.placeholder { .placeholder {
Rectangle().fill(Color("PlaceholderColor")) Rectangle().foregroundColor(Color("PlaceholderColor"))
} }
.retryOnAppear(true) .retryOnAppear(true)
.onFailure { _ in .onFailure { _ in
guard let url = url else { return }
thumbnails.insertUnloadable(url) thumbnails.insertUnloadable(url)
} }
.indicator(.activity)
#if os(tvOS) #if os(tvOS)
.frame(minHeight: 320) .frame(minHeight: 320)
#endif #endif
} else {
ZStack {
Color("PlaceholderColor")
Image(systemName: "exclamationmark.triangle")
}
.font(.system(size: 30))
} }
} }
.mask(RoundedRectangle(cornerRadius: thumbnailRoundingCornerRadius)) .mask(RoundedRectangle(cornerRadius: thumbnailRoundingCornerRadius))

View File

@ -37,15 +37,23 @@ struct ChannelCell: View {
.opacity(0.6) .opacity(0.6)
} }
.foregroundColor(.secondary) .foregroundColor(.secondary)
if #available(iOS 15, macOS 12, *) {
WebImage(url: channel.thumbnailURL) AsyncImage(url: channel.thumbnailURL) { image in
.resizable() image
.placeholder { .resizable()
Rectangle().fill(Color("PlaceholderColor")) } placeholder: {
Rectangle().foregroundColor(Color("PlaceholderColor"))
} }
.indicator(.activity) } else {
.frame(width: 88, height: 88) WebImage(url: channel.thumbnailURL)
.clipShape(Circle()) .resizable()
.placeholder {
Rectangle().fill(Color("PlaceholderColor"))
}
.indicator(.activity)
.frame(width: 88, height: 88)
.clipShape(Circle())
}
DetailBadge(text: channel.name, style: .prominent) DetailBadge(text: channel.name, style: .prominent)

View File

@ -37,15 +37,23 @@ struct ChannelPlaylistCell: View {
} }
.foregroundColor(.secondary) .foregroundColor(.secondary)
WebImage(url: playlist.thumbnailURL) if #available(iOS 15, macOS 12, *) {
.resizable() AsyncImage(url: playlist.thumbnailURL) { image in
.placeholder { image
Rectangle().fill(Color("PlaceholderColor")) .resizable()
} placeholder: {
Rectangle().foregroundColor(Color("PlaceholderColor"))
} }
.indicator(.activity) } else {
.frame(width: 165, height: 88) WebImage(url: playlist.thumbnailURL)
.clipShape(RoundedRectangle(cornerRadius: 10)) .resizable()
.placeholder {
Rectangle().fill(Color("PlaceholderColor"))
}
.indicator(.activity)
.frame(width: 165, height: 88)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
Group { Group {
DetailBadge(text: playlist.title, style: .prominent) DetailBadge(text: playlist.title, style: .prominent)
.lineLimit(2) .lineLimit(2)

View File

@ -12,7 +12,6 @@ struct ControlsBar: View {
@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<PlayerModel> private var model @EnvironmentObject<PlayerModel> private var model
@EnvironmentObject<PlaylistsModel> private var playlists @EnvironmentObject<PlaylistsModel> private var playlists
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@ -63,8 +62,8 @@ struct ControlsBar: View {
} }
} else if detailsToggleFullScreen { } else if detailsToggleFullScreen {
Button { Button {
playerControls.presentingControlsOverlay = false model.controls.presentingControlsOverlay = false
playerControls.presentingControls = false model.controls.presentingControls = false
withAnimation { withAnimation {
fullScreen.toggle() fullScreen.toggle()
} }
@ -83,7 +82,7 @@ struct ControlsBar: View {
var controls: some View { var controls: some View {
HStack(spacing: 4) { HStack(spacing: 4) {
Group { Group {
if playerControls.isPlaying { if model.controls.isPlaying {
Button(action: { Button(action: {
model.pause() model.pause()
}) { }) {
@ -103,7 +102,7 @@ struct ControlsBar: View {
} }
} }
} }
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil) .disabled(model.controls.isLoadingVideo || model.currentItem.isNil)
Button(action: { model.advanceToNextItem() }) { Button(action: { model.advanceToNextItem() }) {
Label("Next", systemImage: "forward.fill") Label("Next", systemImage: "forward.fill")
@ -268,13 +267,22 @@ struct ControlsBar: View {
private var authorAvatar: some View { private var authorAvatar: some View {
Group { Group {
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) if #available(iOS 15, macOS 12, *) {
.resizable() AsyncImage(url: url) { image in
.placeholder { image
Rectangle().fill(Color("PlaceholderColor")) .resizable()
} placeholder: {
Rectangle().foregroundColor(Color("PlaceholderColor"))
} }
.retryOnAppear(true) } else {
.indicator(.activity) WebImage(url: url)
.resizable()
.placeholder {
Rectangle().fill(Color("PlaceholderColor"))
}
.retryOnAppear(true)
.indicator(.activity)
}
} else { } else {
ZStack { ZStack {
Color(white: 0.6) Color(white: 0.6)

View File

@ -40,7 +40,6 @@ struct YatteeApp: App {
@StateObject private var networkState = NetworkStateModel() @StateObject private var networkState = NetworkStateModel()
@StateObject private var player = PlayerModel() @StateObject private var player = PlayerModel()
@StateObject private var playerControls = PlayerControlsModel() @StateObject private var playerControls = PlayerControlsModel()
@StateObject private var playerTime = PlayerTimeModel()
@StateObject private var playlists = PlaylistsModel() @StateObject private var playlists = PlaylistsModel()
@StateObject private var recents = RecentsModel() @StateObject private var recents = RecentsModel()
@StateObject private var search = SearchModel() @StateObject private var search = SearchModel()
@ -63,7 +62,6 @@ struct YatteeApp: App {
.environmentObject(networkState) .environmentObject(networkState)
.environmentObject(player) .environmentObject(player)
.environmentObject(playerControls) .environmentObject(playerControls)
.environmentObject(playerTime)
.environmentObject(playlists) .environmentObject(playlists)
.environmentObject(recents) .environmentObject(recents)
.environmentObject(seek) .environmentObject(seek)
@ -137,7 +135,6 @@ struct YatteeApp: App {
.environmentObject(networkState) .environmentObject(networkState)
.environmentObject(player) .environmentObject(player)
.environmentObject(playerControls) .environmentObject(playerControls)
.environmentObject(playerTime)
.environmentObject(playlists) .environmentObject(playlists)
.environmentObject(recents) .environmentObject(recents)
.environmentObject(search) .environmentObject(search)
@ -205,9 +202,10 @@ struct YatteeApp: App {
player.controls = playerControls player.controls = playerControls
player.navigation = navigation player.navigation = navigation
player.networkState = networkState player.networkState = networkState
player.playerTime = playerTime
player.seek = seek player.seek = seek
PlayerTimeModel.shared.player = player
if !accounts.current.isNil { if !accounts.current.isNil {
player.restoreQueue() player.restoreQueue()
} }