diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 8c19ae3f..2dd8ab28 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -148,9 +148,6 @@ final class PlayerModel: ObservableObject { of video: Video, preservingTime: Bool = false ) { - #if !os(macOS) - try? AVAudioSession.sharedInstance().setActive(false) - #endif resetSegments() sponsorBlock.loadSegments(videoID: video.videoID) @@ -353,13 +350,12 @@ final class PlayerModel: ObservableObject { } @objc func itemDidPlayToEndTime() { - #if !os(macOS) - try? AVAudioSession.sharedInstance().setActive(false) - #endif - currentItem.playbackTime = playerItemDuration if queue.isEmpty { + #if !os(macOS) + try? AVAudioSession.sharedInstance().setActive(false) + #endif addCurrentItemToHistory() resetQueue() #if os(tvOS) diff --git a/Model/ThumbnailsModel.swift b/Model/ThumbnailsModel.swift new file mode 100644 index 00000000..c8649f76 --- /dev/null +++ b/Model/ThumbnailsModel.swift @@ -0,0 +1,25 @@ +import Foundation + +final class ThumbnailsModel: ObservableObject { + @Published var unloadable = Set() + + func insertUnloadable(_ url: URL) { + unloadable.insert(url) + } + + func isUnloadable(_ url: URL!) -> Bool { + guard !url.isNil else { + return true + } + + return unloadable.contains(url) + } + + func loadableURL(_ url: URL!) -> URL? { + guard !url.isNil else { + return nil + } + + return isUnloadable(url) ? nil : url + } +} diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index ec26b13b..f8dd8d5f 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -308,6 +308,9 @@ 37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; }; 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; }; 37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; }; + 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; }; + 37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; }; + 37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; }; 37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; 37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; 37C3A241272359900087A57A /* Double+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C3A240272359900087A57A /* Double+Format.swift */; }; @@ -585,6 +588,7 @@ 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = ""; }; 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItemBridge.swift; sourceTree = ""; }; 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMTime+DefaultTimescale.swift"; sourceTree = ""; }; + 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailsModel.swift; sourceTree = ""; }; 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentsModel.swift; sourceTree = ""; }; 37C3A240272359900087A57A /* Double+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Format.swift"; sourceTree = ""; }; 37C3A24427235DA70087A57A /* ChannelPlaylist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylist.swift; sourceTree = ""; }; @@ -1074,23 +1078,24 @@ children = ( 3743B86627216A1E00261544 /* Accounts */, 3743B864272169E200261544 /* Applications */, - 3743B86527216A0600261544 /* Player */, - 37FB283F2721B20800A57617 /* Search */, - 374C0539272436DA009BDDBE /* SponsorBlock */, 37AAF28F26740715007FC770 /* Channel.swift */, 37C3A24427235DA70087A57A /* ChannelPlaylist.swift */, 37FB28402721B22200A57617 /* ContentItem.swift */, 37141672267A8E10006CA35D /* Country.swift */, 371F2F19269B43D300E4A7AB /* NavigationModel.swift */, + 3743B86527216A0600261544 /* Player */, 376578882685471400D4EA09 /* Playlist.swift */, 37BA794226DBA973002A0235 /* PlaylistsModel.swift */, 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */, + 37FB283F2721B20800A57617 /* Search */, 37EAD86E267B9ED100D9E01B /* Segment.swift */, 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */, + 374C0539272436DA009BDDBE /* SponsorBlock */, 3797758A2689345500DD52A8 /* Store.swift */, 37CEE4C02677B697005A1EFE /* Stream.swift */, 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */, 373CFADA269663F1003CB2C6 /* Thumbnail.swift */, + 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */, 3705B181267B4E4900704544 /* TrendingCategory.swift */, 37D4B19626717E1500C925CA /* Video.swift */, ); @@ -1618,6 +1623,7 @@ 37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, + 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 37BE0BD326A1D4780092E2DB /* Player.swift in Sources */, 37A9965E26D6F9B9006E3224 /* WatchNowView.swift in Sources */, @@ -1736,6 +1742,7 @@ 3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */, 37FB285F272225E800A57617 /* ContentItemView.swift in Sources */, 37FD43DC270470B70073EE42 /* InstancesSettings.swift in Sources */, + 37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */, 37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */, 37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */, @@ -1880,6 +1887,7 @@ 37AAF29226740715007FC770 /* Channel.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */, + 37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */, 3765788B2685471400D4EA09 /* Playlist.swift in Sources */, 376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */, diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 847045ef..4e94d5fc 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -15,6 +15,7 @@ struct ContentView: View { @StateObject private var recents = RecentsModel() @StateObject private var search = SearchModel() @StateObject private var subscriptions = SubscriptionsModel() + @StateObject private var thumbnailsModel = ThumbnailsModel() #if os(iOS) @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -44,6 +45,8 @@ struct ContentView: View { .environmentObject(recents) .environmentObject(search) .environmentObject(subscriptions) + .environmentObject(thumbnailsModel) + .sheet(isPresented: $navigation.presentingWelcomeScreen) { WelcomeScreen() .environmentObject(accounts) @@ -57,6 +60,7 @@ struct ContentView: View { .environmentObject(navigation) .environmentObject(player) .environmentObject(subscriptions) + .environmentObject(thumbnailsModel) } #elseif os(macOS) .sheet(isPresented: $player.presentingPlayer) { @@ -67,6 +71,7 @@ struct ContentView: View { .environmentObject(navigation) .environmentObject(player) .environmentObject(subscriptions) + .environmentObject(thumbnailsModel) } #endif #if !os(tvOS) diff --git a/Shared/Player/Player.swift b/Shared/Player/Player.swift index 7b30c440..9ef28a04 100644 --- a/Shared/Player/Player.swift +++ b/Shared/Player/Player.swift @@ -23,5 +23,7 @@ struct Player: UIViewControllerRepresentable { return controller } - func updateUIViewController(_: PlayerViewController, context _: Context) {} + func updateUIViewController(_: PlayerViewController, context _: Context) { + player.rebuildTVMenu() + } } diff --git a/Shared/Videos/HorizontalCells.swift b/Shared/Videos/HorizontalCells.swift index 97404625..938f25f8 100644 --- a/Shared/Videos/HorizontalCells.swift +++ b/Shared/Videos/HorizontalCells.swift @@ -15,7 +15,7 @@ struct HorizontalCells: View { .padding(.trailing, 20) .padding(.bottom, 40) #else - .frame(width: 300) + .frame(width: 285) #endif } } diff --git a/Shared/Videos/VideoCell.swift b/Shared/Videos/VideoCell.swift index ae25d386..1f830e5e 100644 --- a/Shared/Videos/VideoCell.swift +++ b/Shared/Videos/VideoCell.swift @@ -4,7 +4,7 @@ import SwiftUI struct VideoCell: View { var video: Video - @State private var lowQualityThumbnail = false + @State private var mediumQualityThumbnail = false @Environment(\.inNavigationView) private var inNavigationView @@ -14,6 +14,7 @@ struct VideoCell: View { #endif @EnvironmentObject private var player + @EnvironmentObject private var thumbnails var body: some View { Group { @@ -175,7 +176,7 @@ struct VideoCell: View { var thumbnail: some View { ZStack(alignment: .leading) { - thumbnailImage(quality: lowQualityThumbnail ? .medium : .maxresdefault) + thumbnailImage(quality: mediumQualityThumbnail ? .medium : .maxresdefault) VStack { HStack(alignment: .top) { @@ -207,20 +208,38 @@ struct VideoCell: View { } func thumbnailImage(quality: Thumbnail.Quality) -> some View { - WebImage(url: video.thumbnailURL(quality: quality)) - .resizable() - .placeholder { - Rectangle().fill(Color("PlaceholderColor")) + Group { + if let url = thumbnails.loadableURL(video.thumbnailURL(quality: quality)) { + WebImage(url: url) + .resizable() + .placeholder { + Rectangle().fill(Color("PlaceholderColor")) + } + .retryOnAppear(false) + .onFailure { _ in + if let url = video.thumbnailURL(quality: quality) { + thumbnails.insertUnloadable(url) + } + + if !thumbnails.isUnloadable(video.thumbnailURL(quality: .medium)) { + mediumQualityThumbnail = true + } + } + .indicator(.activity) + + #if os(tvOS) + .frame(minHeight: 320) + #endif + } else { + ZStack { + Color("PlaceholderColor") + Image(systemName: "exclamationmark.triangle") + } + .font(.system(size: 30)) } - .onFailure { _ in - lowQualityThumbnail = true - } - .indicator(.activity) - .mask(RoundedRectangle(cornerRadius: 12)) - .modifier(AspectRatioModifier()) - #if os(tvOS) - .frame(minHeight: 320) - #endif + } + .mask(RoundedRectangle(cornerRadius: 12)) + .modifier(AspectRatioModifier()) } func videoDetail(_ text: String, lineLimit: Int = 1) -> some View {