diff --git a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift index d638e022..31b2f7ee 100644 --- a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift +++ b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift @@ -40,10 +40,11 @@ struct LandscapeItemElement: View { @State var backgroundURL: URL? var item: BaseItemDto + var inSeasonView: Bool? var body: some View { VStack { - ImageView(src: (item.type == "Episode" ? item.getSeriesBackdropImage(maxWidth: 445) : item.getBackdropImage(maxWidth: 445)), bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash()) + ImageView(src: (item.type == "Episode" && !(inSeasonView ?? false) ? item.getSeriesBackdropImage(maxWidth: 445) : item.getBackdropImage(maxWidth: 445)), bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash()) .frame(width: 445, height: 250) .cornerRadius(10) .overlay( @@ -80,11 +81,19 @@ struct LandscapeItemElement: View { .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) if focused { - Text(item.type == "Episode" ? "\(item.seriesName ?? "") • S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))" : item.name ?? "") - .font(.callout) - .fontWeight(.semibold) - .lineLimit(1) - .frame(width: 445) + if(inSeasonView ?? false) { + Text("\(item.getEpisodeLocator()) • \(item.name ?? "")") + .font(.callout) + .fontWeight(.semibold) + .lineLimit(1) + .frame(width: 445) + } else { + Text(item.type == "Episode" ? "\(item.seriesName ?? "") • \(item.getEpisodeLocator())" : item.name ?? "") + .font(.callout) + .fontWeight(.semibold) + .lineLimit(1) + .frame(width: 445) + } } else { Spacer().frame(height: 25) } diff --git a/JellyfinPlayer tvOS/Components/MediaViewActionButton.swift b/JellyfinPlayer tvOS/Components/MediaViewActionButton.swift index ff1573ad..3dac0d68 100644 --- a/JellyfinPlayer tvOS/Components/MediaViewActionButton.swift +++ b/JellyfinPlayer tvOS/Components/MediaViewActionButton.swift @@ -13,7 +13,7 @@ struct MediaViewActionButton: View { @Environment(\.isFocused) var envFocused: Bool @State var focused: Bool = false var icon: String - @Binding var scrollView: UIScrollView? + var scrollView: Binding? var iconColor: Color? var body: some View { @@ -21,9 +21,9 @@ struct MediaViewActionButton: View { .foregroundColor(focused ? .black : iconColor ?? .white) .onChange(of: envFocused) { envFocus in if(envFocus == true) { - scrollView?.scrollToTop() + scrollView?.wrappedValue?.scrollToTop() DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - scrollView?.scrollToTop() + scrollView?.wrappedValue?.scrollToTop() } } diff --git a/JellyfinPlayer tvOS/EpisodeItemView.swift b/JellyfinPlayer tvOS/EpisodeItemView.swift new file mode 100644 index 00000000..47224bd3 --- /dev/null +++ b/JellyfinPlayer tvOS/EpisodeItemView.swift @@ -0,0 +1,180 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import JellyfinAPI + +struct EpisodeItemView: View { + @ObservedObject var viewModel: EpisodeItemViewModel + + @State var actors: [BaseItemPerson] = []; + @State var studio: String? = nil; + @State var director: String? = nil; + + func onAppear() { + actors = [] + director = nil + studio = nil + var actor_index = 0; + viewModel.item.people?.forEach { person in + if(person.type == "Actor") { + if(actor_index < 4) { + actors.append(person) + } + actor_index = actor_index + 1; + } + if(person.type == "Director") { + director = person.name ?? "" + } + } + + studio = viewModel.item.studios?.first?.name ?? nil + } + + var body: some View { + ZStack { + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash()) + .opacity(0.4) + LazyVStack(alignment: .leading) { + Text(viewModel.item.name ?? "") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + Text(viewModel.item.seriesName ?? "") + .fontWeight(.bold) + .foregroundColor(.primary) + HStack { + if viewModel.item.productionYear != nil { + Text(String(viewModel.item.productionYear!)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + Text(viewModel.item.getItemRuntime()).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if viewModel.item.officialRating != nil { + Text(viewModel.item.officialRating!).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + }.padding(.top, 15) + + HStack { + VStack(alignment: .trailing) { + if(studio != nil) { + Text("STUDIO") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text(studio!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .padding(.bottom, 40) + } + + if(director != nil) { + Text("DIRECTOR") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text(director!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .padding(.bottom, 40) + } + + if(!actors.isEmpty) { + Text("CAST") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + ForEach(actors, id: \.id) { person in + Text(person.name!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + } + } + Spacer() + } + VStack(alignment: .leading) { + if(!(viewModel.item.taglines ?? []).isEmpty) { + Text(viewModel.item.taglines?.first ?? "") + .font(.body) + .italic() + .fontWeight(.medium) + .foregroundColor(.primary) + } + Text(viewModel.item.overview ?? "") + .font(.body) + .fontWeight(.medium) + .foregroundColor(.primary) + + HStack { + VStack { + Button { + viewModel.updateFavoriteState() + } label: { + MediaViewActionButton(icon: "heart.fill", iconColor: viewModel.isFavorited ? .red : .white) + } + Text(viewModel.isFavorited ? "Unfavorite" : "Favorite") + .font(.caption) + } + VStack { + NavigationLink(destination: VideoPlayerView(item: viewModel.item)) { + MediaViewActionButton(icon: "play.fill") + } + Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : "Play") + .font(.caption) + } + VStack { + Button { + viewModel.updateWatchState() + } label: { + MediaViewActionButton(icon: "eye.fill", iconColor: viewModel.isWatched ? .red : .white) + } + Text(viewModel.isWatched ? "Unwatch" : "Mark Watched") + .font(.caption) + } + Spacer() + } + .padding(.top, 15) + } + }.padding(.top, 50) + + if(!viewModel.similarItems.isEmpty) { + Text("More Like This") + .font(.headline) + .fontWeight(.semibold) + ScrollView(.horizontal) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(viewModel.similarItems, id: \.id) { similarItems in + NavigationLink(destination: ItemView(item: similarItems)) { + PortraitItemElement(item: similarItems) + }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90)) + .frame(height: 360) + } + Spacer() + }.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90)) + }.onAppear(perform: onAppear) + } +} diff --git a/JellyfinPlayer tvOS/ItemView.swift b/JellyfinPlayer tvOS/ItemView.swift index 68ead546..62692415 100644 --- a/JellyfinPlayer tvOS/ItemView.swift +++ b/JellyfinPlayer tvOS/ItemView.swift @@ -9,19 +9,9 @@ import SwiftUI import Introspect import JellyfinAPI -class VideoPlayerItem: ObservableObject { - @Published var shouldShowPlayer: Bool = false - @Published var itemToPlay: BaseItemDto = BaseItemDto() -} - struct ItemView: View { private var item: BaseItemDto - @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() - @State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view. - @State private var isLoading: Bool = false - @State private var viewDidLoad: Bool = false - init(item: BaseItemDto) { self.item = item } @@ -32,6 +22,10 @@ struct ItemView: View { MovieItemView(viewModel: .init(item: item)) } else if item.type == "Series" { SeriesItemView(viewModel: .init(item: item)) + } else if item.type == "Season" { + SeasonItemView(viewModel: .init(item: item)) + } else if item.type == "Episode" { + EpisodeItemView(viewModel: .init(item: item)) } else { Text("Type: \(item.type ?? "") not implemented yet :(") } diff --git a/JellyfinPlayer tvOS/LibraryView.swift b/JellyfinPlayer tvOS/LibraryView.swift index cf6a0f0a..bcc3188e 100644 --- a/JellyfinPlayer tvOS/LibraryView.swift +++ b/JellyfinPlayer tvOS/LibraryView.swift @@ -26,7 +26,6 @@ struct LibraryView: View { ProgressView() } else if !viewModel.items.isEmpty { ScrollView(.vertical) { - Spacer().frame(height: 16) LazyVGrid(columns: tracks) { ForEach(viewModel.items, id: \.id) { item in if(item.type != "Folder") { @@ -41,32 +40,13 @@ struct LibraryView: View { } } } - } - Spacer().frame(height: 16) + }.padding() } } else { Text("No results.") } } - .navigationBarTitle(title) - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - if viewModel.hasPreviousPage { - Button { - viewModel.requestPreviousPage() - } label: { - Image(systemName: "chevron.left") - }.disabled(viewModel.isLoading) - } - if viewModel.hasNextPage { - Button { - viewModel.requestNextPage() - } label: { - Image(systemName: "chevron.right") - }.disabled(viewModel.isLoading) - } - } - }/* + /* .sheet(isPresented: $isShowingFilterView) { LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "") } diff --git a/JellyfinPlayer tvOS/MovieItemView.swift b/JellyfinPlayer tvOS/MovieItemView.swift index 5cd3567c..6251df29 100644 --- a/JellyfinPlayer tvOS/MovieItemView.swift +++ b/JellyfinPlayer tvOS/MovieItemView.swift @@ -158,7 +158,7 @@ struct MovieItemView: View { Spacer() } .padding(.top, 15) - .addFocusGuide(using: focusBag, name: "actionButtons", destinations: [.bottom: "moreLikeThis"], debug: true) + .addFocusGuide(using: focusBag, name: "actionButtons", destinations: [.bottom: "moreLikeThis"], debug: false) } }.padding(.top, 50) diff --git a/JellyfinPlayer tvOS/SeasonItemView.swift b/JellyfinPlayer tvOS/SeasonItemView.swift new file mode 100644 index 00000000..5d636045 --- /dev/null +++ b/JellyfinPlayer tvOS/SeasonItemView.swift @@ -0,0 +1,119 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import JellyfinAPI +import SwiftUIFocusGuide + +struct SeasonItemView: View { + @ObservedObject var viewModel: SeasonItemViewModel + @State var wrappedScrollView: UIScrollView?; + + @StateObject var focusBag = SwiftUIFocusBag() + + @Environment(\.resetFocus) var resetFocus + @Namespace private var namespace + + var body: some View { + ZStack { + ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 1920), bh: viewModel.item.getSeriesBackdropImageBlurHash()) + .opacity(0.4) + ScrollView { + LazyVStack(alignment: .leading) { + Text("\(viewModel.item.seriesName ?? "") • \(viewModel.item.name ?? "")") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + HStack { + if(viewModel.item.productionYear != nil) { + Text(String(viewModel.item.productionYear!)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + if viewModel.item.officialRating != nil { + Text(viewModel.item.officialRating!).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + if viewModel.item.communityRating != nil { + HStack { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + .font(.subheadline) + Text(String(viewModel.item.communityRating!)).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + } + + VStack(alignment: .leading) { + if(!(viewModel.item.taglines ?? []).isEmpty) { + Text(viewModel.item.taglines?.first ?? "") + .font(.body) + .italic() + .fontWeight(.medium) + .foregroundColor(.primary) + } + Text(viewModel.item.overview ?? "") + .font(.body) + .fontWeight(.medium) + .foregroundColor(.primary) + + HStack { + VStack { + Button { + viewModel.updateFavoriteState() + } label: { + MediaViewActionButton(icon: "heart.fill", scrollView: $wrappedScrollView, iconColor: viewModel.isFavorited ? .red : .white) + }.prefersDefaultFocus(in: namespace) + Text(viewModel.isFavorited ? "Unfavorite" : "Favorite") + .font(.caption) + } + VStack { + Button { + viewModel.updateWatchState() + } label: { + MediaViewActionButton(icon: "eye.fill", scrollView: $wrappedScrollView, iconColor: viewModel.isWatched ? .red : .white) + } + Text(viewModel.isWatched ? "Unwatch" : "Mark Watched") + .font(.caption) + } + }.padding(.top, 15) + Spacer() + }.padding(.top, 50) + + if(!viewModel.episodes.isEmpty) { + Text("Episodes") + .font(.headline) + .fontWeight(.semibold) + ScrollView(.horizontal) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(viewModel.episodes, id: \.id) { episode in + NavigationLink(destination: ItemView(item: episode)) { + LandscapeItemElement(item: episode, inSeasonView: true) + }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90)) + .frame(height: 360) + } + }.padding(EdgeInsets(top: 90, leading: 90, bottom: 45, trailing: 90)) + } + } + } +} diff --git a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift index f799be1a..b8d4388f 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift @@ -19,21 +19,6 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate var infoContainerPos: CGRect? var tabBarHeight: CGFloat = 0 -// override func viewWillAppear(_ animated: Bool) { -// tabBar.standardAppearance.backgroundColor = .clear -// tabBar.standardAppearance.backgroundImage = UIImage() -// tabBar.standardAppearance.backgroundEffect = .none -// tabBar.barTintColor = .clear -// for view in tabBar.subviews { -// print(view.description) -//// if view.description.contains("_UIBarBackground") { -//// -//// view.removeFromSuperview() -//// } -// } -// -// } -// override func viewDidLoad() { super.viewDidLoad() mediaInfoController = MediaInfoViewController() diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift index 41ba5325..eaa76494 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift @@ -52,25 +52,27 @@ struct MediaInfoView: View { if item.type == "Episode" { Text(item.seriesName ?? "Series") .fontWeight(.bold) + + HStack { + Text(item.name ?? "Episode") + .foregroundColor(.secondary) + + Text(item.getEpisodeLocator()) - Text(item.name ?? "Episode") - .foregroundColor(.secondary) + if let date = item.premiereDate { + Text(formatDate(date: date)) + } + } } else { Text(item.name ?? "Movie") .fontWeight(.bold) } HStack(spacing: 10) { - if item.type == "Episode" { - Text("S\(item.parentIndexNumber ?? 0) • E\(item.indexNumber ?? 0)") - - if let date = item.premiereDate { - Text("•") - Text(formatDate(date: date)) + if(item.type != "Episode") { + if let year = item.productionYear { + Text(String(year)) } - - } else if let year = item.productionYear { - Text(String(year)) } if item.runTimeTicks != nil { @@ -113,7 +115,7 @@ struct MediaInfoView: View { func formatDate(date: Date) -> String { let formatter = DateFormatter() - formatter.dateFormat = "d MMM yyyy" + formatter.dateFormat = "MMM d, yyyy" return formatter.string(from: date) } diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index ccb50005..093ceb8b 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -488,8 +488,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, let translation = panGestureRecognizer.translation(in: view) let velocity = panGestureRecognizer.velocity(in: view) - print(translation) - // Swiped up - Handle dismissing info panel if translation.y < -200 && (focusedOnTabBar && showingInfoPanel) { toggleInfoContainer() diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index dac2d585..e7f0fee1 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; }; 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */; }; 53272535268BF9710035FBF1 /* SwiftUIFocusGuide in Frameworks */ = {isa = PBXBuildFile; productRef = 53272534268BF9710035FBF1 /* SwiftUIFocusGuide */; }; + 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */; }; + 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272538268C20100035FBF1 /* EpisodeItemView.swift */; }; 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */; }; 53313B90265EEA6D00947AA3 /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */; }; 53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; }; @@ -235,6 +237,8 @@ 531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaViewActionButton.swift; sourceTree = ""; }; + 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemView.swift; sourceTree = ""; }; + 53272538268C20100035FBF1 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCastDeviceSelector.swift; sourceTree = ""; }; 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = ""; }; 5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; @@ -467,6 +471,8 @@ 53CD2A3F268A49C2002ABD4E /* ItemView.swift */, 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */, 53116A16268B919A003024C9 /* SeriesItemView.swift */, + 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */, + 53272538268C20100035FBF1 /* EpisodeItemView.swift */, ); path = "JellyfinPlayer tvOS"; sourceTree = ""; @@ -978,6 +984,7 @@ 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, + 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */, 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */, @@ -1006,6 +1013,7 @@ 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, 531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */, + 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */,