diff --git a/Apple TV/TVNavigationView.swift b/Apple TV/TVNavigationView.swift index f62f74b9..7b89162f 100644 --- a/Apple TV/TVNavigationView.swift +++ b/Apple TV/TVNavigationView.swift @@ -13,7 +13,7 @@ struct TVNavigationView: View { .tabItem { Text("Subscriptions") } .tag(TabSelection.subscriptions) - PopularVideosView() + PopularView() .tabItem { Text("Popular") } .tag(TabSelection.popular) diff --git a/Apple TV/VideoCellView.swift b/Apple TV/VideoCellView.swift deleted file mode 100644 index 587c3a0f..00000000 --- a/Apple TV/VideoCellView.swift +++ /dev/null @@ -1,108 +0,0 @@ -import SwiftUI - -struct VideoCellView: View { - @EnvironmentObject private var navigationState - - var video: Video - - var body: some View { - Button(action: { navigationState.playVideo(video) }) { - VStack(alignment: .leading) { - ZStack { - if let url = video.thumbnailURL(quality: .high) { - AsyncImage(url: url) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 550, height: 310) - } placeholder: { - ProgressView() - } - .mask(RoundedRectangle(cornerRadius: 12)) - } else { - Image(systemName: "exclamationmark.square") - .frame(width: 550, height: 310) - } - - VStack { - HStack(alignment: .top) { - if video.live { - DetailBadge(text: "Live", style: .outstanding) - } else if video.upcoming { - DetailBadge(text: "Upcoming", style: .informational) - } - - Spacer() - - DetailBadge(text: video.author, style: .prominent) - } - .padding(10) - - Spacer() - - HStack(alignment: .top) { - Spacer() - - if let time = video.playTime { - DetailBadge(text: time, style: .prominent) - } - } - .padding(10) - } - } - .frame(width: 550, height: 310) - - VStack(alignment: .leading) { - Text(video.title) - .bold() - .lineLimit(2) - .multilineTextAlignment(.leading) - .padding(.horizontal) - .padding(.bottom, 2) - .frame(minHeight: 80, alignment: .top) - .truncationMode(.middle) - - HStack(spacing: 8) { - if video.publishedDate != nil || video.views != 0 { - if let date = video.publishedDate { - Image(systemName: "calendar") - Text(date) - } - - if video.views != 0 { - Image(systemName: "eye") - Text(video.viewsCount) - } - } else { - Section { - if video.live { - Image(systemName: "camera.fill") - Text("Premiering now") - } else { - Image(systemName: "questionmark.app.fill") - Text("date and views unavailable") - } - } - .opacity(0.6) - } - } - .padding([.horizontal, .bottom]) - .foregroundColor(.secondary) - } - } - .frame(width: 550, alignment: .leading) - } - .buttonStyle(.plain) - .padding(.vertical) - } -} - -struct VideoCellView_Preview: PreviewProvider { - static var previews: some View { - HStack { - VideoCellView(video: Video.fixture) - VideoCellView(video: Video.fixtureUpcomingWithoutPublishedOrViews) - VideoCellView(video: Video.fixtureLiveWithoutPublishedOrViews) - } - } -} diff --git a/Apple TV/VideoListRowView.swift b/Apple TV/VideoListRowView.swift deleted file mode 100644 index 0c86d0c5..00000000 --- a/Apple TV/VideoListRowView.swift +++ /dev/null @@ -1,215 +0,0 @@ -import SwiftUI - -struct VideoListRowView: View { - @EnvironmentObject private var navigationState - - @Environment(\.isFocused) private var focused: Bool - - #if os(iOS) - @Environment(\.verticalSizeClass) private var verticalSizeClass - #endif - - var video: Video - - var body: some View { - #if os(tvOS) - Button(action: { navigationState.playVideo(video) }) { - horizontalRow(detailsOnThumbnail: false) - } - #elseif os(macOS) - NavigationLink(destination: VideoPlayerView(video)) { - verticalRow - } - #else - ZStack { - #if os(macOS) - verticalRow - #else - if verticalSizeClass == .compact { - horizontalRow(padding: 4) - } else { - verticalRow - } - #endif - - NavigationLink(destination: VideoPlayerView(video)) { - EmptyView() - } - .buttonStyle(PlainButtonStyle()) - .opacity(0) - .frame(height: 0) - } - #endif - } - - func horizontalRow(detailsOnThumbnail: Bool = true, padding: Double = 0) -> some View { - HStack(alignment: .top, spacing: 2) { - if detailsOnThumbnail { - thumbnailWithDetails() - .padding(padding) - } else { - thumbnail(.medium, maxWidth: 320, maxHeight: 180) - } - - VStack(alignment: .leading, spacing: 0) { - videoDetail(video.title, bold: true) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - - if !detailsOnThumbnail { - videoDetail(video.author, color: .secondary, bold: true) - } - - Spacer() - - additionalDetails - } - .padding() - .frame(minHeight: 180) - - if !detailsOnThumbnail, let time = video.playTime { - Spacer() - - VStack(alignment: .center) { - Spacer() - HStack(spacing: 8) { - Image(systemName: "clock") - Text(time) - .fontWeight(.bold) - } - Spacer() - } - .foregroundColor(.secondary) - } - } - } - - var verticalRow: some View { - VStack(alignment: .leading) { - thumbnailWithDetails(minWidth: 250, maxWidth: 600, minHeight: 180) - .frame(idealWidth: 320) - .padding([.leading, .top, .trailing], 4) - - VStack(alignment: .leading) { - videoDetail(video.title, bold: true) - .padding(.bottom) - - additionalDetails - .padding(.bottom, 10) - } - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 8) - } - } - - var additionalDetails: some View { - VStack { - if !video.published.isEmpty || video.views != 0 { - HStack(spacing: 8) { - if !video.published.isEmpty { - Image(systemName: "calendar") - Text(video.published) - } - - if video.views != 0 { - Image(systemName: "eye") - Text(video.viewsCount) - } - } - #if os(tvOS) - .foregroundColor(.secondary) - #else - .foregroundColor(focused ? .white : .secondary) - #endif - } - } - } - - func thumbnailWithDetails( - minWidth: Double = 250, - maxWidth: Double = .infinity, - minHeight: Double = 140, - maxHeight: Double = .infinity - ) -> some View { - ZStack(alignment: .trailing) { - thumbnail(.maxres, minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) - - VStack(alignment: .trailing) { - detailOnThinMaterial(video.author) - .offset(x: -5, y: 5) - - Spacer() - - if let time = video.playTime { - detailOnThinMaterial(time, bold: true) - .offset(x: -5, y: -5) - } - } - } - } - - func detailOnThinMaterial(_ text: String, bold: Bool = false) -> some View { - Text(text) - .fontWeight(bold ? .semibold : .regular) - .padding(8) - .background(.thinMaterial) - .mask(RoundedRectangle(cornerRadius: 12)) - } - - func thumbnail( - _ quality: Thumbnail.Quality, - minWidth: Double = 320, - maxWidth: Double = .infinity, - minHeight: Double = 180, - maxHeight: Double = .infinity - ) -> some View { - Group { - if let url = video.thumbnailURL(quality: quality) { - AsyncImage(url: url) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) - } placeholder: { - ProgressView() - } - .mask(RoundedRectangle(cornerRadius: 12)) - } else { - Image(systemName: "exclamationmark.square") - } - } - .frame(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) - } - - func videoDetail(_ text: String, color: Color? = .primary, bold: Bool = false) -> some View { - Text(text) - .fontWeight(bold ? .bold : .regular) - #if os(tvOS) - .foregroundColor(color) - .lineLimit(1) - .truncationMode(.middle) - #elseif os(iOS) || os(macOS) - .foregroundColor(focused ? .white : color) - #endif - } -} - -struct VideoListRowPreview: PreviewProvider { - static var previews: some View { - List { - VideoListRowView(video: Video.fixture) - VideoListRowView(video: Video.fixtureUpcomingWithoutPublishedOrViews) - VideoListRowView(video: Video.fixtureLiveWithoutPublishedOrViews) - } - .frame(maxWidth: 400) - - #if os(iOS) - List { - VideoListRowView(video: Video.fixture) - VideoListRowView(video: Video.fixtureUpcomingWithoutPublishedOrViews) - VideoListRowView(video: Video.fixtureLiveWithoutPublishedOrViews) - } - .environment(\.verticalSizeClass, .compact) - .frame(maxWidth: 800) - #endif - } -} diff --git a/Apple TV/VideosCellsView.swift b/Apple TV/VideosCellsView.swift index 37c537e5..e4a6ed3b 100644 --- a/Apple TV/VideosCellsView.swift +++ b/Apple TV/VideosCellsView.swift @@ -15,7 +15,7 @@ struct VideosCellsView: View { ScrollView(.vertical, showsIndicators: false) { LazyVGrid(columns: items, alignment: .center) { ForEach(videos) { video in - VideoCellView(video: video) + VideoView(video: video) .contextMenu { VideoContextMenuView(video: video) } } } diff --git a/Fixtures/Video+Fixtures.swift b/Fixtures/Video+Fixtures.swift index 2512d12f..283f8460 100644 --- a/Fixtures/Video+Fixtures.swift +++ b/Fixtures/Video+Fixtures.swift @@ -37,4 +37,8 @@ extension Video { return video } + + static var allFixtures: [Video] { + [fixture, fixtureLiveWithoutPublishedOrViews, fixtureUpcomingWithoutPublishedOrViews] + } } diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 365ad961..3d10928c 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -72,14 +72,14 @@ 377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; }; 377FC7D5267A080300A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7D4267A080300A6BBAF /* SwiftyJSON */; }; 377FC7DB267A080300A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7DA267A080300A6BBAF /* Logging */; }; - 377FC7DC267A081800A6BBAF /* PopularVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularVideosView.swift */; }; - 377FC7DD267A081A00A6BBAF /* PopularVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularVideosView.swift */; }; + 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; }; + 377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; }; 377FC7DE267A082100A6BBAF /* VideosListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29926740A01007FC770 /* VideosListView.swift */; }; 377FC7DF267A082200A6BBAF /* VideosListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF29926740A01007FC770 /* VideosListView.swift */; }; 377FC7E0267A082600A6BBAF /* ChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2892673AB89007FC770 /* ChannelView.swift */; }; 377FC7E1267A082600A6BBAF /* ChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2892673AB89007FC770 /* ChannelView.swift */; }; - 377FC7E2267A084A00A6BBAF /* VideoListRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoListRowView.swift */; }; - 377FC7E3267A084A00A6BBAF /* VideoListRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoListRowView.swift */; }; + 377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoView.swift */; }; + 377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoView.swift */; }; 377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; }; 377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; }; 377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7EC267A0A0800A6BBAF /* SwiftyJSON */; }; @@ -94,7 +94,7 @@ 379775932689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; }; 379775942689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; }; 379775952689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; }; - 37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularVideosView.swift */; }; + 37AAF27E26737323007FC770 /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; }; 37AAF28026737550007FC770 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; }; 37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF2892673AB89007FC770 /* ChannelView.swift */; }; 37AAF29026740715007FC770 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; }; @@ -166,7 +166,7 @@ 37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C22671614700C925CA /* PearvidiousApp.swift */; }; 37D4B1812671653A00C925CA /* AppTabNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C32671614700C925CA /* AppTabNavigation.swift */; }; 37D4B1862671691600C925CA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37D4B0C42671614800C925CA /* Assets.xcassets */; }; - 37D4B18E26717B3800C925CA /* VideoListRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoListRowView.swift */; }; + 37D4B18E26717B3800C925CA /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B18B26717B3800C925CA /* VideoView.swift */; }; 37D4B19726717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; }; 37D4B19826717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; }; 37D4B19926717E1500C925CA /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B19626717E1500C925CA /* Video.swift */; }; @@ -180,9 +180,6 @@ 37F4AE7226828F0900BD60EA /* VideosCellsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */; }; 37F4AE7326828F0900BD60EA /* VideosCellsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */; }; 37F4AE7426828F0900BD60EA /* VideosCellsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */; }; - 37F4AE762682908700BD60EA /* VideoCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE752682908700BD60EA /* VideoCellView.swift */; }; - 37F4AE772682908700BD60EA /* VideoCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE752682908700BD60EA /* VideoCellView.swift */; }; - 37F4AE782682908700BD60EA /* VideoCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE752682908700BD60EA /* VideoCellView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -235,7 +232,7 @@ 37977582268922F600DD52A8 /* InvidiousAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvidiousAPI.swift; sourceTree = ""; }; 3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = ""; }; - 37AAF27D26737323007FC770 /* PopularVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularVideosView.swift; sourceTree = ""; }; + 37AAF27D26737323007FC770 /* PopularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularView.swift; sourceTree = ""; }; 37AAF27F26737550007FC770 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 37AAF2892673AB89007FC770 /* ChannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelView.swift; sourceTree = ""; }; 37AAF28F26740715007FC770 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = ""; }; @@ -272,13 +269,12 @@ 37D4B15E267164AF00C925CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37D4B171267164B000C925CA /* Tests Apple TV.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests Apple TV.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 37D4B175267164B000C925CA /* PearvidiousUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearvidiousUITests.swift; sourceTree = ""; }; - 37D4B18B26717B3800C925CA /* VideoListRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoListRowView.swift; sourceTree = ""; }; + 37D4B18B26717B3800C925CA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = ""; }; 37D4B19626717E1500C925CA /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = ""; }; 37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = ""; }; 37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = ""; }; 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosCellsView.swift; sourceTree = ""; }; - 37F4AE752682908700BD60EA /* VideoCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCellView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -407,8 +403,16 @@ 37D4B0C22671614700C925CA /* PearvidiousApp.swift */, 37BE0BD226A1D4780092E2DB /* Player.swift */, 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */, + 376578902685490700D4EA09 /* PlaylistsView.swift */, + 37AAF27D26737323007FC770 /* PopularView.swift */, + 37AAF27F26737550007FC770 /* SearchView.swift */, + 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */, 37AAF2932674086B007FC770 /* TabSelection.swift */, + 3714166E267A8ACC006CA35D /* TrendingView.swift */, 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */, + 37AAF29926740A01007FC770 /* VideosListView.swift */, + 371231832683E62F0000B307 /* VideosView.swift */, + 37D4B18B26717B3800C925CA /* VideoView.swift */, 37D4B0C42671614800C925CA /* Assets.xcassets */, 37BD07C42698ADEE003EBB87 /* Pearvidious.entitlements */, ); @@ -453,22 +457,13 @@ 373CFABD26966115003CB2C6 /* CoverSectionView.swift */, 37B76E95268747C900CE5671 /* OptionsView.swift */, 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */, - 376578902685490700D4EA09 /* PlaylistsView.swift */, - 37AAF27D26737323007FC770 /* PopularVideosView.swift */, 373CFAC52696617C003CB2C6 /* SearchOptionsView.swift */, - 37AAF27F26737550007FC770 /* SearchView.swift */, - 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */, 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */, - 3714166E267A8ACC006CA35D /* TrendingView.swift */, 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */, - 37F4AE752682908700BD60EA /* VideoCellView.swift */, 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */, 37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */, - 37D4B18B26717B3800C925CA /* VideoListRowView.swift */, 372F954926A4D0C900502766 /* VideoLoading.swift */, 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */, - 37AAF29926740A01007FC770 /* VideosListView.swift */, - 371231832683E62F0000B307 /* VideosView.swift */, 37D4B15E267164AF00C925CA /* Assets.xcassets */, 37D4B1AE26729DEB00C925CA /* Info.plist */, ); @@ -758,7 +753,6 @@ 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */, - 37F4AE762682908700BD60EA /* VideoCellView.swift in Sources */, 37C7A1DA267CACF50010EAD6 /* TrendingCountrySelectionView.swift in Sources */, 37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */, @@ -767,7 +761,7 @@ 37F4AE7226828F0900BD60EA /* VideosCellsView.swift in Sources */, 376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */, - 377FC7DC267A081800A6BBAF /* PopularVideosView.swift in Sources */, + 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, 373CFAC62696617C003CB2C6 /* SearchOptionsView.swift in Sources */, 371231842683E62F0000B307 /* VideosView.swift in Sources */, 3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */, @@ -778,7 +772,7 @@ 373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */, 3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, - 377FC7E3267A084A00A6BBAF /* VideoListRowView.swift in Sources */, + 377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */, 37AAF29026740715007FC770 /* Channel.swift in Sources */, 3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, 37AAF2942674086B007FC770 /* TabSelection.swift in Sources */, @@ -813,17 +807,16 @@ files = ( 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, - 37F4AE772682908700BD60EA /* VideoCellView.swift in Sources */, 373CFABF26966149003CB2C6 /* CoverSectionView.swift in Sources */, 37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, 371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */, - 377FC7DD267A081A00A6BBAF /* PopularVideosView.swift in Sources */, + 377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */, 3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */, 37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */, 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */, 37141670267A8ACC006CA35D /* TrendingView.swift in Sources */, - 377FC7E2267A084A00A6BBAF /* VideoListRowView.swift in Sources */, + 377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */, 3765788A2685471400D4EA09 /* Playlist.swift in Sources */, 373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */, 373CFAC32696616C003CB2C6 /* CoverSectionRowView.swift in Sources */, @@ -886,7 +879,6 @@ 37AAF28026737550007FC770 /* SearchView.swift in Sources */, 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */, 37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */, - 37F4AE782682908700BD60EA /* VideoCellView.swift in Sources */, 37BE0BD426A1D47D0092E2DB /* Player.swift in Sources */, 37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37F4AE7426828F0900BD60EA /* VideosCellsView.swift in Sources */, @@ -907,9 +899,9 @@ 373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */, 37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */, 373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, - 37D4B18E26717B3800C925CA /* VideoListRowView.swift in Sources */, + 37D4B18E26717B3800C925CA /* VideoView.swift in Sources */, 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, - 37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */, + 37AAF27E26737323007FC770 /* PopularView.swift in Sources */, 37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */, 37AAF2962674086B007FC770 /* TabSelection.swift in Sources */, 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, diff --git a/Pearvidious.xcodeproj/xcuserdata/arek.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Pearvidious.xcodeproj/xcuserdata/arek.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 1cf6f03c..15e94d43 100644 --- a/Pearvidious.xcodeproj/xcuserdata/arek.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Pearvidious.xcodeproj/xcuserdata/arek.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -3,38 +3,4 @@ uuid = "E30DA302-B258-4C14-8808-5E4CE238A4FF" type = "1" version = "2.0"> - - - - - - - - - - diff --git a/Shared/AppSidebarNavigation.swift b/Shared/AppSidebarNavigation.swift index 33caa0f1..69ec7847 100644 --- a/Shared/AppSidebarNavigation.swift +++ b/Shared/AppSidebarNavigation.swift @@ -44,15 +44,54 @@ struct AppSidebarNavigation: View { SubscriptionsView() } label: { - Label("Subscriptions", systemImage: "star") + Label("Subscriptions", systemImage: "play.rectangle.fill") + .accessibility(label: Text("Subscriptions")) } NavigationLink(tag: TabSelection.popular, selection: navigationState.tabSelectionOptionalBinding) { - PopularVideosView() + PopularView() } label: { Label("Popular", systemImage: "chart.bar") + .accessibility(label: Text("Popular")) + } + + NavigationLink(tag: TabSelection.trending, selection: navigationState.tabSelectionOptionalBinding) { + TrendingView() + } + label: { + Label("Trending", systemImage: "chart.line.uptrend.xyaxis") + .accessibility(label: Text("Trending")) + } + + NavigationLink(tag: TabSelection.playlists, selection: navigationState.tabSelectionOptionalBinding) { + PlaylistsView() + } + label: { + Label("Playlists", systemImage: "list.and.film") + .accessibility(label: Text("Playlists")) + } + + NavigationLink(tag: TabSelection.search, selection: navigationState.tabSelectionOptionalBinding) { + SearchView() + } + label: { + Label("Search", systemImage: "magnifyingglass") + .accessibility(label: Text("Search")) } } + #if os(macOS) + .toolbar { + Button(action: toggleSidebar) { + Image(systemName: "sidebar.left").help("Toggle Sidebar") + } + } + #endif } + + #if os(macOS) + private func toggleSidebar() { + NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) + } + #endif } diff --git a/Shared/AppTabNavigation.swift b/Shared/AppTabNavigation.swift index e2f65d70..d16b9095 100644 --- a/Shared/AppTabNavigation.swift +++ b/Shared/AppTabNavigation.swift @@ -16,7 +16,7 @@ struct AppTabNavigation: View { .tag(TabSelection.subscriptions) NavigationView { - PopularVideosView() + PopularView() } .tabItem { Label("Popular", systemImage: "chart.bar") diff --git a/Shared/PearvidiousApp.swift b/Shared/PearvidiousApp.swift index 303e0967..598b4b27 100644 --- a/Shared/PearvidiousApp.swift +++ b/Shared/PearvidiousApp.swift @@ -6,5 +6,10 @@ struct PearvidiousApp: App { WindowGroup { ContentView() } + #if !os(tvOS) + .commands { + SidebarCommands() + } + #endif } } diff --git a/Apple TV/PlaylistsView.swift b/Shared/PlaylistsView.swift similarity index 100% rename from Apple TV/PlaylistsView.swift rename to Shared/PlaylistsView.swift diff --git a/Apple TV/PopularVideosView.swift b/Shared/PopularView.swift similarity index 92% rename from Apple TV/PopularVideosView.swift rename to Shared/PopularView.swift index 47daa666..0fd1739d 100644 --- a/Apple TV/PopularVideosView.swift +++ b/Shared/PopularView.swift @@ -1,7 +1,7 @@ import Siesta import SwiftUI -struct PopularVideosView: View { +struct PopularView: View { @ObservedObject private var store = Store<[Video]>() var resource = InvidiousAPI.shared.popular diff --git a/Apple TV/SearchView.swift b/Shared/SearchView.swift similarity index 100% rename from Apple TV/SearchView.swift rename to Shared/SearchView.swift diff --git a/Apple TV/SubscriptionsView.swift b/Shared/SubscriptionsView.swift similarity index 100% rename from Apple TV/SubscriptionsView.swift rename to Shared/SubscriptionsView.swift diff --git a/Apple TV/TrendingView.swift b/Shared/TrendingView.swift similarity index 100% rename from Apple TV/TrendingView.swift rename to Shared/TrendingView.swift diff --git a/Shared/VideoView.swift b/Shared/VideoView.swift index 25a85531..97b6e5ed 100644 --- a/Shared/VideoView.swift +++ b/Shared/VideoView.swift @@ -1,20 +1,301 @@ -// -// VideoView.swift -// VideoView -// -// Created by Arkadiusz Fal on 26/07/2021. -// - +import Defaults import SwiftUI struct VideoView: View { + @EnvironmentObject private var navigationState + + @Environment(\.isFocused) private var focused: Bool + + #if os(iOS) + @Environment(\.verticalSizeClass) private var verticalSizeClass + #endif + + var layout: ListingLayout? + + var video: Video + + init(video: Video, layout: ListingLayout? = nil) { + self.video = video + self.layout = layout + + #if os(tvOS) + if self.layout == nil { + self.layout = Defaults[.layout] + } + #endif + } + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + #if os(tvOS) + if layout == .cells { + tvOSButton + .buttonStyle(.plain) + .padding(.vertical) + } else { + tvOSButton + } + #elseif os(macOS) + NavigationLink(destination: VideoPlayerView(video)) { + verticalRow + } + #else + ZStack { + #if os(macOS) + verticalRow + #else + if verticalSizeClass == .compact { + horizontalRow(padding: 4) + } else { + verticalRow + } + #endif + + NavigationLink(destination: VideoPlayerView(video)) { + EmptyView() + } + .buttonStyle(PlainButtonStyle()) + .opacity(0) + .frame(height: 0) + } + #endif + } + + #if os(tvOS) + var tvOSButton: some View { + Button(action: { navigationState.playVideo(video) }) { + if layout == .cells { + cellRow + } else { + horizontalRow(detailsOnThumbnail: false) + } + } + } + #endif + + func horizontalRow(detailsOnThumbnail: Bool = true, padding: Double = 0) -> some View { + HStack(alignment: .top, spacing: 2) { + if detailsOnThumbnail { + thumbnailWithDetails() + .padding(padding) + } else { + thumbnail(.medium, maxWidth: 320, maxHeight: 180) + } + + VStack(alignment: .leading, spacing: 0) { + videoDetail(video.title, bold: true) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + if !detailsOnThumbnail { + videoDetail(video.author, color: .secondary, bold: true) + } + + Spacer() + + additionalDetails + } + .padding() + .frame(minHeight: 180) + + if !detailsOnThumbnail { + if video.playTime != nil || video.live || video.upcoming { + Spacer() + + VStack(alignment: .center) { + Spacer() + + if let time = video.playTime { + HStack(spacing: 4) { + Image(systemName: "clock") + Text(time) + .fontWeight(.bold) + } + .foregroundColor(.secondary) + } else if video.live { + DetailBadge(text: "Live", style: .outstanding) + } else if video.upcoming { + DetailBadge(text: "Upcoming", style: .informational) + } + + Spacer() + } + } + } + } + } + + var verticalRow: some View { + VStack(alignment: .leading) { + thumbnailWithDetails(minWidth: 250, maxWidth: 600, minHeight: 180) + .frame(idealWidth: 320) + .padding([.leading, .top, .trailing], 4) + + VStack(alignment: .leading) { + videoDetail(video.title, bold: true) + .padding(.bottom) + + additionalDetails + .padding(.bottom, 10) + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 8) + } + } + + var cellRow: some View { + VStack(alignment: .leading) { + thumbnailWithDetails(minWidth: 550, maxWidth: 550, minHeight: 310, maxHeight: 310) + .padding([.leading, .top, .trailing], 4) + + VStack(alignment: .leading) { + videoDetail(video.title, bold: true, lineLimit: additionalDetailsAvailable ? 2 : 3) + .frame(minHeight: 80, alignment: .top) + .padding(.bottom) + + if additionalDetailsAvailable { + additionalDetails + .padding(.bottom, 10) + } else { + Spacer() + } + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 150, alignment: .leading) + .padding(10) + } + .frame(width: 558) + } + + var additionalDetailsAvailable: Bool { + video.publishedDate != nil || video.views != 0 + } + + var additionalDetails: some View { + HStack(spacing: 8) { + if let date = video.publishedDate { + Image(systemName: "calendar") + Text(date) + } + + if video.views != 0 { + Image(systemName: "eye") + Text(video.viewsCount) + } + } + #if os(tvOS) + .foregroundColor(.secondary) + #else + .foregroundColor(focused ? .white : .secondary) + #endif + } + + func thumbnailWithDetails( + minWidth: Double = 250, + maxWidth: Double = .infinity, + minHeight: Double = 140, + maxHeight: Double = .infinity + ) -> some View { + ZStack(alignment: .trailing) { + thumbnail(.maxres, minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) + + VStack { + HStack(alignment: .top) { + if video.live { + DetailBadge(text: "Live", style: .outstanding) + } else if video.upcoming { + DetailBadge(text: "Upcoming", style: .informational) + } + + Spacer() + + DetailBadge(text: video.author, style: .prominent) + } + .padding(10) + + Spacer() + + HStack(alignment: .top) { + Spacer() + + if let time = video.playTime { + DetailBadge(text: time, style: .prominent) + } + } + .padding(10) + } + } + } + + func thumbnail( + _ quality: Thumbnail.Quality, + minWidth: Double = 320, + maxWidth: Double = .infinity, + minHeight: Double = 180, + maxHeight: Double = .infinity + ) -> some View { + Group { + if let url = video.thumbnailURL(quality: quality) { + AsyncImage(url: url) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) + } placeholder: { + ProgressView() + } + .mask(RoundedRectangle(cornerRadius: 12)) + } else { + Image(systemName: "exclamationmark.square") + } + } + .frame(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) + } + + func videoDetail(_ text: String, color: Color? = .primary, bold: Bool = false, lineLimit: Int = 1) -> some View { + Text(text) + .fontWeight(bold ? .bold : .regular) + #if os(tvOS) + .foregroundColor(color) + .lineLimit(lineLimit) + .truncationMode(.middle) + #elseif os(iOS) || os(macOS) + .foregroundColor(focused ? .white : color) + #endif } } -struct VideoView_Previews: PreviewProvider { +struct VideoListRowPreview: PreviewProvider { static var previews: some View { - VideoView() + #if os(tvOS) + List { + ForEach(Video.allFixtures) { video in + VideoView(video: video, layout: .list) + } + } + .listStyle(GroupedListStyle()) + + HStack { + ForEach(Video.allFixtures) { video in + VideoView(video: video, layout: .cells) + } + } + .frame(maxHeight: 600) + #else + List { + ForEach(Video.allFixtures) { video in + VideoView(video: video, layout: .list) + } + } + #if os(macOS) + .frame(minHeight: 800) + #endif + + #if os(iOS) + List { + ForEach(Video.allFixtures) { video in + VideoView(video: video, layout: .list) + } + } + .previewInterfaceOrientation(.landscapeRight) + #endif + #endif } } diff --git a/Apple TV/VideosListView.swift b/Shared/VideosListView.swift similarity index 79% rename from Apple TV/VideosListView.swift rename to Shared/VideosListView.swift index 6cb2e5d8..f276bb38 100644 --- a/Apple TV/VideosListView.swift +++ b/Shared/VideosListView.swift @@ -8,7 +8,7 @@ struct VideosListView: View { Section { List { ForEach(videos) { video in - VideoListRowView(video: video) + VideoView(video: video, layout: .list) .contextMenu { VideoContextMenuView(video: video) } #if os(tvOS) .listRowInsets(listRowInsets) @@ -29,3 +29,9 @@ struct VideosListView: View { EdgeInsets(top: .zero, leading: .zero, bottom: .zero, trailing: 30) } } + +struct VideosListView_Previews: PreviewProvider { + static var previews: some View { + VideosListView(videos: Video.allFixtures) + } +} diff --git a/Apple TV/VideosView.swift b/Shared/VideosView.swift similarity index 100% rename from Apple TV/VideosView.swift rename to Shared/VideosView.swift