Simplify video views

This commit is contained in:
Arkadiusz Fal 2021-07-28 00:40:04 +02:00
parent 52ffe19324
commit 5b0a3458f3
18 changed files with 374 additions and 404 deletions

View File

@ -13,7 +13,7 @@ struct TVNavigationView: View {
.tabItem { Text("Subscriptions") }
.tag(TabSelection.subscriptions)
PopularVideosView()
PopularView()
.tabItem { Text("Popular") }
.tag(TabSelection.popular)

View File

@ -1,108 +0,0 @@
import SwiftUI
struct VideoCellView: View {
@EnvironmentObject<NavigationState> 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)
}
}
}

View File

@ -1,215 +0,0 @@
import SwiftUI
struct VideoListRowView: View {
@EnvironmentObject<NavigationState> 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
}
}

View File

@ -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) }
}
}

View File

@ -37,4 +37,8 @@ extension Video {
return video
}
static var allFixtures: [Video] {
[fixture, fixtureLiveWithoutPublishedOrViews, fixtureUpcomingWithoutPublishedOrViews]
}
}

View File

@ -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 = "<group>"; };
3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = "<group>"; };
37AAF27D26737323007FC770 /* PopularVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularVideosView.swift; sourceTree = "<group>"; };
37AAF27D26737323007FC770 /* PopularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularView.swift; sourceTree = "<group>"; };
37AAF27F26737550007FC770 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
37AAF2892673AB89007FC770 /* ChannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelView.swift; sourceTree = "<group>"; };
37AAF28F26740715007FC770 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
@ -272,13 +269,12 @@
37D4B15E267164AF00C925CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
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 = "<group>"; };
37D4B18B26717B3800C925CA /* VideoListRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoListRowView.swift; sourceTree = "<group>"; };
37D4B18B26717B3800C925CA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = "<group>"; };
37D4B19626717E1500C925CA /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; };
37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
37F4AE7126828F0900BD60EA /* VideosCellsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosCellsView.swift; sourceTree = "<group>"; };
37F4AE752682908700BD60EA /* VideoCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCellView.swift; sourceTree = "<group>"; };
/* 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 */,

View File

@ -3,38 +3,4 @@
uuid = "E30DA302-B258-4C14-8808-5E4CE238A4FF"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "FD786D10-33CD-43A8-8D52-4F647010EC88"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Shared/DetailBadge.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "75"
endingLineNumber = "75"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "15B71F2E-CE6A-4ECA-9390-ED336CB3691D"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Model/PlayerState.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "105"
endingLineNumber = "105"
landmarkName = "playNewStream(_:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -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
}

View File

@ -16,7 +16,7 @@ struct AppTabNavigation: View {
.tag(TabSelection.subscriptions)
NavigationView {
PopularVideosView()
PopularView()
}
.tabItem {
Label("Popular", systemImage: "chart.bar")

View File

@ -6,5 +6,10 @@ struct PearvidiousApp: App {
WindowGroup {
ContentView()
}
#if !os(tvOS)
.commands {
SidebarCommands()
}
#endif
}
}

View File

@ -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

View File

@ -1,20 +1,301 @@
//
// VideoView.swift
// VideoView
//
// Created by Arkadiusz Fal on 26/07/2021.
//
import Defaults
import SwiftUI
struct VideoView: View {
@EnvironmentObject<NavigationState> 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()
}
}
}
}
}
struct VideoView_Previews: PreviewProvider {
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 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
}
}

View File

@ -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)
}
}