mirror of
https://github.com/yattee/yattee.git
synced 2024-12-13 05:40:32 +05:30
Model improvements
This commit is contained in:
parent
b220f212df
commit
0d3ccc00ce
@ -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 }
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user