From a9f09edd812475d1bf73057b8bba352702eb6e8c Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Fri, 5 Aug 2022 10:54:40 -0600 Subject: [PATCH] Navigation and Item Overhaul (#492) --- .swiftformat | 2 +- Shared/BlurHashKit/BlurHash.swift | 63 + Shared/BlurHashKit/ColourProbes.swift | 102 ++ Shared/BlurHashKit/ColourSpace.swift | 25 + Shared/BlurHashKit/EscapeSequences.swift | 43 + Shared/BlurHashKit/FromString.swift | 76 + Shared/BlurHashKit/FromUIImage.swift | 92 ++ Shared/BlurHashKit/Generation.swift | 115 ++ Shared/BlurHashKit/StringCoding.swift | 48 + Shared/BlurHashKit/ToString.swift | 56 + Shared/BlurHashKit/ToUIImage.swift | 101 ++ Shared/BlurHashKit/TupleMaths.swift | 61 + .../BasicAppSettingsCoordinator.swift | 2 +- Shared/Coordinators/HomeCoordinator.swift | 22 +- Shared/Coordinators/ItemCoordinator.swift | 6 +- Shared/Coordinators/LibraryCoordinator.swift | 12 +- Shared/Coordinators/SettingsCoordinator.swift | 2 +- Shared/Extensions/BlurHashDecode.swift | 165 -- Shared/Extensions/ColorExtension.swift | 4 +- Shared/Extensions/Defaults+Workaround.swift | 0 Shared/Extensions/FontExtensions.swift | 42 + .../BaseItemDto+Images.swift | 91 ++ .../BaseItemDto+Poster.swift | 74 + .../BaseItemDto+Stackable.swift | 65 - .../BaseItemDto+VideoPlayerViewModel.swift | 22 +- .../BaseItemDtoExtensions.swift | 289 +--- .../BaseItemPerson+Poster.swift | 46 + .../BaseItemPersonExtensions.swift | 78 +- .../RequestBuilderExtensions.swift} | 8 +- Shared/Extensions/StringExtensions.swift | 5 + Shared/Extensions/UIDeviceExtensions.swift | 28 + .../Extensions/UIScreenExtensions.swift | 15 +- .../Extensions/UIScrollViewExtensions.swift | 16 + .../BackgroundParallaxHeaderModifier.swift | 40 + .../BottomEdgeGradientModifier.swift | 35 + .../ScrollViewOffsetModifier.swift | 39 + .../ViewExtensions/ViewExtensions.swift | 77 + Shared/Generated/LocalizedLookup.swift | 28 - Shared/Generated/Strings.swift | 466 ------ Shared/Objects/ItemViewType.swift | 27 + Shared/Objects/OverlaySliderColor.swift | 24 - .../PanDirectionGestureRecognizer.swift | 0 Shared/Objects/PortraitImageStackable.swift | 19 - Shared/Objects/Poster.swift | 32 + Shared/Objects/PosterSize.swift | 14 - Shared/Singleton/BackgroundManager.swift | 35 - Shared/Strings/Strings.swift | 488 ++++++ .../SwiftfinStore/SwiftfinStoreDefaults.swift | 4 +- Shared/ViewModels/EpisodesRowManager.swift | 41 +- Shared/ViewModels/HomeViewModel.swift | 8 +- .../CollectionItemViewModel.swift | 2 +- .../ItemViewModel/EpisodeItemViewModel.swift | 59 +- .../ItemViewModel/ItemViewModel.swift | 102 +- .../ItemViewModel/SeasonItemViewModel.swift | 73 +- .../ItemViewModel/SeriesItemViewModel.swift | 98 +- Shared/ViewModels/LatestMediaViewModel.swift | 9 +- Shared/ViewModels/LibraryListViewModel.swift | 13 +- Shared/ViewModels/LibraryViewModel.swift | 10 +- Shared/ViewModels/MainTabViewModel.swift | 33 - .../ViewModels/MovieLibrariesViewModel.swift | 2 +- Shared/ViewModels/TVLibrariesViewModel.swift | 2 +- Shared/ViewModels/UserSignInViewModel.swift | 2 +- .../VideoPlayerViewModel.swift | 5 +- .../Components => Shared/Views}/AppIcon.swift | 0 Shared/Views/AttributeFillView.swift | 32 + Shared/Views/AttributeOutlineView.swift | 26 + Shared/Views/BlurHashView.swift | 18 +- Shared/Views/BlurView.swift | 29 + .../Views/{LazyView.swift => Divider.swift} | 9 +- Shared/Views/ImageView.swift | 69 +- Shared/Views/ParallaxHeader.swift | 50 - Shared/Views/TruncatedTextView.swift | 146 ++ Swiftfin tvOS/Components/DotHStack.swift | 234 +++ .../EpisodesRowView/EpisodesRowCard.swift | 65 - .../EpisodesRowView/EpisodesRowView.swift | 108 -- .../CinematicBackgroundView.swift | 6 +- .../CinematicNextUpCardView.swift | 16 +- .../CinematicResumeCardView.swift | 18 +- .../HomeCinematicView/HomeCinematicView.swift | 6 +- .../Components/ItemDetailsView.swift | 8 +- .../Components/LandscapeItemElement.swift | 110 +- .../Components/MediaPlayButtonRowView.swift | 56 - .../Components/MediaViewActionButton.swift | 38 - .../Components/PlainLinkButton.swift | 31 - Swiftfin tvOS/Components/PortraitButton.swift | 58 + .../Components/PortraitItemElement.swift | 85 +- .../Components/PortraitItemsRowView.swift | 70 - .../Components/PortraitPosterHStack.swift | 89 ++ .../Components/PublicUserButton.swift | 50 - Swiftfin tvOS/Objects/FocusGuide.swift | 149 ++ .../{AboutView.swift => AboutAppView.swift} | 2 +- .../Views/BasicAppSettingsView.swift | 2 +- .../ContinueWatchingCard.swift | 19 +- .../ContinueWatchingView.swift | 2 +- Swiftfin tvOS/Views/HomeView.swift | 59 +- .../CinematicCollectionItemView.swift | 40 +- .../CinematicEpisodeItemView.swift | 36 +- .../CinematicItemAboutView.swift | 0 .../CinematicItemViewTopRowButton.swift | 51 - .../CinematicMovieItemView.swift | 75 - .../CinematicSeasonItemView.swift | 92 -- .../CinematicSeriesItemView.swift | 78 - .../CinematicItemViewTopRow.swift | 59 +- .../ItemView/CinematicSeasonItemView.swift | 84 ++ .../CollectionItemContentView.swift | 111 ++ .../CollectionItemView.swift | 21 + .../CompactItemView/EpisodeItemView.swift | 163 -- .../CompactItemView/MovieItemView.swift | 180 --- .../CompactItemView/SeasonItemView.swift | 139 -- .../CompactItemView/SeriesItemView.swift | 194 --- .../Components/AboutView/AboutView.swift | 108 ++ .../Components/AboutView/AboutViewCard.swift | 43 + .../Components/ActionButtonHStack.swift | 56 + .../ItemView/Components/AttributeHStack.swift | 54 + .../EpisodeItemContentView.swift | 187 +++ .../EpisodeItemView/EpisodeItemView.swift | 17 +- .../ItemView/Components/PlayButton.swift | 71 + Swiftfin tvOS/Views/ItemView/ItemView.swift | 62 +- .../MovieItemView/MovieItemContentView.swift | 111 ++ .../MovieItemView/MovieItemView.swift | 21 + .../ScrollViews/CinematicScrollView.swift | 129 ++ .../Components/EpisodeCard.swift | 84 ++ .../Components/SeriesEpisodesView.swift | 153 ++ .../SeriesItemContentView.swift | 121 ++ .../SeriesItemView/SeriesItemView.swift | 21 + Swiftfin tvOS/Views/LatestInLibraryView.swift | 56 + Swiftfin tvOS/Views/LatestMediaView.swift | 87 -- Swiftfin tvOS/Views/LibraryFilterView.swift | 2 +- Swiftfin tvOS/Views/LibraryListView.swift | 23 +- Swiftfin tvOS/Views/LibraryView.swift | 4 +- .../Views/LiveTVChannelItemElement.swift | 2 +- Swiftfin tvOS/Views/LiveTVChannelsView.swift | 2 +- Swiftfin tvOS/Views/LiveTVProgramsView.swift | 2 +- Swiftfin tvOS/Views/MovieLibrariesView.swift | 2 +- .../Views/NextUpView/NextUpCard.swift | 51 - .../Views/NextUpView/NextUpView.swift | 37 - Swiftfin tvOS/Views/ServerListView.swift | 2 +- .../Views/SettingsView/SettingsView.swift | 10 +- Swiftfin tvOS/Views/TVLibrariesView.swift | 2 +- Swiftfin tvOS/Views/UserListView.swift | 2 +- .../Overlays/tvOSLiveTVOverlay.swift | 1 + .../VideoPlayer/Overlays/tvOSVLCOverlay.swift | 1 + Swiftfin.xcodeproj/project.pbxproj | 1338 +++++++++-------- .../xcshareddata/swiftpm/Package.resolved | 9 - Swiftfin/App/JellyfinPlayerApp.swift | 12 +- .../PreferenceUIHostingController.swift | 4 +- .../PreferenceUIHostingSwizzling.swift | 2 +- Swiftfin/Components/DotHStack.swift | 235 +++ .../EpisodesRowView/EpisodeRowCard.swift | 73 - .../EpisodesRowView/EpisodesRowView.swift | 137 -- ...{PillHStackView.swift => PillHStack.swift} | 25 +- Swiftfin/Components/PortraitHStackView.swift | 87 -- ...utton.swift => PortraitPosterButton.swift} | 27 +- .../Components/PortraitPosterHStack.swift | 84 ++ ...ryButtonView.swift => PrimaryButton.swift} | 8 +- .../Components/RefreshableScrollView.swift | 33 + Swiftfin/Components/TruncatedTextView.swift | 118 -- .../NavBarOffsetModifier.swift | 25 + .../NavBarOffsetView.swift | 94 ++ .../iOSViewExtensions/iOSViewExtensions.swift | 15 + .../{AboutView.swift => AboutAppView.swift} | 2 +- Swiftfin/Views/BasicAppSettingsView.swift | 2 +- Swiftfin/Views/ContinueWatchingView.swift | 112 -- Swiftfin/Views/HomeView.swift | 147 -- .../ContinueWatchingCard.swift | 97 ++ .../ContinueWatchingView.swift | 37 + .../Components/LatestInLibraryView.swift | 39 + Swiftfin/Views/HomeView/HomeContentView.swift | 57 + Swiftfin/Views/HomeView/HomeErrorView.swift | 48 + Swiftfin/Views/HomeView/HomeView.swift | 41 + Swiftfin/Views/ItemOverviewView.swift | 4 +- .../Views/ItemView/Components/AboutView.swift | 81 + .../Components/ActionButtonHStack.swift | 90 ++ .../ItemView/Components/AttributeHStack.swift | 49 + .../EpisodesRowView/EpisodeCard.swift | 68 + .../EpisodesRowView/SeriesEpisodesView.swift | 85 ++ .../ItemView/Components/ListDetailsView.swift | 42 + .../ItemView/Components/PlayButton.swift | 59 + Swiftfin/Views/ItemView/ItemView.swift | 84 +- Swiftfin/Views/ItemView/ItemViewBody.swift | 182 --- .../Views/ItemView/ItemViewDetailsView.swift | 82 - .../Landscape/ItemLandscapeMainView.swift | 120 -- .../Landscape/ItemLandscapeTopBarView.swift | 133 -- .../ItemPortraitHeaderOverlayView.swift | 202 --- .../Portrait/ItemPortraitMainView.swift | 60 - .../CollectionItemContentView.swift | 62 + .../CollectionItemView.swift | 35 + .../EpisodeItemContentView.swift | 149 ++ .../iOS/EpisodeItemView/EpisodeItemView.swift | 32 + .../MovieItemView/MovieItemContentView.swift | 88 ++ .../iOS/MovieItemView/MovieItemView.swift | 36 + .../iOS/ScrollViews/CinematicScrollView.swift | 182 +++ .../ScrollViews/CompactLogoScrollView.swift | 189 +++ .../CompactPortraitScrollView.swift | 203 +++ .../SeriesItemContentView.swift | 85 ++ .../iOS/SeriesItemView/SeriesItemView.swift | 36 + .../iPadOSCollectionItemContentView.swift | 65 + .../iPadOSCollectionItemView.swift | 21 + .../iPadOSEpisodeContentView.swift | 67 + .../iPadOSEpisodeItemView.swift | 24 + .../iPadOSMovieItemContentView.swift | 84 ++ .../MovieItemView/iPadOSMovieItemView.swift} | 11 +- .../iPadOSCinematicScrollView.swift | 165 ++ .../iPadOSSeriesItemContentView.swift | 88 ++ .../SeriesItemView/iPadOSSeriesItemView.swift | 14 +- Swiftfin/Views/LatestMediaView.swift | 30 - Swiftfin/Views/LibraryFilterView.swift | 2 +- Swiftfin/Views/LibraryListView.swift | 25 +- Swiftfin/Views/LibrarySearchView.swift | 4 +- Swiftfin/Views/LibraryView.swift | 4 +- Swiftfin/Views/LiveTVChannelItemElement.swift | 2 +- .../Views/LiveTVChannelItemWideElement.swift | 2 +- Swiftfin/Views/LiveTVChannelsView.swift | 4 +- Swiftfin/Views/LiveTVProgramsView.swift | 81 +- Swiftfin/Views/ServerListView.swift | 6 +- .../Views/SettingsView/SettingsView.swift | 36 +- Swiftfin/Views/UserListView.swift | 20 +- .../Overlays/VLCPlayerOverlayView.swift | 1 + Translations/en.lproj/Localizable.strings | Bin 14392 -> 14650 bytes .../AccentColor.colorset/Contents.json | 32 - WidgetExtension/Assets.xcassets/Contents.json | 6 - .../WidgetBackground.colorset/Contents.json | 20 - .../WidgetHeaderSymbol.imageset/1024.png | Bin 261282 -> 0 bytes .../WidgetHeaderSymbol.imageset/Contents.json | 12 - WidgetExtension/Info.plist | 29 - WidgetExtension/NextUpWidget.swift | 542 ------- swiftgen.yml | 4 +- 227 files changed, 8418 insertions(+), 6399 deletions(-) create mode 100755 Shared/BlurHashKit/BlurHash.swift create mode 100755 Shared/BlurHashKit/ColourProbes.swift create mode 100755 Shared/BlurHashKit/ColourSpace.swift create mode 100755 Shared/BlurHashKit/EscapeSequences.swift create mode 100755 Shared/BlurHashKit/FromString.swift create mode 100755 Shared/BlurHashKit/FromUIImage.swift create mode 100755 Shared/BlurHashKit/Generation.swift create mode 100755 Shared/BlurHashKit/StringCoding.swift create mode 100755 Shared/BlurHashKit/ToString.swift create mode 100755 Shared/BlurHashKit/ToUIImage.swift create mode 100755 Shared/BlurHashKit/TupleMaths.swift delete mode 100644 Shared/Extensions/BlurHashDecode.swift mode change 100644 => 100755 Shared/Extensions/Defaults+Workaround.swift create mode 100644 Shared/Extensions/FontExtensions.swift create mode 100644 Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift create mode 100644 Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Poster.swift delete mode 100644 Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Stackable.swift create mode 100644 Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift rename Shared/Extensions/{ViewExtensions.swift => JellyfinAPIExtensions/RequestBuilderExtensions.swift} (71%) rename WidgetExtension/JellyfinWidget.swift => Shared/Extensions/UIScreenExtensions.swift (60%) create mode 100644 Shared/Extensions/UIScrollViewExtensions.swift create mode 100644 Shared/Extensions/ViewExtensions/BackgroundParallaxHeaderModifier.swift create mode 100644 Shared/Extensions/ViewExtensions/BottomEdgeGradientModifier.swift create mode 100644 Shared/Extensions/ViewExtensions/ScrollViewOffsetModifier.swift create mode 100644 Shared/Extensions/ViewExtensions/ViewExtensions.swift delete mode 100644 Shared/Generated/LocalizedLookup.swift delete mode 100644 Shared/Generated/Strings.swift create mode 100644 Shared/Objects/ItemViewType.swift delete mode 100644 Shared/Objects/OverlaySliderColor.swift rename Shared/{UIKit => Objects}/PanDirectionGestureRecognizer.swift (100%) delete mode 100644 Shared/Objects/PortraitImageStackable.swift create mode 100644 Shared/Objects/Poster.swift delete mode 100644 Shared/Objects/PosterSize.swift delete mode 100644 Shared/Singleton/BackgroundManager.swift create mode 100644 Shared/Strings/Strings.swift delete mode 100644 Shared/ViewModels/MainTabViewModel.swift rename {Swiftfin/Components => Shared/Views}/AppIcon.swift (100%) create mode 100644 Shared/Views/AttributeFillView.swift create mode 100644 Shared/Views/AttributeOutlineView.swift create mode 100644 Shared/Views/BlurView.swift rename Shared/Views/{LazyView.swift => Divider.swift} (70%) delete mode 100644 Shared/Views/ParallaxHeader.swift create mode 100644 Shared/Views/TruncatedTextView.swift create mode 100644 Swiftfin tvOS/Components/DotHStack.swift delete mode 100644 Swiftfin tvOS/Components/EpisodesRowView/EpisodesRowCard.swift delete mode 100644 Swiftfin tvOS/Components/EpisodesRowView/EpisodesRowView.swift delete mode 100644 Swiftfin tvOS/Components/MediaPlayButtonRowView.swift delete mode 100644 Swiftfin tvOS/Components/MediaViewActionButton.swift delete mode 100644 Swiftfin tvOS/Components/PlainLinkButton.swift create mode 100644 Swiftfin tvOS/Components/PortraitButton.swift delete mode 100644 Swiftfin tvOS/Components/PortraitItemsRowView.swift create mode 100644 Swiftfin tvOS/Components/PortraitPosterHStack.swift delete mode 100644 Swiftfin tvOS/Components/PublicUserButton.swift create mode 100644 Swiftfin tvOS/Objects/FocusGuide.swift rename Swiftfin tvOS/Views/{AboutView.swift => AboutAppView.swift} (92%) rename Swiftfin tvOS/Views/ItemView/{CinematicItemView => }/CinematicCollectionItemView.swift (61%) rename Swiftfin tvOS/Views/ItemView/{CinematicItemView => }/CinematicEpisodeItemView.swift (66%) rename Swiftfin tvOS/Views/ItemView/{CinematicItemView => }/CinematicItemAboutView.swift (100%) delete mode 100644 Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRowButton.swift delete mode 100644 Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicMovieItemView.swift delete mode 100644 Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicSeasonItemView.swift delete mode 100644 Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicSeriesItemView.swift rename Swiftfin tvOS/Views/ItemView/{CinematicItemView => }/CinematicItemViewTopRow.swift (63%) create mode 100644 Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift delete mode 100644 Swiftfin tvOS/Views/ItemView/CompactItemView/EpisodeItemView.swift delete mode 100644 Swiftfin tvOS/Views/ItemView/CompactItemView/MovieItemView.swift delete mode 100644 Swiftfin tvOS/Views/ItemView/CompactItemView/SeasonItemView.swift delete mode 100644 Swiftfin tvOS/Views/ItemView/CompactItemView/SeriesItemView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutViewCard.swift create mode 100644 Swiftfin tvOS/Views/ItemView/Components/ActionButtonHStack.swift create mode 100644 Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift create mode 100644 Swiftfin tvOS/Views/ItemView/Components/EpisodeItemView/EpisodeItemContentView.swift rename Shared/Extensions/ImageExtensions.swift => Swiftfin tvOS/Views/ItemView/Components/EpisodeItemView/EpisodeItemView.swift (50%) create mode 100644 Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift create mode 100644 Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/EpisodeCard.swift create mode 100644 Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodesView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift create mode 100644 Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift create mode 100644 Swiftfin tvOS/Views/LatestInLibraryView.swift delete mode 100644 Swiftfin tvOS/Views/LatestMediaView.swift delete mode 100644 Swiftfin tvOS/Views/NextUpView/NextUpCard.swift delete mode 100644 Swiftfin tvOS/Views/NextUpView/NextUpView.swift create mode 100644 Swiftfin/Components/DotHStack.swift delete mode 100644 Swiftfin/Components/EpisodesRowView/EpisodeRowCard.swift delete mode 100644 Swiftfin/Components/EpisodesRowView/EpisodesRowView.swift rename Swiftfin/Components/{PillHStackView.swift => PillHStack.swift} (70%) delete mode 100644 Swiftfin/Components/PortraitHStackView.swift rename Swiftfin/Components/{PortraitItemButton.swift => PortraitPosterButton.swift} (74%) create mode 100644 Swiftfin/Components/PortraitPosterHStack.swift rename Swiftfin/Components/{PrimaryButtonView.swift => PrimaryButton.swift} (76%) create mode 100644 Swiftfin/Components/RefreshableScrollView.swift delete mode 100644 Swiftfin/Components/TruncatedTextView.swift create mode 100644 Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetModifier.swift create mode 100644 Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetView.swift create mode 100644 Swiftfin/Extensions/iOSViewExtensions/iOSViewExtensions.swift rename Swiftfin/Views/{AboutView.swift => AboutAppView.swift} (99%) delete mode 100644 Swiftfin/Views/ContinueWatchingView.swift delete mode 100644 Swiftfin/Views/HomeView.swift create mode 100644 Swiftfin/Views/HomeView/Components/ContinueWatchingView/ContinueWatchingCard.swift create mode 100644 Swiftfin/Views/HomeView/Components/ContinueWatchingView/ContinueWatchingView.swift create mode 100644 Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift create mode 100644 Swiftfin/Views/HomeView/HomeContentView.swift create mode 100644 Swiftfin/Views/HomeView/HomeErrorView.swift create mode 100644 Swiftfin/Views/HomeView/HomeView.swift create mode 100644 Swiftfin/Views/ItemView/Components/AboutView.swift create mode 100644 Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift create mode 100644 Swiftfin/Views/ItemView/Components/AttributeHStack.swift create mode 100644 Swiftfin/Views/ItemView/Components/EpisodesRowView/EpisodeCard.swift create mode 100644 Swiftfin/Views/ItemView/Components/EpisodesRowView/SeriesEpisodesView.swift create mode 100644 Swiftfin/Views/ItemView/Components/ListDetailsView.swift create mode 100644 Swiftfin/Views/ItemView/Components/PlayButton.swift delete mode 100644 Swiftfin/Views/ItemView/ItemViewBody.swift delete mode 100644 Swiftfin/Views/ItemView/ItemViewDetailsView.swift delete mode 100644 Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift delete mode 100644 Swiftfin/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift delete mode 100644 Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift delete mode 100644 Swiftfin/Views/ItemView/Portrait/ItemPortraitMainView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift create mode 100644 Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift create mode 100644 Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift create mode 100644 Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift create mode 100644 Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift create mode 100644 Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift create mode 100644 Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift rename Swiftfin/{Components/PortraitItemElement.swift => Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift} (59%) create mode 100644 Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift create mode 100644 Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift rename Shared/Views/PortraitItemSize.swift => Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift (51%) delete mode 100644 Swiftfin/Views/LatestMediaView.swift delete mode 100644 WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 WidgetExtension/Assets.xcassets/Contents.json delete mode 100644 WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json delete mode 100644 WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/1024.png delete mode 100644 WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/Contents.json delete mode 100644 WidgetExtension/Info.plist delete mode 100644 WidgetExtension/NextUpWidget.swift diff --git a/.swiftformat b/.swiftformat index 7b2a75eb..6b951e8f 100644 --- a/.swiftformat +++ b/.swiftformat @@ -43,6 +43,6 @@ redundantClosure, \ redundantType ---exclude Shared/Generated/Strings.swift +--exclude Shared/Strings/Strings.swift --header "\nSwiftfin is subject to the terms of the Mozilla Public\nLicense, v2.0. If a copy of the MPL was not distributed with this\nfile, you can obtain one at https://mozilla.org/MPL/2.0/.\n\nCopyright (c) {year} Jellyfin & Jellyfin Contributors\n" diff --git a/Shared/BlurHashKit/BlurHash.swift b/Shared/BlurHashKit/BlurHash.swift new file mode 100755 index 00000000..e5a03f98 --- /dev/null +++ b/Shared/BlurHashKit/BlurHash.swift @@ -0,0 +1,63 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +public struct BlurHash { + public let components: [[(Float, Float, Float)]] + + public var numberOfHorizontalComponents: Int { components.first!.count } + public var numberOfVerticalComponents: Int { components.count } + + public init(components: [[(Float, Float, Float)]]) { + self.components = components + } + + public func punch(_ factor: Float) -> BlurHash { + BlurHash(components: components.enumerated().map { j, horizontalComponents -> [(Float, Float, Float)] in + horizontalComponents.enumerated().map { i, component -> (Float, Float, Float) in + if i == 0 && j == 0 { + return component + } else { + return component * factor + } + } + }) + } +} + +public func + (lhs: BlurHash, rhs: BlurHash) throws -> BlurHash { + BlurHash(components: paddedZip(lhs.components, rhs.components, [], []).map { + paddedZip($0.0, $0.1, (0, 0, 0) as (Float, Float, Float), (0, 0, 0) as (Float, Float, Float)) + .map { ($0.0.0 + $0.1.0, $0.0.1 + $0.1.1, $0.0.2 + $0.1.2) } + }) +} + +public func - (lhs: BlurHash, rhs: BlurHash) throws -> BlurHash { + BlurHash(components: paddedZip(lhs.components, rhs.components, [], []).map { + paddedZip($0.0, $0.1, (0, 0, 0) as (Float, Float, Float), (0, 0, 0) as (Float, Float, Float)) + .map { ($0.0.0 - $0.1.0, $0.0.1 - $0.1.1, $0.0.2 - $0.1.2) } + }) +} + +private func paddedZip( + _ collection1: Collection1, + _ collection2: Collection2, + _ padding1: Collection1.Element, + _ padding2: Collection2.Element +) -> Zip2Sequence<[Collection1.Element], [Collection2.Element]> where Collection1: Collection, Collection2: Collection { + if collection1.count < collection2.count { + let padded = collection1 + Array(repeating: padding1, count: collection2.count - collection1.count) + return zip(padded, Array(collection2)) + } else if collection2.count < collection1.count { + let padded = collection2 + Array(repeating: padding2, count: collection1.count - collection2.count) + return zip(Array(collection1), padded) + } else { + return zip(Array(collection1), Array(collection2)) + } +} diff --git a/Shared/BlurHashKit/ColourProbes.swift b/Shared/BlurHashKit/ColourProbes.swift new file mode 100755 index 00000000..26817493 --- /dev/null +++ b/Shared/BlurHashKit/ColourProbes.swift @@ -0,0 +1,102 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +public extension BlurHash { + func linearRGB(atX x: Float) -> (Float, Float, Float) { + return components[0].enumerated().reduce((0, 0, 0)) { sum, horizontalEnumerated -> (Float, Float, Float) in + let (i, component) = horizontalEnumerated + return sum + component * cos(Float.pi * Float(i) * x) + } + } + + func linearRGB(atY y: Float) -> (Float, Float, Float) { + return components.enumerated().reduce((0, 0, 0)) { sum, verticalEnumerated in + let (j, horizontalComponents) = verticalEnumerated + return sum + horizontalComponents[0] * cos(Float.pi * Float(j) * y) + } + } + + func linearRGB(at position: (Float, Float)) -> (Float, Float, Float) { + return components.enumerated().reduce((0, 0, 0)) { sum, verticalEnumerated in + let (j, horizontalComponents) = verticalEnumerated + return horizontalComponents.enumerated().reduce(sum) { sum, horizontalEnumerated in + let (i, component) = horizontalEnumerated + return sum + component * cos(Float.pi * Float(i) * position.0) * cos(Float.pi * Float(j) * position.1) + } + } + } + + func linearRGB(from upperLeft: (Float, Float), to lowerRight: (Float, Float)) -> (Float, Float, Float) { + return components.enumerated().reduce((0, 0, 0)) { sum, verticalEnumerated in + let (j, horizontalComponents) = verticalEnumerated + return horizontalComponents.enumerated().reduce(sum) { sum, horizontalEnumerated in + let (i, component) = horizontalEnumerated + let horizontalAverage: Float = i == 0 ? 1 : + (sin(Float.pi * Float(i) * lowerRight.0) - sin(Float.pi * Float(i) * upperLeft.0)) / + (Float(i) * Float.pi * (lowerRight.0 - upperLeft.0)) + let veritcalAverage: Float = j == 0 ? 1 : + (sin(Float.pi * Float(j) * lowerRight.1) - sin(Float.pi * Float(j) * upperLeft.1)) / + (Float(j) * Float.pi * (lowerRight.1 - upperLeft.1)) + return sum + component * horizontalAverage * veritcalAverage + } + } + } + + func linearRGB(at upperLeft: (Float, Float), size: (Float, Float)) -> (Float, Float, Float) { + return linearRGB(from: upperLeft, to: (upperLeft.0 + size.0, upperLeft.1 + size.1)) + } + + var averageLinearRGB: (Float, Float, Float) { + return components[0][0] + } + + var leftEdgeLinearRGB: (Float, Float, Float) { return linearRGB(atX: 0) } + var rightEdgeLinearRGB: (Float, Float, Float) { return linearRGB(atX: 1) } + var topEdgeLinearRGB: (Float, Float, Float) { return linearRGB(atY: 0) } + var bottomEdgeLinearRGB: (Float, Float, Float) { return linearRGB(atY: 1) } + var topLeftCornerLinearRGB: (Float, Float, Float) { return linearRGB(at: (0, 0)) } + var topRightCornerLinearRGB: (Float, Float, Float) { return linearRGB(at: (1, 0)) } + var bottomLeftCornerLinearRGB: (Float, Float, Float) { return linearRGB(at: (0, 1)) } + var bottomRightCornerLinearRGB: (Float, Float, Float) { return linearRGB(at: (1, 1)) } +} + +public extension BlurHash { + func isDark(linearRGB rgb: (Float, Float, Float), threshold: Float = 0.3) -> Bool { + rgb.0 * 0.299 + rgb.1 * 0.587 + rgb.2 * 0.114 < threshold + } + + func isDark(threshold: Float = 0.3) -> Bool { isDark(linearRGB: averageLinearRGB, threshold: threshold) } + + func isDark(atX x: Float, threshold: Float = 0.3) -> Bool { isDark(linearRGB: linearRGB(atX: x), threshold: threshold) } + func isDark(atY y: Float, threshold: Float = 0.3) -> Bool { isDark(linearRGB: linearRGB(atY: y), threshold: threshold) } + func isDark( + at position: (Float, Float), + threshold: Float = 0.3 + ) -> Bool { isDark(linearRGB: linearRGB(at: position), threshold: threshold) } + func isDark( + from upperLeft: (Float, Float), + to lowerRight: (Float, Float), + threshold: Float = 0.3 + ) -> Bool { isDark(linearRGB: linearRGB(from: upperLeft, to: lowerRight), threshold: threshold) } + func isDark( + at upperLeft: (Float, Float), + size: (Float, Float), + threshold: Float = 0.3 + ) -> Bool { isDark(linearRGB: linearRGB(at: upperLeft, size: size), threshold: threshold) } + + var isLeftEdgeDark: Bool { isDark(atX: 0) } + var isRightEdgeDark: Bool { isDark(atX: 1) } + var isTopEdgeDark: Bool { isDark(atY: 0) } + var isBottomEdgeDark: Bool { isDark(atY: 1) } + var isTopLeftCornerDark: Bool { isDark(at: (0, 0)) } + var isTopRightCornerDark: Bool { isDark(at: (1, 0)) } + var isBottomLeftCornerDark: Bool { isDark(at: (0, 1)) } + var isBottomRightCornerDark: Bool { isDark(at: (1, 1)) } +} diff --git a/Shared/BlurHashKit/ColourSpace.swift b/Shared/BlurHashKit/ColourSpace.swift new file mode 100755 index 00000000..dc2f464b --- /dev/null +++ b/Shared/BlurHashKit/ColourSpace.swift @@ -0,0 +1,25 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +func signPow(_ value: Float, _ exp: Float) -> Float { + copysign(pow(abs(value), exp), value) +} + +func linearTosRGB(_ value: Float) -> Int { + let v = max(0, min(1, value)) + if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } + else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } +} + +func sRGBToLinear(_ value: Type) -> Float { + let v = Float(Int64(value)) / 255 + if v <= 0.04045 { return v / 12.92 } + else { return pow((v + 0.055) / 1.055, 2.4) } +} diff --git a/Shared/BlurHashKit/EscapeSequences.swift b/Shared/BlurHashKit/EscapeSequences.swift new file mode 100755 index 00000000..8185a922 --- /dev/null +++ b/Shared/BlurHashKit/EscapeSequences.swift @@ -0,0 +1,43 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +extension BlurHash { + var twoByThreeEscapeSequence: String { + let areas: [(from: (Float, Float), to: (Float, Float))] = [ + (from: (0, 0), to: (0.333, 0.5)), + (from: (0, 0.5), to: (0.333, 1.0)), + (from: (0.333, 0), to: (0.666, 0.5)), + (from: (0.333, 0.5), to: (0.666, 1.0)), + (from: (0.666, 0), to: (1.0, 0.5)), + (from: (0.666, 0.5), to: (1.0, 1.0)), + ] + + let rgb: [(Float, Float, Float)] = areas.map { area in + linearRGB(from: area.from, to: area.to) + } + + let maxRgb: (Float, Float, Float) = rgb.reduce((-Float.infinity, -Float.infinity, -Float.infinity), max) + let minRgb: (Float, Float, Float) = rgb.reduce((Float.infinity, Float.infinity, Float.infinity), min) + + let positiveScale: (Float, Float, Float) = ((1, 1, 1) - averageLinearRGB) / (maxRgb - averageLinearRGB) + let negativeScale: (Float, Float, Float) = averageLinearRGB / (averageLinearRGB - minRgb) + let scale: (Float, Float, Float) = min(positiveScale, negativeScale) + + let scaledRgb: [(Float, Float, Float)] = rgb.map { rgb in + (rgb - averageLinearRGB) * scale + averageLinearRGB + } + + let c = scaledRgb.map { rgb in + (linearTosRGB(rgb.0) / 51) * 36 + (linearTosRGB(rgb.1) / 51) * 6 + (linearTosRGB(rgb.2) / 51) + 16 + } + + return "\u{1b}[38;5;\(c[1]);48;5;\(c[0])m▄\u{1b}[38;5;\(c[3]);48;5;\(c[2])m▄\u{1b}[38;5;\(c[5]);48;5;\(c[4])m▄\u{1b}[m" + } +} diff --git a/Shared/BlurHashKit/FromString.swift b/Shared/BlurHashKit/FromString.swift new file mode 100755 index 00000000..756a793c --- /dev/null +++ b/Shared/BlurHashKit/FromString.swift @@ -0,0 +1,76 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +public extension BlurHash { + init?(string: String) { + guard string.count >= 6 else { return nil } + + let sizeFlag = String(string[0]).decode83() + let numberOfHorizontalComponents = (sizeFlag % 9) + 1 + let numberOfVerticalComponents = (sizeFlag / 9) + 1 + + let quantisedMaximumValue = String(string[1]).decode83() + let maximumValue = Float(quantisedMaximumValue + 1) / 166 + + guard string.count == 4 + 2 * numberOfHorizontalComponents * numberOfVerticalComponents else { return nil } + + self.components = (0 ..< numberOfVerticalComponents).map { j in + (0 ..< numberOfHorizontalComponents).map { i in + if i == 0 && j == 0 { + let value = String(string[2 ..< 6]).decode83() + return BlurHash.decodeDC(value) + } else { + let index = i + j * numberOfHorizontalComponents + let value = String(string[4 + index * 2 ..< 4 + index * 2 + 2]).decode83() + return BlurHash.decodeAC(value, maximumValue: maximumValue) + } + } + } + } + + private static func decodeDC(_ value: Int) -> (Float, Float, Float) { + let intR = value >> 16 + let intG = (value >> 8) & 255 + let intB = value & 255 + return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) + } + + private static func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { + let quantR = value / (19 * 19) + let quantG = (value / 19) % 19 + let quantB = value % 19 + + let rgb = ( + signPow((Float(quantR) - 9) / 9, 2) * maximumValue, + signPow((Float(quantG) - 9) / 9, 2) * maximumValue, + signPow((Float(quantB) - 9) / 9, 2) * maximumValue + ) + + return rgb + } +} + +private extension String { + subscript(offset: Int) -> Character { + self[index(startIndex, offsetBy: offset)] + } + + subscript(bounds: CountableClosedRange) -> Substring { + let start = index(startIndex, offsetBy: bounds.lowerBound) + let end = index(startIndex, offsetBy: bounds.upperBound) + return self[start ... end] + } + + subscript(bounds: CountableRange) -> Substring { + let start = index(startIndex, offsetBy: bounds.lowerBound) + let end = index(startIndex, offsetBy: bounds.upperBound) + return self[start ..< end] + } +} diff --git a/Shared/BlurHashKit/FromUIImage.swift b/Shared/BlurHashKit/FromUIImage.swift new file mode 100755 index 00000000..799efa4d --- /dev/null +++ b/Shared/BlurHashKit/FromUIImage.swift @@ -0,0 +1,92 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import UIKit + +public extension BlurHash { + init?(image: UIImage, numberOfComponents components: (Int, Int)) { + guard components.0 >= 1, components.0 <= 9, + components.1 >= 1, components.1 <= 9 + else { + fatalError("Number of components bust be between 1 and 9 inclusive on each axis") + } + + let pixelWidth = Int(round(image.size.width * image.scale)) + let pixelHeight = Int(round(image.size.height * image.scale)) + + let context = CGContext( + data: nil, + width: pixelWidth, + height: pixelHeight, + bitsPerComponent: 8, + bytesPerRow: pixelWidth * 4, + space: CGColorSpace(name: CGColorSpace.sRGB)!, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + )! + context.scaleBy(x: image.scale, y: -image.scale) + context.translateBy(x: 0, y: -image.size.height) + + UIGraphicsPushContext(context) + image.draw(at: .zero) + UIGraphicsPopContext() + + guard let cgImage = context.makeImage(), + let dataProvider = cgImage.dataProvider, + let data = dataProvider.data, + let pixels = CFDataGetBytePtr(data) + else { + assertionFailure("Unexpected error!") + return nil + } + + let width = cgImage.width + let height = cgImage.height + let bytesPerRow = cgImage.bytesPerRow + + self.components = (0 ..< components.1).map { j -> [(Float, Float, Float)] in + (0 ..< components.0).map { i -> (Float, Float, Float) in + let normalisation: Float = (i == 0 && j == 0) ? 1 : 2 + return BlurHash.multiplyBasisFunction( + pixels: pixels, + width: width, + height: height, + bytesPerRow: bytesPerRow, + bytesPerPixel: cgImage.bitsPerPixel / 8 + ) { x, y in + normalisation * cos(Float.pi * Float(i) * x / Float(width)) as Float * + cos(Float.pi * Float(j) * y / Float(height)) as Float + } + } + } + } + + private static func multiplyBasisFunction( + pixels: UnsafePointer, + width: Int, + height: Int, + bytesPerRow: Int, + bytesPerPixel: Int, + basisFunction: (Float, Float) -> Float + ) -> (Float, Float, Float) { + var c: (Float, Float, Float) = (0, 0, 0) + + let buffer = UnsafeBufferPointer(start: pixels, count: height * bytesPerRow) + + for x in 0 ..< width { + for y in 0 ..< height { + c += basisFunction(Float(x), Float(y)) * ( + sRGBToLinear(buffer[bytesPerPixel * x + 0 + y * bytesPerRow]), + sRGBToLinear(buffer[bytesPerPixel * x + 1 + y * bytesPerRow]), + sRGBToLinear(buffer[bytesPerPixel * x + 2 + y * bytesPerRow]) + ) + } + } + + return c / Float(width * height) + } +} diff --git a/Shared/BlurHashKit/Generation.swift b/Shared/BlurHashKit/Generation.swift new file mode 100755 index 00000000..92b27cd8 --- /dev/null +++ b/Shared/BlurHashKit/Generation.swift @@ -0,0 +1,115 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import UIKit + +public extension BlurHash { + init(blendingTop top: BlurHash, bottom: BlurHash) { + guard top.components.count == 1, bottom.components.count == 1 else { + fatalError("Blended BlurHashses must have only one vertical component") + } + + let average = zip(top.components[0], bottom.components[0]).map { ($0 + $1) / 2 } + let difference = zip(top.components[0], bottom.components[0]).map { ($0 - $1) / 2 } + self.init(components: [average, difference]) + } + + init(blendingLeft left: BlurHash, right: BlurHash) { + self = BlurHash(blendingTop: left.transposed, bottom: right.transposed).transposed + } +} + +public extension BlurHash { + init(colour: UIColor) { + self.init(components: [[colour.linear]]) + } + + init(blendingTop topColour: UIColor, bottom bottomColour: UIColor) { + self = BlurHash(blendingTop: .init(colour: topColour), bottom: .init(colour: bottomColour)) + } + + init(blendingLeft leftColour: UIColor, right rightColour: UIColor) { + self = BlurHash(blendingLeft: .init(colour: leftColour), right: .init(colour: rightColour)) + } + + init( + blendingTopLeft topLeftColour: UIColor, + topRight topRightColour: UIColor, + bottomLeft bottomLeftColour: UIColor, + bottomRight bottomRightColour: UIColor + ) { + self = BlurHash( + blendingTop: BlurHash(blendingTop: topLeftColour, bottom: topRightColour).transposed, + bottom: BlurHash(blendingTop: bottomLeftColour, bottom: bottomRightColour).transposed + ) + } +} + +public extension BlurHash { + init(horizontalColours colours: [(Float, Float, Float)], numberOfComponents: Int) { + guard numberOfComponents >= 1, numberOfComponents <= 9 else { + fatalError("Number of components bust be between 1 and 9 inclusive") + } + + self.init(components: [(0 ..< numberOfComponents).map { i in + let normalisation: Float = i == 0 ? 1 : 2 + var sum: (Float, Float, Float) = (0, 0, 0) + for x in 0 ..< colours.count { + let basis = normalisation * cos(Float.pi * Float(i) * Float(x) / Float(colours.count - 1)) + sum += basis * colours[x] + } + + return sum / Float(colours.count) + }]) + } +} + +public extension BlurHash { + var mirroredHorizontally: BlurHash { + .init(components: (0 ..< numberOfVerticalComponents).map { j -> [(Float, Float, Float)] in + (0 ..< numberOfHorizontalComponents).map { i -> (Float, Float, Float) in + components[j][i] * (i % 2 == 0 ? 1 : -1) + } + }) + } + + var mirroredVertically: BlurHash { + .init(components: (0 ..< numberOfVerticalComponents).map { j -> [(Float, Float, Float)] in + (0 ..< numberOfHorizontalComponents).map { i -> (Float, Float, Float) in + components[j][i] * (j % 2 == 0 ? 1 : -1) + } + }) + } + + var transposed: BlurHash { + .init(components: (0 ..< numberOfHorizontalComponents).map { i in + (0 ..< numberOfVerticalComponents).map { j in + components[j][i] + } + }) + } +} + +extension UIColor { + var linear: (Float, Float, Float) { + guard let c = cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil)?.components + else { return (0, 0, 0) } + + switch c.count { + case 1, 2: return (sRGBToLinear(c[0]), sRGBToLinear(c[0]), sRGBToLinear(c[0])) + case 3, 4: return (sRGBToLinear(c[0]), sRGBToLinear(c[1]), sRGBToLinear(c[2])) + default: return (0, 0, 0) + } + } +} + +func sRGBToLinear(_ value: CGFloat) -> Float { + let v = Float(value) + if v <= 0.04045 { return v / 12.92 } + else { return pow((v + 0.055) / 1.055, 2.4) } +} diff --git a/Shared/BlurHashKit/StringCoding.swift b/Shared/BlurHashKit/StringCoding.swift new file mode 100755 index 00000000..857a7295 --- /dev/null +++ b/Shared/BlurHashKit/StringCoding.swift @@ -0,0 +1,48 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +private let encodeCharacters: [String] = { + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } +}() + +private let decodeCharacters: [String: Int] = { + var dict: [String: Int] = [:] + for (index, character) in encodeCharacters.enumerated() { + dict[character] = index + } + return dict +}() + +extension BinaryInteger { + func encode83(length: Int) -> String { + var result = "" + for i in 1 ... length { + let digit = (Int(self) / pow(83, length - i)) % 83 + result += encodeCharacters[Int(digit)] + } + return result + } +} + +extension String { + func decode83() -> Int { + var value: Int = 0 + for character in self { + if let digit = decodeCharacters[String(character)] { + value = value * 83 + digit + } + } + return value + } +} + +private func pow(_ base: Int, _ exponent: Int) -> Int { + (0 ..< exponent).reduce(1) { value, _ in value * base } +} diff --git a/Shared/BlurHashKit/ToString.swift b/Shared/BlurHashKit/ToString.swift new file mode 100755 index 00000000..8c8adfb1 --- /dev/null +++ b/Shared/BlurHashKit/ToString.swift @@ -0,0 +1,56 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +public extension BlurHash { + var string: String { + let flatComponents = components.reduce([]) { $0 + $1 } + let dc = flatComponents.first! + let ac = flatComponents.dropFirst() + + var hash = "" + + let sizeFlag = (numberOfHorizontalComponents - 1) + (numberOfVerticalComponents - 1) * 9 + hash += sizeFlag.encode83(length: 1) + + let maximumValue: Float + if !ac.isEmpty { + let actualMaximumValue = ac.map { max(abs($0.0), abs($0.1), abs($0.2)) }.max()! + let quantisedMaximumValue = Int(max(0, min(82, floor(actualMaximumValue * 166 - 0.5)))) + maximumValue = Float(quantisedMaximumValue + 1) / 166 + hash += quantisedMaximumValue.encode83(length: 1) + } else { + maximumValue = 1 + hash += 0.encode83(length: 1) + } + + hash += encodeDC(dc).encode83(length: 4) + + for factor in ac { + hash += encodeAC(factor, maximumValue: maximumValue).encode83(length: 2) + } + + return hash + } + + private func encodeDC(_ value: (Float, Float, Float)) -> Int { + let roundedR = linearTosRGB(value.0) + let roundedG = linearTosRGB(value.1) + let roundedB = linearTosRGB(value.2) + return (roundedR << 16) + (roundedG << 8) + roundedB + } + + private func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int { + let quantR = Int(max(0, min(18, floor(signPow(value.0 / maximumValue, 0.5) * 9 + 9.5)))) + let quantG = Int(max(0, min(18, floor(signPow(value.1 / maximumValue, 0.5) * 9 + 9.5)))) + let quantB = Int(max(0, min(18, floor(signPow(value.2 / maximumValue, 0.5) * 9 + 9.5)))) + + return quantR * 19 * 19 + quantG * 19 + quantB + } +} diff --git a/Shared/BlurHashKit/ToUIImage.swift b/Shared/BlurHashKit/ToUIImage.swift new file mode 100755 index 00000000..79bbf51b --- /dev/null +++ b/Shared/BlurHashKit/ToUIImage.swift @@ -0,0 +1,101 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import UIKit + +public extension BlurHash { + func cgImage(size: CGSize) -> CGImage? { + let width = Int(size.width) + let height = Int(size.height) + let bytesPerRow = width * 3 + + guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } + CFDataSetLength(data, bytesPerRow * height) + + guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } + + for y in 0 ..< height { + for x in 0 ..< width { + var c: (Float, Float, Float) = (0, 0, 0) + + for j in 0 ..< numberOfVerticalComponents { + for i in 0 ..< numberOfHorizontalComponents { + let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) + let component = components[j][i] + c += component * basis + } + } + + let intR = UInt8(linearTosRGB(c.0)) + let intG = UInt8(linearTosRGB(c.1)) + let intB = UInt8(linearTosRGB(c.2)) + + pixels[3 * x + 0 + y * bytesPerRow] = intR + pixels[3 * x + 1 + y * bytesPerRow] = intG + pixels[3 * x + 2 + y * bytesPerRow] = intB + } + } + + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) + + guard let provider = CGDataProvider(data: data) else { return nil } + guard let cgImage = CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 24, + bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: bitmapInfo, + provider: provider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) else { return nil } + + return cgImage + } + + func cgImage(numberOfPixels: Int = 1024, originalSize size: CGSize) -> CGImage? { + let width: CGFloat + let height: CGFloat + if size.width > size.height { + width = floor(sqrt(CGFloat(numberOfPixels) * size.width / size.height) + 0.5) + height = floor(CGFloat(numberOfPixels) / width + 0.5) + } else { + height = floor(sqrt(CGFloat(numberOfPixels) * size.height / size.width) + 0.5) + width = floor(CGFloat(numberOfPixels) / height + 0.5) + } + return cgImage(size: CGSize(width: width, height: height)) + } + + func image(size: CGSize) -> UIImage? { + guard let cgImage = cgImage(size: size) else { return nil } + return UIImage(cgImage: cgImage) + } + + func image(numberOfPixels: Int = 1024, originalSize size: CGSize) -> UIImage? { + guard let cgImage = cgImage(numberOfPixels: numberOfPixels, originalSize: size) else { return nil } + return UIImage(cgImage: cgImage) + } +} + +@objc +public extension UIImage { + convenience init?(blurHash string: String, size: CGSize, punch: Float = 1) { + guard let blurHash = BlurHash(string: string), + let cgImage = blurHash.punch(punch).cgImage(size: size) else { return nil } + self.init(cgImage: cgImage) + } + + convenience init?(blurHash string: String, numberOfPixels: Int = 1024, originalSize size: CGSize, punch: Float = 1) { + guard let blurHash = BlurHash(string: string), + let cgImage = blurHash.punch(punch).cgImage(numberOfPixels: numberOfPixels, originalSize: size) else { return nil } + self.init(cgImage: cgImage) + } +} diff --git a/Shared/BlurHashKit/TupleMaths.swift b/Shared/BlurHashKit/TupleMaths.swift new file mode 100755 index 00000000..9cbfb18d --- /dev/null +++ b/Shared/BlurHashKit/TupleMaths.swift @@ -0,0 +1,61 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +func + (lhs: (Float, Float, Float), rhs: (Float, Float, Float)) -> (Float, Float, Float) { + return (lhs.0 + rhs.0, lhs.1 + rhs.1, lhs.2 + rhs.2) +} + +func - (lhs: (Float, Float, Float), rhs: (Float, Float, Float)) -> (Float, Float, Float) { + return (lhs.0 - rhs.0, lhs.1 - rhs.1, lhs.2 - rhs.2) +} + +func * (lhs: (Float, Float, Float), rhs: (Float, Float, Float)) -> (Float, Float, Float) { + return (lhs.0 * rhs.0, lhs.1 * rhs.1, lhs.2 * rhs.2) +} + +func * (lhs: (Float, Float, Float), rhs: Float) -> (Float, Float, Float) { + return (lhs.0 * rhs, lhs.1 * rhs, lhs.2 * rhs) +} + +func * (lhs: Float, rhs: (Float, Float, Float)) -> (Float, Float, Float) { + return (lhs * rhs.0, lhs * rhs.1, lhs * rhs.2) +} + +func / (lhs: (Float, Float, Float), rhs: (Float, Float, Float)) -> (Float, Float, Float) { + return (lhs.0 / rhs.0, lhs.1 / rhs.1, lhs.2 / rhs.2) +} + +func / (lhs: (Float, Float, Float), rhs: Float) -> (Float, Float, Float) { + return (lhs.0 / rhs, lhs.1 / rhs, lhs.2 / rhs) +} + +func += (lhs: inout (Float, Float, Float), rhs: (Float, Float, Float)) { + lhs = lhs + rhs +} + +func -= (lhs: inout (Float, Float, Float), rhs: (Float, Float, Float)) { + lhs = lhs - rhs +} + +func *= (lhs: inout (Float, Float, Float), rhs: Float) { + lhs = lhs * rhs +} + +func /= (lhs: inout (Float, Float, Float), rhs: Float) { + lhs = lhs / rhs +} + +func min(_ a: (Float, Float, Float), _ b: (Float, Float, Float)) -> (Float, Float, Float) { + return (min(a.0, b.0), min(a.1, b.1), min(a.2, b.2)) +} + +func max(_ a: (Float, Float, Float), _ b: (Float, Float, Float)) -> (Float, Float, Float) { + return (max(a.0, b.0), max(a.1, b.1), max(a.2, b.2)) +} diff --git a/Shared/Coordinators/BasicAppSettingsCoordinator.swift b/Shared/Coordinators/BasicAppSettingsCoordinator.swift index 1033a87d..d08c0222 100644 --- a/Shared/Coordinators/BasicAppSettingsCoordinator.swift +++ b/Shared/Coordinators/BasicAppSettingsCoordinator.swift @@ -21,7 +21,7 @@ final class BasicAppSettingsCoordinator: NavigationCoordinatable { @ViewBuilder func makeAbout() -> some View { - AboutView() + AboutAppView() } @ViewBuilder diff --git a/Shared/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift index e4501fcf..0e77d30e 100644 --- a/Shared/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -19,14 +19,18 @@ final class HomeCoordinator: NavigationCoordinatable { var start = makeStart @Route(.modal) var settings = makeSettings - @Route(.push) - var library = makeLibrary - @Route(.push) - var item = makeItem - @Route(.modal) - var modalItem = makeModalItem - @Route(.modal) - var modalLibrary = makeModalLibrary + + #if os(tvOS) + @Route(.modal) + var item = makeModalItem + @Route(.modal) + var library = makeModalLibrary + #else + @Route(.push) + var item = makeItem + @Route(.push) + var library = makeLibrary + #endif func makeSettings() -> NavigationViewCoordinator { NavigationViewCoordinator(SettingsCoordinator()) @@ -50,6 +54,6 @@ final class HomeCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { - HomeView() + HomeView(viewModel: .init()) } } diff --git a/Shared/Coordinators/ItemCoordinator.swift b/Shared/Coordinators/ItemCoordinator.swift index a68679fd..147f9c05 100644 --- a/Shared/Coordinators/ItemCoordinator.swift +++ b/Shared/Coordinators/ItemCoordinator.swift @@ -44,12 +44,16 @@ final class ItemCoordinator: NavigationCoordinatable { NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto)) } + func makeSeason(item: BaseItemDto) -> ItemCoordinator { + ItemCoordinator(item: item) + } + func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator { NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel)) } @ViewBuilder func makeStart() -> some View { - ItemNavigationView(item: itemDto) + ItemView(item: itemDto) } } diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index 5dd3a52b..2c637206 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -23,10 +23,14 @@ final class LibraryCoordinator: NavigationCoordinatable { var search = makeSearch @Route(.modal) var filter = makeFilter - @Route(.push) - var item = makeItem - @Route(.modal) - var modalItem = makeModalItem + + #if os(tvOS) + @Route(.modal) + var item = makeModalItem + #else + @Route(.push) + var item = makeItem + #endif let viewModel: LibraryViewModel let title: String diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index 7b08685e..3d75241b 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -63,7 +63,7 @@ final class SettingsCoordinator: NavigationCoordinatable { @ViewBuilder func makeAbout() -> some View { - AboutView() + AboutAppView() } #if !os(tvOS) diff --git a/Shared/Extensions/BlurHashDecode.swift b/Shared/Extensions/BlurHashDecode.swift deleted file mode 100644 index 925c8b21..00000000 --- a/Shared/Extensions/BlurHashDecode.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import UIKit - -// https://github.com/woltapp/blurhash/tree/master/Swift - -public extension UIImage { - convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { - guard blurHash.count >= 6 else { return nil } - - let sizeFlag = String(blurHash[0]).decode83() - let numY = (sizeFlag / 9) + 1 - let numX = (sizeFlag % 9) + 1 - - let quantisedMaximumValue = String(blurHash[1]).decode83() - let maximumValue = Float(quantisedMaximumValue + 1) / 166 - - guard blurHash.count == 4 + 2 * numX * numY else { return nil } - - let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in - if i == 0 { - let value = String(blurHash[2 ..< 6]).decode83() - return decodeDC(value) - } else { - let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() - return decodeAC(value, maximumValue: maximumValue * punch) - } - } - - let width = Int(size.width) - let height = Int(size.height) - let bytesPerRow = width * 3 - guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } - CFDataSetLength(data, bytesPerRow * height) - guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } - - for y in 0 ..< height { - for x in 0 ..< width { - var r: Float = 0 - var g: Float = 0 - var b: Float = 0 - - for j in 0 ..< numY { - for i in 0 ..< numX { - let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) - let colour = colours[i + j * numX] - r += colour.0 * basis - g += colour.1 * basis - b += colour.2 * basis - } - } - - let intR = UInt8(linearTosRGB(r)) - let intG = UInt8(linearTosRGB(g)) - let intB = UInt8(linearTosRGB(b)) - - pixels[3 * x + 0 + y * bytesPerRow] = intR - pixels[3 * x + 1 + y * bytesPerRow] = intG - pixels[3 * x + 2 + y * bytesPerRow] = intB - } - } - - let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) - - guard let provider = CGDataProvider(data: data) else { return nil } - guard let cgImage = CGImage( - width: width, - height: height, - bitsPerComponent: 8, - bitsPerPixel: 24, - bytesPerRow: bytesPerRow, - space: CGColorSpaceCreateDeviceRGB(), - bitmapInfo: bitmapInfo, - provider: provider, - decode: nil, - shouldInterpolate: true, - intent: .defaultIntent - ) else { return nil } - - self.init(cgImage: cgImage) - } -} - -private func decodeDC(_ value: Int) -> (Float, Float, Float) { - let intR = value >> 16 - let intG = (value >> 8) & 255 - let intB = value & 255 - return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) -} - -private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { - let quantR = value / (19 * 19) - let quantG = (value / 19) % 19 - let quantB = value % 19 - - let rgb = ( - signPow((Float(quantR) - 9) / 9, 2) * maximumValue, - signPow((Float(quantG) - 9) / 9, 2) * maximumValue, - signPow((Float(quantB) - 9) / 9, 2) * maximumValue - ) - - return rgb -} - -private func signPow(_ value: Float, _ exp: Float) -> Float { - copysign(pow(abs(value), exp), value) -} - -private func linearTosRGB(_ value: Float) -> Int { - let v = max(0, min(1, value)) - if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } -} - -private func sRGBToLinear(_ value: Type) -> Float { - let v = Float(Int64(value)) / 255 - if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) } -} - -private let encodeCharacters: [String] = { - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } -}() - -private let decodeCharacters: [String: Int] = { - var dict: [String: Int] = [:] - for (index, character) in encodeCharacters.enumerated() { - dict[character] = index - } - return dict -}() - -extension String { - func decode83() -> Int { - var value: Int = 0 - for character in self { - if let digit = decodeCharacters[String(character)] { - value = value * 83 + digit - } - } - return value - } -} - -private extension String { - subscript(offset: Int) -> Character { - self[index(startIndex, offsetBy: offset)] - } - - subscript(bounds: CountableClosedRange) -> Substring { - let start = index(startIndex, offsetBy: bounds.lowerBound) - let end = index(startIndex, offsetBy: bounds.upperBound) - return self[start ... end] - } - - subscript(bounds: CountableRange) -> Substring { - let start = index(startIndex, offsetBy: bounds.lowerBound) - let end = index(startIndex, offsetBy: bounds.upperBound) - return self[start ..< end] - } -} diff --git a/Shared/Extensions/ColorExtension.swift b/Shared/Extensions/ColorExtension.swift index 558aee22..6bbab868 100644 --- a/Shared/Extensions/ColorExtension.swift +++ b/Shared/Extensions/ColorExtension.swift @@ -20,8 +20,8 @@ public extension Color { #else static let systemFill = Color(UIColor.systemFill) static let systemBackground = Color(UIColor.systemBackground) - static let secondarySystemFill = Color(UIColor.secondarySystemBackground) - static let tertiarySystemFill = Color(UIColor.tertiarySystemBackground) + static let secondarySystemFill = Color(UIColor.secondarySystemFill) + static let tertiarySystemFill = Color(UIColor.tertiarySystemFill) #endif } diff --git a/Shared/Extensions/Defaults+Workaround.swift b/Shared/Extensions/Defaults+Workaround.swift old mode 100644 new mode 100755 diff --git a/Shared/Extensions/FontExtensions.swift b/Shared/Extensions/FontExtensions.swift new file mode 100644 index 00000000..d8c2670c --- /dev/null +++ b/Shared/Extensions/FontExtensions.swift @@ -0,0 +1,42 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension Font { + func toUIFont() -> UIFont { + switch self { + #if !os(tvOS) + case .largeTitle: + return UIFont.preferredFont(forTextStyle: .largeTitle) + #endif + case .title: + return UIFont.preferredFont(forTextStyle: .title1) + case .title2: + return UIFont.preferredFont(forTextStyle: .title2) + case .title3: + return UIFont.preferredFont(forTextStyle: .title3) + case .headline: + return UIFont.preferredFont(forTextStyle: .headline) + case .subheadline: + return UIFont.preferredFont(forTextStyle: .subheadline) + case .callout: + return UIFont.preferredFont(forTextStyle: .callout) + case .caption: + return UIFont.preferredFont(forTextStyle: .caption1) + case .caption2: + return UIFont.preferredFont(forTextStyle: .caption2) + case .footnote: + return UIFont.preferredFont(forTextStyle: .footnote) + case .body: + return UIFont.preferredFont(forTextStyle: .body) + default: + return UIFont.preferredFont(forTextStyle: .body) + } + } +} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift new file mode 100644 index 00000000..da8c7bd7 --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift @@ -0,0 +1,91 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI +import UIKit + +extension BaseItemDto { + + // MARK: Item Images + + func imageURL( + _ type: ImageType, + maxWidth: Int + ) -> URL { + _imageURL(type, maxWidth: maxWidth, itemID: id ?? "") + } + + func imageURL( + _ type: ImageType, + maxWidth: CGFloat + ) -> URL { + _imageURL(type, maxWidth: Int(maxWidth), itemID: id ?? "") + } + + func blurHash(_ type: ImageType) -> String? { + guard type != .logo else { return nil } + if let tag = imageTags?[type.rawValue], let taggedBlurHash = imageBlurHashes?[type]?[tag] { + return taggedBlurHash + } else if let firstBlurHash = imageBlurHashes?[type]?.values.first { + return firstBlurHash + } + + return nil + } + + func imageSource(_ type: ImageType, maxWidth: Int) -> ImageSource { + _imageSource(type, maxWidth: maxWidth) + } + + func imageSource(_ type: ImageType, maxWidth: CGFloat) -> ImageSource { + _imageSource(type, maxWidth: Int(maxWidth)) + } + + // MARK: Series Images + + func seriesImageURL(_ type: ImageType, maxWidth: Int) -> URL { + _imageURL(type, maxWidth: maxWidth, itemID: seriesId ?? "") + } + + func seriesImageURL(_ type: ImageType, maxWidth: CGFloat) -> URL { + _imageURL(type, maxWidth: Int(maxWidth), itemID: seriesId ?? "") + } + + func seriesImageSource(_ type: ImageType, maxWidth: Int) -> ImageSource { + let url = _imageURL(type, maxWidth: maxWidth, itemID: seriesId ?? "") + return ImageSource(url: url, blurHash: nil) + } + + func seriesImageSource(_ type: ImageType, maxWidth: CGFloat) -> ImageSource { + seriesImageSource(type, maxWidth: Int(maxWidth)) + } + + // MARK: Fileprivate + + fileprivate func _imageURL( + _ type: ImageType, + maxWidth: Int, + itemID: String + ) -> URL { + let scaleWidth = UIScreen.main.scale(maxWidth) + let tag = imageTags?[type.rawValue] + return ImageAPI.getItemImageWithRequestBuilder( + itemId: itemID, + imageType: type, + maxWidth: scaleWidth, + tag: tag + ).url + } + + fileprivate func _imageSource(_ type: ImageType, maxWidth: Int) -> ImageSource { + let url = _imageURL(type, maxWidth: maxWidth, itemID: id ?? "") + let blurHash = blurHash(type) + return ImageSource(url: url, blurHash: blurHash) + } +} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Poster.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Poster.swift new file mode 100644 index 00000000..542c80a2 --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Poster.swift @@ -0,0 +1,74 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation +import JellyfinAPI +import UIKit + +// MARK: PortraitPoster + +extension BaseItemDto: PortraitPoster { + + var title: String { + switch type { + case .episode: + return seriesName ?? displayName + default: + return displayName + } + } + + var subtitle: String? { + switch type { + case .episode: + return seasonEpisodeLocator + default: + return nil + } + } + + var showTitle: Bool { + switch type { + case .episode, .series, .movie, .boxSet: + return Defaults[.showPosterLabels] + default: + return true + } + } + + func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource { + switch type { + case .episode: + return seriesImageSource(.primary, maxWidth: maxWidth) + default: + return imageSource(.primary, maxWidth: maxWidth) + } + } +} + +// MARK: LandscapePoster + +extension BaseItemDto { + func landscapePosterImageSources(maxWidth: CGFloat) -> [ImageSource] { + switch type { + case .episode: + // TODO: Set episode image preference based on defaults + return [ + seriesImageSource(.thumb, maxWidth: maxWidth), + seriesImageSource(.backdrop, maxWidth: maxWidth), + imageSource(.primary, maxWidth: maxWidth), + ] + default: + return [ + imageSource(.thumb, maxWidth: maxWidth), + imageSource(.backdrop, maxWidth: maxWidth), + ] + } + } +} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Stackable.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Stackable.swift deleted file mode 100644 index c713e11d..00000000 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Stackable.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import Foundation -import JellyfinAPI - -// MARK: PortraitImageStackable - -extension BaseItemDto: PortraitImageStackable { - public var portraitImageID: String { - id ?? "no id" - } - - public func imageURLConstructor(maxWidth: Int) -> URL { - switch self.itemType { - case .episode: - return getSeriesPrimaryImage(maxWidth: maxWidth) - default: - return self.getPrimaryImage(maxWidth: maxWidth) - } - } - - public var title: String { - switch self.itemType { - case .episode: - return self.seriesName ?? self.name ?? "" - default: - return self.name ?? "" - } - } - - public var subtitle: String? { - switch self.itemType { - case .episode: - return getEpisodeLocator() - default: - return nil - } - } - - public var blurHash: String { - self.getPrimaryImageBlurHash() - } - - public var failureInitials: String { - guard let name = self.name else { return "" } - let initials = name.split(separator: " ").compactMap { String($0).first } - return String(initials) - } - - public var showTitle: Bool { - switch self.itemType { - case .episode, .series, .movie, .boxset: - return Defaults[.showPosterLabels] - default: - return true - } - } -} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index 833f3bbd..a58b0d8c 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -125,21 +125,21 @@ extension BaseItemDto { modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams // TODO: other forms of media subtitle - if self.itemType == .episode { - if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { + if self.type == .episode { + if let seriesName = self.seriesName, let episodeLocator = self.episodeLocator { subtitle = "\(seriesName) - \(episodeLocator)" } } let subtitlesEnabled = defaultSubtitleStream != nil - let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode + let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && type == .episode let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay let overlayType = Defaults[.overlayType] - let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode - let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode + let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && type == .episode + let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && type == .episode var fileName: String? if let lastInPath = currentMediaSource.path?.split(separator: "/").last { @@ -155,6 +155,7 @@ extension BaseItemDto { hlsStreamURL: hlsStreamURL, streamType: streamType, response: response, + videoStream: videoStream!, audioStreams: audioStreams, subtitleStreams: subtitleStreams, chapters: modifiedSelfItem.chapters ?? [], @@ -292,21 +293,21 @@ extension BaseItemDto { modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams // TODO: other forms of media subtitle - if self.itemType == .episode { - if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { + if self.type == .episode { + if let seriesName = self.seriesName, let episodeLocator = self.episodeLocator { subtitle = "\(seriesName) - \(episodeLocator)" } } let subtitlesEnabled = defaultSubtitleStream != nil - let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode + let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && type == .episode let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay let overlayType = Defaults[.overlayType] - let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode - let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode + let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && type == .episode + let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && type == .episode var fileName: String? if let lastInPath = currentMediaSource.path?.split(separator: "/").last { @@ -322,6 +323,7 @@ extension BaseItemDto { hlsStreamURL: hlsStreamURL, streamType: streamType, response: response, + videoStream: videoStream!, audioStreams: audioStreams, subtitleStreams: subtitleStreams, chapters: modifiedSelfItem.chapters ?? [], diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index fb786b9f..c3fe1018 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -10,175 +10,22 @@ import Foundation import JellyfinAPI import UIKit -// 001fC^ = dark grey plain blurhash +extension BaseItemDto: Identifiable {} -public extension BaseItemDto { - // MARK: Images +extension BaseItemDto { - func getSeriesBackdropImageBlurHash() -> String { - let imgURL = getSeriesBackdropImage(maxWidth: 1) - guard let imgTag = imgURL.queryParameters?["tag"], - let hash = imageBlurHashes?.backdrop?[imgTag] - else { - return "001fC^" - } - - return hash + var episodeLocator: String? { + guard let episodeNo = indexNumber else { return nil } + return L10n.episodeNumber(episodeNo) } - func getSeriesPrimaryImageBlurHash() -> String { - let imgURL = getSeriesPrimaryImage(maxWidth: 1) - guard let imgTag = imgURL.queryParameters?["tag"], - let hash = imageBlurHashes?.primary?[imgTag] - else { - return "001fC^" - } - - return hash - } - - func getPrimaryImageBlurHash() -> String { - let imgURL = getPrimaryImage(maxWidth: 1) - guard let imgTag = imgURL.queryParameters?["tag"], - let hash = imageBlurHashes?.primary?[imgTag] - else { - return "001fC^" - } - - return hash - } - - func getBackdropImageBlurHash() -> String { - let imgURL = getBackdropImage(maxWidth: 1) - guard let imgTag = imgURL.queryParameters?["tag"] else { - return "001fC^" - } - - if imgURL.queryParameters?[ImageType.backdrop.rawValue] == nil { - if itemType == .episode { - return imageBlurHashes?.backdrop?.values.first ?? "001fC^" - } else { - return imageBlurHashes?.backdrop?[imgTag] ?? "001fC^" - } - } else { - return imageBlurHashes?.primary?[imgTag] ?? "001fC^" - } - } - - func getBackdropImage(maxWidth: Int) -> URL { - var imageType = ImageType.backdrop - var imageTag: String? - var imageItemId = id ?? "" - - if primaryImageAspectRatio ?? 0.0 < 1.0 { - if !(backdropImageTags?.isEmpty ?? true) { - imageTag = backdropImageTags?.first - } - } else { - imageType = .primary - imageTag = imageTags?[ImageType.primary.rawValue] ?? "" - } - - if imageTag == nil || imageItemId.isEmpty { - if !(parentBackdropImageTags?.isEmpty ?? true) { - imageTag = parentBackdropImageTags?.first - imageItemId = parentBackdropItemId ?? "" - } - } - - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - - let urlString = ImageAPI.getItemImageWithRequestBuilder( - itemId: imageItemId, - imageType: imageType, - maxWidth: Int(x), - quality: 96, - tag: imageTag - ).URLString - return URL(string: urlString)! - } - - func getThumbImage(maxWidth: Int) -> URL { - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - - let urlString = ImageAPI.getItemImageWithRequestBuilder( - itemId: id ?? "", - imageType: .thumb, - maxWidth: Int(x), - quality: 96 - ).URLString - return URL(string: urlString)! - } - - func getEpisodeLocator() -> String? { + var seasonEpisodeLocator: String? { if let seasonNo = parentIndexNumber, let episodeNo = indexNumber { return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo)) } return nil } - func getSeriesBackdropImage(maxWidth: Int) -> URL { - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = ImageAPI.getItemImageWithRequestBuilder( - itemId: parentBackdropItemId ?? "", - imageType: .backdrop, - maxWidth: Int(x), - quality: 96, - tag: parentBackdropImageTags?.first - ).URLString - return URL(string: urlString)! - } - - func getSeriesPrimaryImage(maxWidth: Int) -> URL { - guard let seriesId = seriesId else { - return getPrimaryImage(maxWidth: maxWidth) - } - - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = ImageAPI.getItemImageWithRequestBuilder( - itemId: seriesId, - imageType: .primary, - maxWidth: Int(x), - quality: 96, - tag: seriesPrimaryImageTag - ).URLString - return URL(string: urlString)! - } - - func getSeriesThumbImage(maxWidth: Int) -> URL { - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = ImageAPI.getItemImageWithRequestBuilder( - itemId: seriesId ?? "", - imageType: .thumb, - maxWidth: Int(x), - quality: 96, - tag: seriesPrimaryImageTag - ).URLString - return URL(string: urlString)! - } - - func getPrimaryImage(maxWidth: Int) -> URL { - let imageType = ImageType.primary - var imageTag = imageTags?[ImageType.primary.rawValue] ?? "" - var imageItemId = id ?? "" - - if imageTag.isEmpty || imageItemId.isEmpty { - imageTag = seriesPrimaryImageTag ?? "" - imageItemId = seriesId ?? "" - } - - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - - let urlString = ImageAPI.getItemImageWithRequestBuilder( - itemId: imageItemId, - imageType: imageType, - maxWidth: Int(x), - quality: 96, - tag: imageTag - ).URLString - return URL(string: urlString)! - } - // MARK: Calculations func getItemRuntime() -> String? { @@ -238,61 +85,8 @@ public extension BaseItemDto { return 0 } - // MARK: ItemType - - enum ItemType: String { - case movie = "Movie" - case season = "Season" - case episode = "Episode" - case series = "Series" - case boxset = "BoxSet" - case collectionFolder = "CollectionFolder" - case folder = "Folder" - case liveTV = "LiveTV" - - case unknown - - var showDetails: Bool { - switch self { - case .season, .series: - return false - default: - return true - } - } - - public init?(rawValue: String) { - let lowerCase = rawValue.lowercased() - switch lowerCase { - case "movie": self = .movie - case "season": self = .season - case "episode": self = .episode - case "series": self = .series - case "boxset": self = .boxset - case "collectionfolder": self = .collectionFolder - case "folder": self = .folder - case "livetv": self = .liveTV - default: self = .unknown - } - } - } - - var itemType: ItemType { - guard let originalType = type, let knownType = ItemType(rawValue: originalType.rawValue) else { return .unknown } - return knownType - } - - // MARK: PortraitHeaderViewURL - - func portraitHeaderViewURL(maxWidth: Int) -> URL { - switch itemType { - case .movie, .season, .series, .boxset, .collectionFolder, .folder, .liveTV: - return getPrimaryImage(maxWidth: maxWidth) - case .episode: - return getSeriesPrimaryImage(maxWidth: maxWidth) - case .unknown: - return getPrimaryImage(maxWidth: maxWidth) - } + var displayName: String { + name ?? "--" } // MARK: ItemDetail @@ -329,13 +123,13 @@ public extension BaseItemDto { if !audioStreams.isEmpty { let audioList = audioStreams.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" } - .joined(separator: ", ") + .joined(separator: "\n") mediaItems.append(ItemDetail(title: L10n.audio, content: audioList)) } if !subtitleStreams.isEmpty { let subtitleList = subtitleStreams.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" } - .joined(separator: ", ") + .joined(separator: "\n") mediaItems.append(ItemDetail(title: L10n.subtitles, content: subtitleList)) } } @@ -343,6 +137,14 @@ public extension BaseItemDto { return mediaItems } + var subtitleStreams: [MediaStream] { + mediaStreams?.filter { $0.type == .subtitle } ?? [] + } + + var audioStreams: [MediaStream] { + mediaStreams?.filter { $0.type == .audio } ?? [] + } + // MARK: Missing and Unaired var missing: Bool { @@ -370,6 +172,13 @@ public extension BaseItemDto { return dateFormatter.string(from: premiereDate) } + var premiereDateYear: String? { + guard let premiereDate = premiereDate else { return nil } + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "YYYY" + return dateFormatter.string(from: premiereDate) + } + // MARK: Chapter Images func getChapterImage(maxWidth: Int) -> [URL] { @@ -389,4 +198,52 @@ public extension BaseItemDto { return chapterImageURLs } + + // TODO: Don't use spoof objects as a placeholder or no results + + static var placeHolder: BaseItemDto { + .init( + name: "Placeholder", + id: "1", + overview: String(repeating: "a", count: 100), + indexNumber: 20 + ) + } + + static var noResults: BaseItemDto { + .init(name: L10n.noResults) + } +} + +extension BaseItemDtoImageBlurHashes { + subscript(imageType: ImageType) -> [String: String]? { + switch imageType { + case .primary: + return primary + case .art: + return art + case .backdrop: + return backdrop + case .banner: + return banner + case .logo: + return logo + case .thumb: + return thumb + case .disc: + return disc + case .box: + return box + case .screenshot: + return screenshot + case .menu: + return menu + case .chapter: + return chapter + case .boxRear: + return boxRear + case .profile: + return profile + } + } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift new file mode 100644 index 00000000..9cebba93 --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift @@ -0,0 +1,46 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI +import UIKit + +// MARK: PortraitImageStackable + +extension BaseItemPerson: PortraitPoster { + + var title: String { + self.name ?? "--" + } + + var subtitle: String? { + self.firstRole + } + + var showTitle: Bool { + true + } + + func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource { + let scaleWidth = UIScreen.main.scale(maxWidth) + let url = ImageAPI.getItemImageWithRequestBuilder( + itemId: id ?? "", + imageType: .primary, + maxWidth: scaleWidth, + tag: primaryImageTag + ).url + + var blurHash: String? + + if let tag = primaryImageTag, let taggedBlurHash = imageBlurHashes?.primary?[tag] { + blurHash = taggedBlurHash + } + + return ImageSource(url: url, blurHash: blurHash) + } +} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift index 338840d5..68395287 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift @@ -12,39 +12,13 @@ import UIKit extension BaseItemPerson { - // MARK: Get Image - - func getImage(baseURL: String, maxWidth: Int) -> URL { - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - - let urlString = ImageAPI.getItemImageWithRequestBuilder( - itemId: id ?? "", - imageType: .primary, - maxWidth: Int(x), - quality: 96, - tag: primaryImageTag - ).URLString - return URL(string: urlString)! - } - - func getBlurHash() -> String { - let imgURL = getImage(baseURL: "", maxWidth: 1) - guard let imgTag = imgURL.queryParameters?["tag"], - let hash = imageBlurHashes?.primary?[imgTag] - else { - return "001fC^" - } - - return hash - } - // MARK: First Role // Jellyfin will grab all roles the person played in the show which makes the role // text too long. This will grab the first role which: // - assumes that the most important role is the first // - will also grab the last "()" instance, like "(voice)" - func firstRole() -> String? { + var firstRole: String? { guard let role = self.role else { return nil } let split = role.split(separator: "/") guard split.count > 1 else { return role } @@ -61,56 +35,18 @@ extension BaseItemPerson { return final } -} - -// MARK: PortraitImageStackable - -extension BaseItemPerson: PortraitImageStackable { - public var portraitImageID: String { - (id ?? "noid") + title + (subtitle ?? "nodescription") + blurHash + failureInitials - } - - public func imageURLConstructor(maxWidth: Int) -> URL { - self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth) - } - - public var title: String { - self.name ?? "" - } - - public var subtitle: String? { - self.firstRole() - } - - public var blurHash: String { - self.getBlurHash() - } - - public var failureInitials: String { - guard let name = self.name else { return "" } - let initials = name.split(separator: " ").compactMap { String($0).first } - return String(initials) - } - - public var showTitle: Bool { - true - } -} - -// MARK: DiplayedType - -extension BaseItemPerson { // Only displayed person types. - // Will ignore people like "GuestStar" - enum DisplayedType: String, CaseIterable { + // Will ignore types like "GuestStar" + enum DisplayedType: String { case actor = "Actor" case director = "Director" case writer = "Writer" case producer = "Producer" + } - static var allCasesRaw: [String] { - self.allCases.map(\.rawValue) - } + var isDisplayed: Bool { + guard let type = type else { return false } + return DisplayedType(rawValue: type) != nil } } diff --git a/Shared/Extensions/ViewExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/RequestBuilderExtensions.swift similarity index 71% rename from Shared/Extensions/ViewExtensions.swift rename to Shared/Extensions/JellyfinAPIExtensions/RequestBuilderExtensions.swift index a636b664..6c76bfa5 100644 --- a/Shared/Extensions/ViewExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/RequestBuilderExtensions.swift @@ -7,10 +7,10 @@ // import Foundation -import SwiftUI +import JellyfinAPI -extension View { - func eraseToAnyView() -> AnyView { - AnyView(self) +extension RequestBuilder where T == URL { + var url: URL { + URL(string: URLString)! } } diff --git a/Shared/Extensions/StringExtensions.swift b/Shared/Extensions/StringExtensions.swift index 00bd1755..d71d04d4 100644 --- a/Shared/Extensions/StringExtensions.swift +++ b/Shared/Extensions/StringExtensions.swift @@ -37,4 +37,9 @@ extension String { var text: Text { Text(self) } + + var initials: String { + let initials = self.split(separator: " ").compactMap(\.first) + return String(initials) + } } diff --git a/Shared/Extensions/UIDeviceExtensions.swift b/Shared/Extensions/UIDeviceExtensions.swift index 40467175..8ccea68a 100644 --- a/Shared/Extensions/UIDeviceExtensions.swift +++ b/Shared/Extensions/UIDeviceExtensions.swift @@ -12,4 +12,32 @@ extension UIDevice { static var vendorUUIDString: String { current.identifierForVendor!.uuidString } + + static var isIPad: Bool { + UIDevice.current.userInterfaceIdiom == .pad + } + + static var isPhone: Bool { + UIDevice.current.userInterfaceIdiom == .phone + } + + #if os(iOS) + static var isPortrait: Bool { + UIDevice.current.orientation.isPortrait + } + + static var isLandscape: Bool { + isIPad || UIDevice.current.orientation.isLandscape + } + + static func feedback(_ type: UINotificationFeedbackGenerator.FeedbackType) { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(type) + } + + static func impact(_ type: UIImpactFeedbackGenerator.FeedbackStyle) { + let generator = UIImpactFeedbackGenerator(style: type) + generator.impactOccurred() + } + #endif } diff --git a/WidgetExtension/JellyfinWidget.swift b/Shared/Extensions/UIScreenExtensions.swift similarity index 60% rename from WidgetExtension/JellyfinWidget.swift rename to Shared/Extensions/UIScreenExtensions.swift index 2764028b..beeb5987 100644 --- a/WidgetExtension/JellyfinWidget.swift +++ b/Shared/Extensions/UIScreenExtensions.swift @@ -6,13 +6,14 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // -import SwiftUI -import WidgetKit +import UIKit -@main -struct JellyfinWidgetBundle: WidgetBundle { - @WidgetBundleBuilder - var body: some Widget { - NextUpWidget() +extension UIScreen { + func scale(_ x: Int) -> Int { + Int(nativeScale) * x + } + + func scale(_ x: CGFloat) -> Int { + Int(nativeScale * x) } } diff --git a/Shared/Extensions/UIScrollViewExtensions.swift b/Shared/Extensions/UIScrollViewExtensions.swift new file mode 100644 index 00000000..59651d6f --- /dev/null +++ b/Shared/Extensions/UIScrollViewExtensions.swift @@ -0,0 +1,16 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import UIKit + +extension UIScrollView { + func scrollToTop(animated: Bool = true) { + let desiredOffset = CGPoint(x: 0, y: 0) + setContentOffset(desiredOffset, animated: animated) + } +} diff --git a/Shared/Extensions/ViewExtensions/BackgroundParallaxHeaderModifier.swift b/Shared/Extensions/ViewExtensions/BackgroundParallaxHeaderModifier.swift new file mode 100644 index 00000000..e2d1afce --- /dev/null +++ b/Shared/Extensions/ViewExtensions/BackgroundParallaxHeaderModifier.swift @@ -0,0 +1,40 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct BackgroundParallaxHeaderModifier: ViewModifier { + + @Binding + var scrollViewOffset: CGFloat + + let height: CGFloat + let multiplier: CGFloat + let header: () -> Header + + init( + _ scrollViewOffset: Binding, + height: CGFloat, + multiplier: CGFloat = 1, + @ViewBuilder header: @escaping () -> Header + ) { + self._scrollViewOffset = scrollViewOffset + self.height = height + self.multiplier = multiplier + self.header = header + } + + func body(content: Content) -> some View { + content.background(alignment: .top) { + header() + .offset(y: scrollViewOffset > 0 ? -scrollViewOffset * multiplier : 0) + .scaleEffect(scrollViewOffset < 0 ? (height - scrollViewOffset) / height : 1, anchor: .top) + .ignoresSafeArea() + } + } +} diff --git a/Shared/Extensions/ViewExtensions/BottomEdgeGradientModifier.swift b/Shared/Extensions/ViewExtensions/BottomEdgeGradientModifier.swift new file mode 100644 index 00000000..0f6934da --- /dev/null +++ b/Shared/Extensions/ViewExtensions/BottomEdgeGradientModifier.swift @@ -0,0 +1,35 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct BottomEdgeGradientModifier: ViewModifier { + + let bottomColor: Color + + func body(content: Content) -> some View { + VStack(spacing: 0) { + content + .overlay { + bottomColor + .mask { + LinearGradient( + stops: [ + .init(color: .clear, location: 0.8), + .init(color: .white, location: 0.95), + ], + startPoint: .top, + endPoint: .bottom + ) + } + } + + bottomColor + } + } +} diff --git a/Shared/Extensions/ViewExtensions/ScrollViewOffsetModifier.swift b/Shared/Extensions/ViewExtensions/ScrollViewOffsetModifier.swift new file mode 100644 index 00000000..6ae59916 --- /dev/null +++ b/Shared/Extensions/ViewExtensions/ScrollViewOffsetModifier.swift @@ -0,0 +1,39 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Introspect +import SwiftUI + +struct ScrollViewOffsetModifier: ViewModifier { + + @Binding + var scrollViewOffset: CGFloat + + private let scrollViewDelegate: ScrollViewDelegate? + + init(scrollViewOffset: Binding) { + self._scrollViewOffset = scrollViewOffset + self.scrollViewDelegate = ScrollViewDelegate() + self.scrollViewDelegate?.parent = self + } + + func body(content: Content) -> some View { + content.introspectScrollView { scrollView in + scrollView.delegate = scrollViewDelegate + } + } + + private class ScrollViewDelegate: NSObject, UIScrollViewDelegate { + + var parent: ScrollViewOffsetModifier? + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + parent?.scrollViewOffset = scrollView.contentOffset.y + } + } +} diff --git a/Shared/Extensions/ViewExtensions/ViewExtensions.swift b/Shared/Extensions/ViewExtensions/ViewExtensions.swift new file mode 100644 index 00000000..09822496 --- /dev/null +++ b/Shared/Extensions/ViewExtensions/ViewExtensions.swift @@ -0,0 +1,77 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import SwiftUI + +extension View { + @inlinable + func eraseToAnyView() -> AnyView { + AnyView(self) + } + + public func inverseMask(_ mask: M) -> some View { + // exchange foreground and background + let inversed = mask + .foregroundColor(.black) // hide foreground + .background(Color.white) // let the background stand out + .compositingGroup() + .luminanceToAlpha() + return self.mask(inversed) + } + + // From: https://www.avanderlee.com/swiftui/conditional-view-modifier/ + @ViewBuilder + @inlinable + func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } + + @ViewBuilder + @inlinable + func `if`(_ condition: Bool, transformIf: (Self) -> Content, transformElse: (Self) -> Content) -> some View { + if condition { + transformIf(self) + } else { + transformElse(self) + } + } + + /// Applies Portrait Poster frame with proper corner radius ratio against the width + func portraitPoster(width: CGFloat) -> some View { + self.frame(width: width, height: width * 1.5) + .cornerRadius((width * 1.5) / 40) + } + + @inlinable + func padding2(_ edges: Edge.Set = .all) -> some View { + self.padding(edges) + .padding(edges) + } + + func scrollViewOffset(_ scrollViewOffset: Binding) -> some View { + self.modifier(ScrollViewOffsetModifier(scrollViewOffset: scrollViewOffset)) + } + + func backgroundParallaxHeader( + _ scrollViewOffset: Binding, + height: CGFloat, + multiplier: CGFloat = 1, + @ViewBuilder header: @escaping () -> Header + ) -> some View { + self.modifier(BackgroundParallaxHeaderModifier(scrollViewOffset, height: height, multiplier: multiplier, header: header)) + } + + func bottomEdgeGradient(bottomColor: Color) -> some View { + self.modifier(BottomEdgeGradientModifier(bottomColor: bottomColor)) + } +} diff --git a/Shared/Generated/LocalizedLookup.swift b/Shared/Generated/LocalizedLookup.swift deleted file mode 100644 index e42cc58b..00000000 --- a/Shared/Generated/LocalizedLookup.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation - -class TranslationService { - - static let shared = TranslationService() - - func lookupTranslation(forKey key: String, inTable table: String) -> String { - - let expectedValue = Bundle.main.localizedString(forKey: key, value: nil, table: table) - - if expectedValue == key || NSLocale.preferredLanguages.first == "en" { - guard let path = Bundle.main.path(forResource: "en", ofType: "lproj"), - let bundle = Bundle(path: path) else { return expectedValue } - - return NSLocalizedString(key, bundle: bundle, comment: "") - } else { - return expectedValue - } - } -} diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift deleted file mode 100644 index 29c82ada..00000000 --- a/Shared/Generated/Strings.swift +++ /dev/null @@ -1,466 +0,0 @@ -// swiftlint:disable all -// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen - -import Foundation - -// swiftlint:disable superfluous_disable_command file_length implicit_return - -// MARK: - Strings - -// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length -// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces -internal enum L10n { - /// About - internal static var about: String { return L10n.tr("Localizable", "about") } - /// Accessibility - internal static var accessibility: String { return L10n.tr("Localizable", "accessibility") } - /// Add URL - internal static var addURL: String { return L10n.tr("Localizable", "addURL") } - /// Airs %s - internal static func airWithDate(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "airWithDate", p1) - } - /// All Genres - internal static var allGenres: String { return L10n.tr("Localizable", "allGenres") } - /// All Media - internal static var allMedia: String { return L10n.tr("Localizable", "allMedia") } - /// Appearance - internal static var appearance: String { return L10n.tr("Localizable", "appearance") } - /// Apply - internal static var apply: String { return L10n.tr("Localizable", "apply") } - /// Audio - internal static var audio: String { return L10n.tr("Localizable", "audio") } - /// Audio & Captions - internal static var audioAndCaptions: String { return L10n.tr("Localizable", "audioAndCaptions") } - /// Audio Track - internal static var audioTrack: String { return L10n.tr("Localizable", "audioTrack") } - /// Authorize - internal static var authorize: String { return L10n.tr("Localizable", "authorize") } - /// Auto Play - internal static var autoPlay: String { return L10n.tr("Localizable", "autoPlay") } - /// Back - internal static var back: String { return L10n.tr("Localizable", "back") } - /// Cancel - internal static var cancel: String { return L10n.tr("Localizable", "cancel") } - /// Cannot connect to host - internal static var cannotConnectToHost: String { return L10n.tr("Localizable", "cannotConnectToHost") } - /// CAST - internal static var cast: String { return L10n.tr("Localizable", "cast") } - /// Cast & Crew - internal static var castAndCrew: String { return L10n.tr("Localizable", "castAndCrew") } - /// Change Server - internal static var changeServer: String { return L10n.tr("Localizable", "changeServer") } - /// Channels - internal static var channels: String { return L10n.tr("Localizable", "channels") } - /// Chapters - internal static var chapters: String { return L10n.tr("Localizable", "chapters") } - /// Cinematic Views - internal static var cinematicViews: String { return L10n.tr("Localizable", "cinematicViews") } - /// Close - internal static var close: String { return L10n.tr("Localizable", "close") } - /// Closed Captions - internal static var closedCaptions: String { return L10n.tr("Localizable", "closedCaptions") } - /// Compact - internal static var compact: String { return L10n.tr("Localizable", "compact") } - /// Confirm Close - internal static var confirmClose: String { return L10n.tr("Localizable", "confirmClose") } - /// Connect - internal static var connect: String { return L10n.tr("Localizable", "connect") } - /// Connect Manually - internal static var connectManually: String { return L10n.tr("Localizable", "connectManually") } - /// Connect to Jellyfin - internal static var connectToJellyfin: String { return L10n.tr("Localizable", "connectToJellyfin") } - /// Connect to a Jellyfin server - internal static var connectToJellyfinServer: String { return L10n.tr("Localizable", "connectToJellyfinServer") } - /// Connect to a Jellyfin server to get started - internal static var connectToJellyfinServerStart: String { return L10n.tr("Localizable", "connectToJellyfinServerStart") } - /// Connect to Server - internal static var connectToServer: String { return L10n.tr("Localizable", "connectToServer") } - /// Containers - internal static var containers: String { return L10n.tr("Localizable", "containers") } - /// Continue - internal static var `continue`: String { return L10n.tr("Localizable", "continue") } - /// Continue Watching - internal static var continueWatching: String { return L10n.tr("Localizable", "continueWatching") } - /// Current Position - internal static var currentPosition: String { return L10n.tr("Localizable", "currentPosition") } - /// Customize - internal static var customize: String { return L10n.tr("Localizable", "customize") } - /// Dark - internal static var dark: String { return L10n.tr("Localizable", "dark") } - /// Default Scheme - internal static var defaultScheme: String { return L10n.tr("Localizable", "defaultScheme") } - /// DIRECTOR - internal static var director: String { return L10n.tr("Localizable", "director") } - /// Discovered Servers - internal static var discoveredServers: String { return L10n.tr("Localizable", "discoveredServers") } - /// Display order - internal static var displayOrder: String { return L10n.tr("Localizable", "displayOrder") } - /// Edit Jump Lengths - internal static var editJumpLengths: String { return L10n.tr("Localizable", "editJumpLengths") } - /// Empty Next Up - internal static var emptyNextUp: String { return L10n.tr("Localizable", "emptyNextUp") } - /// Episodes - internal static var episodes: String { return L10n.tr("Localizable", "episodes") } - /// Error - internal static var error: String { return L10n.tr("Localizable", "error") } - /// Existing Server - internal static var existingServer: String { return L10n.tr("Localizable", "existingServer") } - /// Existing User - internal static var existingUser: String { return L10n.tr("Localizable", "existingUser") } - /// Experimental - internal static var experimental: String { return L10n.tr("Localizable", "experimental") } - /// Favorites - internal static var favorites: String { return L10n.tr("Localizable", "favorites") } - /// File - internal static var file: String { return L10n.tr("Localizable", "file") } - /// Filter Results - internal static var filterResults: String { return L10n.tr("Localizable", "filterResults") } - /// Filters - internal static var filters: String { return L10n.tr("Localizable", "filters") } - /// Genres - internal static var genres: String { return L10n.tr("Localizable", "genres") } - /// Home - internal static var home: String { return L10n.tr("Localizable", "home") } - /// Information - internal static var information: String { return L10n.tr("Localizable", "information") } - /// Items - internal static var items: String { return L10n.tr("Localizable", "items") } - /// Jump Backward - internal static var jumpBackward: String { return L10n.tr("Localizable", "jumpBackward") } - /// Jump Backward Length - internal static var jumpBackwardLength: String { return L10n.tr("Localizable", "jumpBackwardLength") } - /// Jump Forward - internal static var jumpForward: String { return L10n.tr("Localizable", "jumpForward") } - /// Jump Forward Length - internal static var jumpForwardLength: String { return L10n.tr("Localizable", "jumpForwardLength") } - /// Jump Gestures Enabled - internal static var jumpGesturesEnabled: String { return L10n.tr("Localizable", "jumpGesturesEnabled") } - /// %s seconds - internal static func jumpLengthSeconds(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "jumpLengthSeconds", p1) - } - /// Larger - internal static var larger: String { return L10n.tr("Localizable", "larger") } - /// Largest - internal static var largest: String { return L10n.tr("Localizable", "largest") } - /// Latest %@ - internal static func latestWithString(_ p1: Any) -> String { - return L10n.tr("Localizable", "latestWithString", String(describing: p1)) - } - /// Library - internal static var library: String { return L10n.tr("Localizable", "library") } - /// Light - internal static var light: String { return L10n.tr("Localizable", "light") } - /// Loading - internal static var loading: String { return L10n.tr("Localizable", "loading") } - /// Local Servers - internal static var localServers: String { return L10n.tr("Localizable", "localServers") } - /// Login - internal static var login: String { return L10n.tr("Localizable", "login") } - /// Login to %@ - internal static func loginToWithString(_ p1: Any) -> String { - return L10n.tr("Localizable", "loginToWithString", String(describing: p1)) - } - /// Media - internal static var media: String { return L10n.tr("Localizable", "media") } - /// Missing - internal static var missing: String { return L10n.tr("Localizable", "missing") } - /// Missing Items - internal static var missingItems: String { return L10n.tr("Localizable", "missingItems") } - /// More Like This - internal static var moreLikeThis: String { return L10n.tr("Localizable", "moreLikeThis") } - /// Movies - internal static var movies: String { return L10n.tr("Localizable", "movies") } - /// %d users - internal static func multipleUsers(_ p1: Int) -> String { - return L10n.tr("Localizable", "multipleUsers", p1) - } - /// Name - internal static var name: String { return L10n.tr("Localizable", "name") } - /// Networking - internal static var networking: String { return L10n.tr("Localizable", "networking") } - /// Network timed out - internal static var networkTimedOut: String { return L10n.tr("Localizable", "networkTimedOut") } - /// Next - internal static var next: String { return L10n.tr("Localizable", "next") } - /// Next Item - internal static var nextItem: String { return L10n.tr("Localizable", "nextItem") } - /// Next Up - internal static var nextUp: String { return L10n.tr("Localizable", "nextUp") } - /// No Cast devices found.. - internal static var noCastdevicesfound: String { return L10n.tr("Localizable", "noCastdevicesfound") } - /// No Codec - internal static var noCodec: String { return L10n.tr("Localizable", "noCodec") } - /// No episodes available - internal static var noEpisodesAvailable: String { return L10n.tr("Localizable", "noEpisodesAvailable") } - /// No local servers found - internal static var noLocalServersFound: String { return L10n.tr("Localizable", "noLocalServersFound") } - /// None - internal static var `none`: String { return L10n.tr("Localizable", "none") } - /// No overview available - internal static var noOverviewAvailable: String { return L10n.tr("Localizable", "noOverviewAvailable") } - /// No public Users - internal static var noPublicUsers: String { return L10n.tr("Localizable", "noPublicUsers") } - /// No results. - internal static var noResults: String { return L10n.tr("Localizable", "noResults") } - /// Normal - internal static var normal: String { return L10n.tr("Localizable", "normal") } - /// N/A - internal static var notAvailableSlash: String { return L10n.tr("Localizable", "notAvailableSlash") } - /// Type: %@ not implemented yet :( - internal static func notImplementedYetWithType(_ p1: Any) -> String { - return L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1)) - } - /// No title - internal static var noTitle: String { return L10n.tr("Localizable", "noTitle") } - /// Ok - internal static var ok: String { return L10n.tr("Localizable", "ok") } - /// 1 user - internal static var oneUser: String { return L10n.tr("Localizable", "oneUser") } - /// Operating System - internal static var operatingSystem: String { return L10n.tr("Localizable", "operatingSystem") } - /// Other - internal static var other: String { return L10n.tr("Localizable", "other") } - /// Other User - internal static var otherUser: String { return L10n.tr("Localizable", "otherUser") } - /// Overlay - internal static var overlay: String { return L10n.tr("Localizable", "overlay") } - /// Overlay Type - internal static var overlayType: String { return L10n.tr("Localizable", "overlayType") } - /// Overview - internal static var overview: String { return L10n.tr("Localizable", "overview") } - /// Page %1$@ of %2$@ - internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String { - return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2)) - } - /// Password - internal static var password: String { return L10n.tr("Localizable", "password") } - /// Play - internal static var play: String { return L10n.tr("Localizable", "play") } - /// Play / Pause - internal static var playAndPause: String { return L10n.tr("Localizable", "playAndPause") } - /// Playback settings - internal static var playbackSettings: String { return L10n.tr("Localizable", "playbackSettings") } - /// Playback Speed - internal static var playbackSpeed: String { return L10n.tr("Localizable", "playbackSpeed") } - /// Player Gestures Lock Gesture Enabled - internal static var playerGesturesLockGestureEnabled: String { return L10n.tr("Localizable", "playerGesturesLockGestureEnabled") } - /// Play From Beginning - internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") } - /// Play Next - internal static var playNext: String { return L10n.tr("Localizable", "playNext") } - /// Play Next Item - internal static var playNextItem: String { return L10n.tr("Localizable", "playNextItem") } - /// Play Previous Item - internal static var playPreviousItem: String { return L10n.tr("Localizable", "playPreviousItem") } - /// Present - internal static var present: String { return L10n.tr("Localizable", "present") } - /// Press Down for Menu - internal static var pressDownForMenu: String { return L10n.tr("Localizable", "pressDownForMenu") } - /// Previous Item - internal static var previousItem: String { return L10n.tr("Localizable", "previousItem") } - /// Programs - internal static var programs: String { return L10n.tr("Localizable", "programs") } - /// Public Users - internal static var publicUsers: String { return L10n.tr("Localizable", "publicUsers") } - /// Quick Connect - internal static var quickConnect: String { return L10n.tr("Localizable", "quickConnect") } - /// Quick Connect code - internal static var quickConnectCode: String { return L10n.tr("Localizable", "quickConnectCode") } - /// Invalid Quick Connect code - internal static var quickConnectInvalidError: String { return L10n.tr("Localizable", "quickConnectInvalidError") } - /// Note: Quick Connect not enabled - internal static var quickConnectNotEnabled: String { return L10n.tr("Localizable", "quickConnectNotEnabled") } - /// 1. Open the Jellyfin app on your phone or web browser and sign in with your account - internal static var quickConnectStep1: String { return L10n.tr("Localizable", "quickConnectStep1") } - /// 2. Open the user menu and go to the Quick Connect page - internal static var quickConnectStep2: String { return L10n.tr("Localizable", "quickConnectStep2") } - /// 3. Enter the following code: - internal static var quickConnectStep3: String { return L10n.tr("Localizable", "quickConnectStep3") } - /// Authorizing Quick Connect successful. Please continue on your other device. - internal static var quickConnectSuccessMessage: String { return L10n.tr("Localizable", "quickConnectSuccessMessage") } - /// Rated - internal static var rated: String { return L10n.tr("Localizable", "rated") } - /// Recently Added - internal static var recentlyAdded: String { return L10n.tr("Localizable", "recentlyAdded") } - /// Recommended - internal static var recommended: String { return L10n.tr("Localizable", "recommended") } - /// Refresh - internal static var refresh: String { return L10n.tr("Localizable", "refresh") } - /// Regular - internal static var regular: String { return L10n.tr("Localizable", "regular") } - /// Released - internal static var released: String { return L10n.tr("Localizable", "released") } - /// Remaining Time - internal static var remainingTime: String { return L10n.tr("Localizable", "remainingTime") } - /// Remove - internal static var remove: String { return L10n.tr("Localizable", "remove") } - /// Remove All Users - internal static var removeAllUsers: String { return L10n.tr("Localizable", "removeAllUsers") } - /// Remove From Resume - internal static var removeFromResume: String { return L10n.tr("Localizable", "removeFromResume") } - /// Report an Issue - internal static var reportIssue: String { return L10n.tr("Localizable", "reportIssue") } - /// Request a Feature - internal static var requestFeature: String { return L10n.tr("Localizable", "requestFeature") } - /// Reset - internal static var reset: String { return L10n.tr("Localizable", "reset") } - /// Reset App Settings - internal static var resetAppSettings: String { return L10n.tr("Localizable", "resetAppSettings") } - /// Reset User Settings - internal static var resetUserSettings: String { return L10n.tr("Localizable", "resetUserSettings") } - /// Resume 5 Second Offset - internal static var resume5SecondOffset: String { return L10n.tr("Localizable", "resume5SecondOffset") } - /// Retry - internal static var retry: String { return L10n.tr("Localizable", "retry") } - /// Runtime - internal static var runtime: String { return L10n.tr("Localizable", "runtime") } - /// Search - internal static var search: String { return L10n.tr("Localizable", "search") } - /// Search… - internal static var searchDots: String { return L10n.tr("Localizable", "searchDots") } - /// Searching… - internal static var searchingDots: String { return L10n.tr("Localizable", "searchingDots") } - /// Season - internal static var season: String { return L10n.tr("Localizable", "season") } - /// S%1$@:E%2$@ - internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String { - return L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2)) - } - /// Seasons - internal static var seasons: String { return L10n.tr("Localizable", "seasons") } - /// See All - internal static var seeAll: String { return L10n.tr("Localizable", "seeAll") } - /// Seek Slide Gesture Enabled - internal static var seekSlideGestureEnabled: String { return L10n.tr("Localizable", "seekSlideGestureEnabled") } - /// See More - internal static var seeMore: String { return L10n.tr("Localizable", "seeMore") } - /// Select Cast Destination - internal static var selectCastDestination: String { return L10n.tr("Localizable", "selectCastDestination") } - /// Series - internal static var series: String { return L10n.tr("Localizable", "series") } - /// Server - internal static var server: String { return L10n.tr("Localizable", "server") } - /// Server %s is already connected - internal static func serverAlreadyConnected(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "serverAlreadyConnected", p1) - } - /// Server %s already exists. Add new URL? - internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1) - } - /// Server Details - internal static var serverDetails: String { return L10n.tr("Localizable", "serverDetails") } - /// Server Information - internal static var serverInformation: String { return L10n.tr("Localizable", "serverInformation") } - /// Servers - internal static var servers: String { return L10n.tr("Localizable", "servers") } - /// Server URL - internal static var serverURL: String { return L10n.tr("Localizable", "serverURL") } - /// Settings - internal static var settings: String { return L10n.tr("Localizable", "settings") } - /// Show Cast & Crew - internal static var showCastAndCrew: String { return L10n.tr("Localizable", "showCastAndCrew") } - /// Show Chapters Info In Bottom Overlay - internal static var showChaptersInfoInBottomOverlay: String { return L10n.tr("Localizable", "showChaptersInfoInBottomOverlay") } - /// Flatten Library Items - internal static var showFlattenView: String { return L10n.tr("Localizable", "showFlattenView") } - /// Show Missing Episodes - internal static var showMissingEpisodes: String { return L10n.tr("Localizable", "showMissingEpisodes") } - /// Show Missing Seasons - internal static var showMissingSeasons: String { return L10n.tr("Localizable", "showMissingSeasons") } - /// Show Poster Labels - internal static var showPosterLabels: String { return L10n.tr("Localizable", "showPosterLabels") } - /// Signed in as %@ - internal static func signedInAsWithString(_ p1: Any) -> String { - return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1)) - } - /// Sign In - internal static var signIn: String { return L10n.tr("Localizable", "signIn") } - /// Sign in to get started - internal static var signInGetStarted: String { return L10n.tr("Localizable", "signInGetStarted") } - /// Sign In to %s - internal static func signInToServer(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "signInToServer", p1) - } - /// Smaller - internal static var smaller: String { return L10n.tr("Localizable", "smaller") } - /// Smallest - internal static var smallest: String { return L10n.tr("Localizable", "smallest") } - /// Sort by - internal static var sortBy: String { return L10n.tr("Localizable", "sortBy") } - /// Source Code - internal static var sourceCode: String { return L10n.tr("Localizable", "sourceCode") } - /// STUDIO - internal static var studio: String { return L10n.tr("Localizable", "studio") } - /// Studios - internal static var studios: String { return L10n.tr("Localizable", "studios") } - /// Subtitle Font - internal static var subtitleFont: String { return L10n.tr("Localizable", "subtitleFont") } - /// Subtitles - internal static var subtitles: String { return L10n.tr("Localizable", "subtitles") } - /// Subtitle Size - internal static var subtitleSize: String { return L10n.tr("Localizable", "subtitleSize") } - /// Suggestions - internal static var suggestions: String { return L10n.tr("Localizable", "suggestions") } - /// Switch User - internal static var switchUser: String { return L10n.tr("Localizable", "switchUser") } - /// System - internal static var system: String { return L10n.tr("Localizable", "system") } - /// System Control Gestures Enabled - internal static var systemControlGesturesEnabled: String { return L10n.tr("Localizable", "systemControlGesturesEnabled") } - /// Tags - internal static var tags: String { return L10n.tr("Localizable", "tags") } - /// Too Many Redirects - internal static var tooManyRedirects: String { return L10n.tr("Localizable", "tooManyRedirects") } - /// Try again - internal static var tryAgain: String { return L10n.tr("Localizable", "tryAgain") } - /// TV Shows - internal static var tvShows: String { return L10n.tr("Localizable", "tvShows") } - /// Unable to connect to server - internal static var unableToConnectServer: String { return L10n.tr("Localizable", "unableToConnectServer") } - /// Unable to find host - internal static var unableToFindHost: String { return L10n.tr("Localizable", "unableToFindHost") } - /// Unaired - internal static var unaired: String { return L10n.tr("Localizable", "unaired") } - /// Unauthorized - internal static var unauthorized: String { return L10n.tr("Localizable", "unauthorized") } - /// Unauthorized user - internal static var unauthorizedUser: String { return L10n.tr("Localizable", "unauthorizedUser") } - /// Unknown - internal static var unknown: String { return L10n.tr("Localizable", "unknown") } - /// Unknown Error - internal static var unknownError: String { return L10n.tr("Localizable", "unknownError") } - /// URL - internal static var url: String { return L10n.tr("Localizable", "url") } - /// User - internal static var user: String { return L10n.tr("Localizable", "user") } - /// User %s is already signed in - internal static func userAlreadySignedIn(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "userAlreadySignedIn", p1) - } - /// Username - internal static var username: String { return L10n.tr("Localizable", "username") } - /// Version - internal static var version: String { return L10n.tr("Localizable", "version") } - /// Video Player - internal static var videoPlayer: String { return L10n.tr("Localizable", "videoPlayer") } - /// Who's watching? - internal static var whosWatching: String { return L10n.tr("Localizable", "WhosWatching") } - /// WIP - internal static var wip: String { return L10n.tr("Localizable", "wip") } - /// Your Favorites - internal static var yourFavorites: String { return L10n.tr("Localizable", "yourFavorites") } -} -// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length -// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces - -// MARK: - Implementation Details - -extension L10n { - private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - let format = TranslationService.shared.lookupTranslation(forKey:inTable:)(key, table) - return String(format: format, locale: Locale.current, arguments: args) - } -} diff --git a/Shared/Objects/ItemViewType.swift b/Shared/Objects/ItemViewType.swift new file mode 100644 index 00000000..c94a67c4 --- /dev/null +++ b/Shared/Objects/ItemViewType.swift @@ -0,0 +1,27 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation + +enum ItemViewType: String, CaseIterable, Defaults.Serializable { + case compactPoster + case compactLogo + case cinematic + + var label: String { + switch self { + case .compactPoster: + return L10n.compactPoster + case .compactLogo: + return L10n.compactLogo + case .cinematic: + return L10n.cinematic + } + } +} diff --git a/Shared/Objects/OverlaySliderColor.swift b/Shared/Objects/OverlaySliderColor.swift deleted file mode 100644 index 9200b5c5..00000000 --- a/Shared/Objects/OverlaySliderColor.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import UIKit - -enum OverlaySliderColor: String, CaseIterable, DefaultsSerializable { - case white - case jellyfinPurple - - var displayLabel: String { - switch self { - case .white: - return "White" - case .jellyfinPurple: - return "Jellyfin Purple" - } - } -} diff --git a/Shared/UIKit/PanDirectionGestureRecognizer.swift b/Shared/Objects/PanDirectionGestureRecognizer.swift similarity index 100% rename from Shared/UIKit/PanDirectionGestureRecognizer.swift rename to Shared/Objects/PanDirectionGestureRecognizer.swift diff --git a/Shared/Objects/PortraitImageStackable.swift b/Shared/Objects/PortraitImageStackable.swift deleted file mode 100644 index 1245e1ef..00000000 --- a/Shared/Objects/PortraitImageStackable.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation - -public protocol PortraitImageStackable { - func imageURLConstructor(maxWidth: Int) -> URL - var title: String { get } - var subtitle: String? { get } - var blurHash: String { get } - var failureInitials: String { get } - var portraitImageID: String { get } - var showTitle: Bool { get } -} diff --git a/Shared/Objects/Poster.swift b/Shared/Objects/Poster.swift new file mode 100644 index 00000000..d041a4ec --- /dev/null +++ b/Shared/Objects/Poster.swift @@ -0,0 +1,32 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation +import SwiftUI + +protocol Poster: Hashable { + var title: String { get } + var subtitle: String? { get } + var showTitle: Bool { get } +} + +extension Poster { + func hash(into hasher: inout Hasher) { + hasher.combine(title) + hasher.combine(subtitle) + } +} + +protocol PortraitPoster: Poster { + func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource +} + +protocol LandscapePoster: Poster { + func landscapePosterImageSources(maxWidth: CGFloat) -> [ImageSource] +} diff --git a/Shared/Objects/PosterSize.swift b/Shared/Objects/PosterSize.swift deleted file mode 100644 index b4b9fd17..00000000 --- a/Shared/Objects/PosterSize.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation - -enum PosterSize { - case small - case normal -} diff --git a/Shared/Singleton/BackgroundManager.swift b/Shared/Singleton/BackgroundManager.swift deleted file mode 100644 index 817e925b..00000000 --- a/Shared/Singleton/BackgroundManager.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation - -final class BackgroundManager { - static let current = BackgroundManager() - fileprivate(set) var backgroundURL: URL? - fileprivate(set) var blurhash: String = "001fC^" - - init() { - backgroundURL = nil - } - - func setBackground(to: URL, hash: String) { - self.backgroundURL = to - self.blurhash = hash - - let nc = NotificationCenter.default - nc.post(name: Notification.Name("backgroundDidChange"), object: nil) - } - - func clearBackground() { - self.backgroundURL = nil - self.blurhash = "001fC^" - - let nc = NotificationCenter.default - nc.post(name: Notification.Name("backgroundDidChange"), object: nil) - } -} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift new file mode 100644 index 00000000..c346679e --- /dev/null +++ b/Shared/Strings/Strings.swift @@ -0,0 +1,488 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references + +// MARK: - Strings + +// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces +internal enum L10n { + /// About + internal static let about = L10n.tr("Localizable", "about", fallback: #"About"#) + /// Accessibility + internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: #"Accessibility"#) + /// Add URL + internal static let addURL = L10n.tr("Localizable", "addURL", fallback: #"Add URL"#) + /// Airs %s + internal static func airWithDate(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "airWithDate", p1, fallback: #"Airs %s"#) + } + /// All Genres + internal static let allGenres = L10n.tr("Localizable", "allGenres", fallback: #"All Genres"#) + /// All Media + internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: #"All Media"#) + /// Appearance + internal static let appearance = L10n.tr("Localizable", "appearance", fallback: #"Appearance"#) + /// Apply + internal static let apply = L10n.tr("Localizable", "apply", fallback: #"Apply"#) + /// Audio + internal static let audio = L10n.tr("Localizable", "audio", fallback: #"Audio"#) + /// Audio & Captions + internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions", fallback: #"Audio & Captions"#) + /// Audio Track + internal static let audioTrack = L10n.tr("Localizable", "audioTrack", fallback: #"Audio Track"#) + /// Authorize + internal static let authorize = L10n.tr("Localizable", "authorize", fallback: #"Authorize"#) + /// Auto Play + internal static let autoPlay = L10n.tr("Localizable", "autoPlay", fallback: #"Auto Play"#) + /// Back + internal static let back = L10n.tr("Localizable", "back", fallback: #"Back"#) + /// Cancel + internal static let cancel = L10n.tr("Localizable", "cancel", fallback: #"Cancel"#) + /// Cannot connect to host + internal static let cannotConnectToHost = L10n.tr("Localizable", "cannotConnectToHost", fallback: #"Cannot connect to host"#) + /// CAST + internal static let cast = L10n.tr("Localizable", "cast", fallback: #"CAST"#) + /// Cast & Crew + internal static let castAndCrew = L10n.tr("Localizable", "castAndCrew", fallback: #"Cast & Crew"#) + /// Change Server + internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: #"Change Server"#) + /// Channels + internal static let channels = L10n.tr("Localizable", "channels", fallback: #"Channels"#) + /// Chapters + internal static let chapters = L10n.tr("Localizable", "chapters", fallback: #"Chapters"#) + /// Cinematic + internal static let cinematic = L10n.tr("Localizable", "cinematic", fallback: #"Cinematic"#) + /// Cinematic Views + internal static let cinematicViews = L10n.tr("Localizable", "cinematicViews", fallback: #"Cinematic Views"#) + /// Close + internal static let close = L10n.tr("Localizable", "close", fallback: #"Close"#) + /// Closed Captions + internal static let closedCaptions = L10n.tr("Localizable", "closedCaptions", fallback: #"Closed Captions"#) + /// Compact + internal static let compact = L10n.tr("Localizable", "compact", fallback: #"Compact"#) + /// Compact Logo + internal static let compactLogo = L10n.tr("Localizable", "compactLogo", fallback: #"Compact Logo"#) + /// Compact Poster + internal static let compactPoster = L10n.tr("Localizable", "compactPoster", fallback: #"Compact Poster"#) + /// Confirm Close + internal static let confirmClose = L10n.tr("Localizable", "confirmClose", fallback: #"Confirm Close"#) + /// Connect + internal static let connect = L10n.tr("Localizable", "connect", fallback: #"Connect"#) + /// Connect Manually + internal static let connectManually = L10n.tr("Localizable", "connectManually", fallback: #"Connect Manually"#) + /// Connect to Jellyfin + internal static let connectToJellyfin = L10n.tr("Localizable", "connectToJellyfin", fallback: #"Connect to Jellyfin"#) + /// Connect to a Jellyfin server + internal static let connectToJellyfinServer = L10n.tr("Localizable", "connectToJellyfinServer", fallback: #"Connect to a Jellyfin server"#) + /// Connect to a Jellyfin server to get started + internal static let connectToJellyfinServerStart = L10n.tr("Localizable", "connectToJellyfinServerStart", fallback: #"Connect to a Jellyfin server to get started"#) + /// Connect to Server + internal static let connectToServer = L10n.tr("Localizable", "connectToServer", fallback: #"Connect to Server"#) + /// Containers + internal static let containers = L10n.tr("Localizable", "containers", fallback: #"Containers"#) + /// Continue + internal static let `continue` = L10n.tr("Localizable", "continue", fallback: #"Continue"#) + /// Continue Watching + internal static let continueWatching = L10n.tr("Localizable", "continueWatching", fallback: #"Continue Watching"#) + /// Current Position + internal static let currentPosition = L10n.tr("Localizable", "currentPosition", fallback: #"Current Position"#) + /// Customize + internal static let customize = L10n.tr("Localizable", "customize", fallback: #"Customize"#) + /// Dark + internal static let dark = L10n.tr("Localizable", "dark", fallback: #"Dark"#) + /// Default Scheme + internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: #"Default Scheme"#) + /// DIRECTOR + internal static let director = L10n.tr("Localizable", "director", fallback: #"DIRECTOR"#) + /// Discovered Servers + internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers", fallback: #"Discovered Servers"#) + /// Display order + internal static let displayOrder = L10n.tr("Localizable", "displayOrder", fallback: #"Display order"#) + /// Edit Jump Lengths + internal static let editJumpLengths = L10n.tr("Localizable", "editJumpLengths", fallback: #"Edit Jump Lengths"#) + /// Empty Next Up + internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: #"Empty Next Up"#) + /// Episode %2$@ + internal static func episodeNumber(_ p1: Any) -> String { + return L10n.tr("Localizable", "episodeNumber", String(describing: p1), fallback: #"Episode %2$@"#) + } + /// Episodes + internal static let episodes = L10n.tr("Localizable", "episodes", fallback: #"Episodes"#) + /// Error + internal static let error = L10n.tr("Localizable", "error", fallback: #"Error"#) + /// Existing Server + internal static let existingServer = L10n.tr("Localizable", "existingServer", fallback: #"Existing Server"#) + /// Existing User + internal static let existingUser = L10n.tr("Localizable", "existingUser", fallback: #"Existing User"#) + /// Experimental + internal static let experimental = L10n.tr("Localizable", "experimental", fallback: #"Experimental"#) + /// Favorites + internal static let favorites = L10n.tr("Localizable", "favorites", fallback: #"Favorites"#) + /// File + internal static let file = L10n.tr("Localizable", "file", fallback: #"File"#) + /// Filter Results + internal static let filterResults = L10n.tr("Localizable", "filterResults", fallback: #"Filter Results"#) + /// Filters + internal static let filters = L10n.tr("Localizable", "filters", fallback: #"Filters"#) + /// Genres + internal static let genres = L10n.tr("Localizable", "genres", fallback: #"Genres"#) + /// Home + internal static let home = L10n.tr("Localizable", "home", fallback: #"Home"#) + /// Information + internal static let information = L10n.tr("Localizable", "information", fallback: #"Information"#) + /// Items + internal static let items = L10n.tr("Localizable", "items", fallback: #"Items"#) + /// Jump Backward + internal static let jumpBackward = L10n.tr("Localizable", "jumpBackward", fallback: #"Jump Backward"#) + /// Jump Backward Length + internal static let jumpBackwardLength = L10n.tr("Localizable", "jumpBackwardLength", fallback: #"Jump Backward Length"#) + /// Jump Forward + internal static let jumpForward = L10n.tr("Localizable", "jumpForward", fallback: #"Jump Forward"#) + /// Jump Forward Length + internal static let jumpForwardLength = L10n.tr("Localizable", "jumpForwardLength", fallback: #"Jump Forward Length"#) + /// Jump Gestures Enabled + internal static let jumpGesturesEnabled = L10n.tr("Localizable", "jumpGesturesEnabled", fallback: #"Jump Gestures Enabled"#) + /// %s seconds + internal static func jumpLengthSeconds(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "jumpLengthSeconds", p1, fallback: #"%s seconds"#) + } + /// Larger + internal static let larger = L10n.tr("Localizable", "larger", fallback: #"Larger"#) + /// Largest + internal static let largest = L10n.tr("Localizable", "largest", fallback: #"Largest"#) + /// Latest %@ + internal static func latestWithString(_ p1: Any) -> String { + return L10n.tr("Localizable", "latestWithString", String(describing: p1), fallback: #"Latest %@"#) + } + /// Library + internal static let library = L10n.tr("Localizable", "library", fallback: #"Library"#) + /// Light + internal static let light = L10n.tr("Localizable", "light", fallback: #"Light"#) + /// Loading + internal static let loading = L10n.tr("Localizable", "loading", fallback: #"Loading"#) + /// Local Servers + internal static let localServers = L10n.tr("Localizable", "localServers", fallback: #"Local Servers"#) + /// Login + internal static let login = L10n.tr("Localizable", "login", fallback: #"Login"#) + /// Login to %@ + internal static func loginToWithString(_ p1: Any) -> String { + return L10n.tr("Localizable", "loginToWithString", String(describing: p1), fallback: #"Login to %@"#) + } + /// Media + internal static let media = L10n.tr("Localizable", "media", fallback: #"Media"#) + /// Missing + internal static let missing = L10n.tr("Localizable", "missing", fallback: #"Missing"#) + /// Missing Items + internal static let missingItems = L10n.tr("Localizable", "missingItems", fallback: #"Missing Items"#) + /// More Like This + internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis", fallback: #"More Like This"#) + /// Movies + internal static let movies = L10n.tr("Localizable", "movies", fallback: #"Movies"#) + /// %d users + internal static func multipleUsers(_ p1: Int) -> String { + return L10n.tr("Localizable", "multipleUsers", p1, fallback: #"%d users"#) + } + /// Name + internal static let name = L10n.tr("Localizable", "name", fallback: #"Name"#) + /// Networking + internal static let networking = L10n.tr("Localizable", "networking", fallback: #"Networking"#) + /// Network timed out + internal static let networkTimedOut = L10n.tr("Localizable", "networkTimedOut", fallback: #"Network timed out"#) + /// Next + internal static let next = L10n.tr("Localizable", "next", fallback: #"Next"#) + /// Next Item + internal static let nextItem = L10n.tr("Localizable", "nextItem", fallback: #"Next Item"#) + /// Next Up + internal static let nextUp = L10n.tr("Localizable", "nextUp", fallback: #"Next Up"#) + /// No Cast devices found.. + internal static let noCastdevicesfound = L10n.tr("Localizable", "noCastdevicesfound", fallback: #"No Cast devices found.."#) + /// No Codec + internal static let noCodec = L10n.tr("Localizable", "noCodec", fallback: #"No Codec"#) + /// No episodes available + internal static let noEpisodesAvailable = L10n.tr("Localizable", "noEpisodesAvailable", fallback: #"No episodes available"#) + /// No local servers found + internal static let noLocalServersFound = L10n.tr("Localizable", "noLocalServersFound", fallback: #"No local servers found"#) + /// None + internal static let `none` = L10n.tr("Localizable", "none", fallback: #"None"#) + /// No overview available + internal static let noOverviewAvailable = L10n.tr("Localizable", "noOverviewAvailable", fallback: #"No overview available"#) + /// No public Users + internal static let noPublicUsers = L10n.tr("Localizable", "noPublicUsers", fallback: #"No public Users"#) + /// No results. + internal static let noResults = L10n.tr("Localizable", "noResults", fallback: #"No results."#) + /// Normal + internal static let normal = L10n.tr("Localizable", "normal", fallback: #"Normal"#) + /// N/A + internal static let notAvailableSlash = L10n.tr("Localizable", "notAvailableSlash", fallback: #"N/A"#) + /// Type: %@ not implemented yet :( + internal static func notImplementedYetWithType(_ p1: Any) -> String { + return L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1), fallback: #"Type: %@ not implemented yet :("#) + } + /// No title + internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: #"No title"#) + /// Ok + internal static let ok = L10n.tr("Localizable", "ok", fallback: #"Ok"#) + /// 1 user + internal static let oneUser = L10n.tr("Localizable", "oneUser", fallback: #"1 user"#) + /// Operating System + internal static let operatingSystem = L10n.tr("Localizable", "operatingSystem", fallback: #"Operating System"#) + /// Other + internal static let other = L10n.tr("Localizable", "other", fallback: #"Other"#) + /// Other User + internal static let otherUser = L10n.tr("Localizable", "otherUser", fallback: #"Other User"#) + /// Overlay + internal static let overlay = L10n.tr("Localizable", "overlay", fallback: #"Overlay"#) + /// Overlay Type + internal static let overlayType = L10n.tr("Localizable", "overlayType", fallback: #"Overlay Type"#) + /// Overview + internal static let overview = L10n.tr("Localizable", "overview", fallback: #"Overview"#) + /// Page %1$@ of %2$@ + internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2), fallback: #"Page %1$@ of %2$@"#) + } + /// Password + internal static let password = L10n.tr("Localizable", "password", fallback: #"Password"#) + /// Play + internal static let play = L10n.tr("Localizable", "play", fallback: #"Play"#) + /// Play / Pause + internal static let playAndPause = L10n.tr("Localizable", "playAndPause", fallback: #"Play / Pause"#) + /// Playback settings + internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings", fallback: #"Playback settings"#) + /// Playback Speed + internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed", fallback: #"Playback Speed"#) + /// Player Gestures Lock Gesture Enabled + internal static let playerGesturesLockGestureEnabled = L10n.tr("Localizable", "playerGesturesLockGestureEnabled", fallback: #"Player Gestures Lock Gesture Enabled"#) + /// Play From Beginning + internal static let playFromBeginning = L10n.tr("Localizable", "playFromBeginning", fallback: #"Play From Beginning"#) + /// Play Next + internal static let playNext = L10n.tr("Localizable", "playNext", fallback: #"Play Next"#) + /// Play Next Item + internal static let playNextItem = L10n.tr("Localizable", "playNextItem", fallback: #"Play Next Item"#) + /// Play Previous Item + internal static let playPreviousItem = L10n.tr("Localizable", "playPreviousItem", fallback: #"Play Previous Item"#) + /// Present + internal static let present = L10n.tr("Localizable", "present", fallback: #"Present"#) + /// Press Down for Menu + internal static let pressDownForMenu = L10n.tr("Localizable", "pressDownForMenu", fallback: #"Press Down for Menu"#) + /// Previous Item + internal static let previousItem = L10n.tr("Localizable", "previousItem", fallback: #"Previous Item"#) + /// Programs + internal static let programs = L10n.tr("Localizable", "programs", fallback: #"Programs"#) + /// Public Users + internal static let publicUsers = L10n.tr("Localizable", "publicUsers", fallback: #"Public Users"#) + /// Quick Connect + internal static let quickConnect = L10n.tr("Localizable", "quickConnect", fallback: #"Quick Connect"#) + /// Quick Connect code + internal static let quickConnectCode = L10n.tr("Localizable", "quickConnectCode", fallback: #"Quick Connect code"#) + /// Invalid Quick Connect code + internal static let quickConnectInvalidError = L10n.tr("Localizable", "quickConnectInvalidError", fallback: #"Invalid Quick Connect code"#) + /// Note: Quick Connect not enabled + internal static let quickConnectNotEnabled = L10n.tr("Localizable", "quickConnectNotEnabled", fallback: #"Note: Quick Connect not enabled"#) + /// 1. Open the Jellyfin app on your phone or web browser and sign in with your account + internal static let quickConnectStep1 = L10n.tr("Localizable", "quickConnectStep1", fallback: #"1. Open the Jellyfin app on your phone or web browser and sign in with your account"#) + /// 2. Open the user menu and go to the Quick Connect page + internal static let quickConnectStep2 = L10n.tr("Localizable", "quickConnectStep2", fallback: #"2. Open the user menu and go to the Quick Connect page"#) + /// 3. Enter the following code: + internal static let quickConnectStep3 = L10n.tr("Localizable", "quickConnectStep3", fallback: #"3. Enter the following code:"#) + /// Authorizing Quick Connect successful. Please continue on your other device. + internal static let quickConnectSuccessMessage = L10n.tr("Localizable", "quickConnectSuccessMessage", fallback: #"Authorizing Quick Connect successful. Please continue on your other device."#) + /// Rated + internal static let rated = L10n.tr("Localizable", "rated", fallback: #"Rated"#) + /// Recently Added + internal static let recentlyAdded = L10n.tr("Localizable", "recentlyAdded", fallback: #"Recently Added"#) + /// Recommended + internal static let recommended = L10n.tr("Localizable", "recommended", fallback: #"Recommended"#) + /// Refresh + internal static let refresh = L10n.tr("Localizable", "refresh", fallback: #"Refresh"#) + /// Regular + internal static let regular = L10n.tr("Localizable", "regular", fallback: #"Regular"#) + /// Released + internal static let released = L10n.tr("Localizable", "released", fallback: #"Released"#) + /// Remaining Time + internal static let remainingTime = L10n.tr("Localizable", "remainingTime", fallback: #"Remaining Time"#) + /// Remove + internal static let remove = L10n.tr("Localizable", "remove", fallback: #"Remove"#) + /// Remove All Users + internal static let removeAllUsers = L10n.tr("Localizable", "removeAllUsers", fallback: #"Remove All Users"#) + /// Remove From Resume + internal static let removeFromResume = L10n.tr("Localizable", "removeFromResume", fallback: #"Remove From Resume"#) + /// Report an Issue + internal static let reportIssue = L10n.tr("Localizable", "reportIssue", fallback: #"Report an Issue"#) + /// Request a Feature + internal static let requestFeature = L10n.tr("Localizable", "requestFeature", fallback: #"Request a Feature"#) + /// Reset + internal static let reset = L10n.tr("Localizable", "reset", fallback: #"Reset"#) + /// Reset App Settings + internal static let resetAppSettings = L10n.tr("Localizable", "resetAppSettings", fallback: #"Reset App Settings"#) + /// Reset User Settings + internal static let resetUserSettings = L10n.tr("Localizable", "resetUserSettings", fallback: #"Reset User Settings"#) + /// Resume 5 Second Offset + internal static let resume5SecondOffset = L10n.tr("Localizable", "resume5SecondOffset", fallback: #"Resume 5 Second Offset"#) + /// Retry + internal static let retry = L10n.tr("Localizable", "retry", fallback: #"Retry"#) + /// Runtime + internal static let runtime = L10n.tr("Localizable", "runtime", fallback: #"Runtime"#) + /// Search + internal static let search = L10n.tr("Localizable", "search", fallback: #"Search"#) + /// Search… + internal static let searchDots = L10n.tr("Localizable", "searchDots", fallback: #"Search…"#) + /// Searching… + internal static let searchingDots = L10n.tr("Localizable", "searchingDots", fallback: #"Searching…"#) + /// Season + internal static let season = L10n.tr("Localizable", "season", fallback: #"Season"#) + /// S%1$@:E%2$@ + internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2), fallback: #"S%1$@:E%2$@"#) + } + /// Seasons + internal static let seasons = L10n.tr("Localizable", "seasons", fallback: #"Seasons"#) + /// See All + internal static let seeAll = L10n.tr("Localizable", "seeAll", fallback: #"See All"#) + /// Seek Slide Gesture Enabled + internal static let seekSlideGestureEnabled = L10n.tr("Localizable", "seekSlideGestureEnabled", fallback: #"Seek Slide Gesture Enabled"#) + /// See More + internal static let seeMore = L10n.tr("Localizable", "seeMore", fallback: #"See More"#) + /// Select Cast Destination + internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination", fallback: #"Select Cast Destination"#) + /// Series + internal static let series = L10n.tr("Localizable", "series", fallback: #"Series"#) + /// Server + internal static let server = L10n.tr("Localizable", "server", fallback: #"Server"#) + /// Server %s is already connected + internal static func serverAlreadyConnected(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "serverAlreadyConnected", p1, fallback: #"Server %s is already connected"#) + } + /// Server %s already exists. Add new URL? + internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1, fallback: #"Server %s already exists. Add new URL?"#) + } + /// Server Details + internal static let serverDetails = L10n.tr("Localizable", "serverDetails", fallback: #"Server Details"#) + /// Server Information + internal static let serverInformation = L10n.tr("Localizable", "serverInformation", fallback: #"Server Information"#) + /// Servers + internal static let servers = L10n.tr("Localizable", "servers", fallback: #"Servers"#) + /// Server URL + internal static let serverURL = L10n.tr("Localizable", "serverURL", fallback: #"Server URL"#) + /// Settings + internal static let settings = L10n.tr("Localizable", "settings", fallback: #"Settings"#) + /// Show Cast & Crew + internal static let showCastAndCrew = L10n.tr("Localizable", "showCastAndCrew", fallback: #"Show Cast & Crew"#) + /// Show Chapters Info In Bottom Overlay + internal static let showChaptersInfoInBottomOverlay = L10n.tr("Localizable", "showChaptersInfoInBottomOverlay", fallback: #"Show Chapters Info In Bottom Overlay"#) + /// Flatten Library Items + internal static let showFlattenView = L10n.tr("Localizable", "showFlattenView", fallback: #"Flatten Library Items"#) + /// Show Missing Episodes + internal static let showMissingEpisodes = L10n.tr("Localizable", "showMissingEpisodes", fallback: #"Show Missing Episodes"#) + /// Show Missing Seasons + internal static let showMissingSeasons = L10n.tr("Localizable", "showMissingSeasons", fallback: #"Show Missing Seasons"#) + /// Show Poster Labels + internal static let showPosterLabels = L10n.tr("Localizable", "showPosterLabels", fallback: #"Show Poster Labels"#) + /// Signed in as %@ + internal static func signedInAsWithString(_ p1: Any) -> String { + return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1), fallback: #"Signed in as %@"#) + } + /// Sign In + internal static let signIn = L10n.tr("Localizable", "signIn", fallback: #"Sign In"#) + /// Sign in to get started + internal static let signInGetStarted = L10n.tr("Localizable", "signInGetStarted", fallback: #"Sign in to get started"#) + /// Sign In to %s + internal static func signInToServer(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "signInToServer", p1, fallback: #"Sign In to %s"#) + } + /// Smaller + internal static let smaller = L10n.tr("Localizable", "smaller", fallback: #"Smaller"#) + /// Smallest + internal static let smallest = L10n.tr("Localizable", "smallest", fallback: #"Smallest"#) + /// Sort by + internal static let sortBy = L10n.tr("Localizable", "sortBy", fallback: #"Sort by"#) + /// Source Code + internal static let sourceCode = L10n.tr("Localizable", "sourceCode", fallback: #"Source Code"#) + /// STUDIO + internal static let studio = L10n.tr("Localizable", "studio", fallback: #"STUDIO"#) + /// Studios + internal static let studios = L10n.tr("Localizable", "studios", fallback: #"Studios"#) + /// Subtitle Font + internal static let subtitleFont = L10n.tr("Localizable", "subtitleFont", fallback: #"Subtitle Font"#) + /// Subtitles + internal static let subtitles = L10n.tr("Localizable", "subtitles", fallback: #"Subtitles"#) + /// Subtitle Size + internal static let subtitleSize = L10n.tr("Localizable", "subtitleSize", fallback: #"Subtitle Size"#) + /// Suggestions + internal static let suggestions = L10n.tr("Localizable", "suggestions", fallback: #"Suggestions"#) + /// Switch User + internal static let switchUser = L10n.tr("Localizable", "switchUser", fallback: #"Switch User"#) + /// System + internal static let system = L10n.tr("Localizable", "system", fallback: #"System"#) + /// System Control Gestures Enabled + internal static let systemControlGesturesEnabled = L10n.tr("Localizable", "systemControlGesturesEnabled", fallback: #"System Control Gestures Enabled"#) + /// Tags + internal static let tags = L10n.tr("Localizable", "tags", fallback: #"Tags"#) + /// Too Many Redirects + internal static let tooManyRedirects = L10n.tr("Localizable", "tooManyRedirects", fallback: #"Too Many Redirects"#) + /// Try again + internal static let tryAgain = L10n.tr("Localizable", "tryAgain", fallback: #"Try again"#) + /// TV Shows + internal static let tvShows = L10n.tr("Localizable", "tvShows", fallback: #"TV Shows"#) + /// Unable to connect to server + internal static let unableToConnectServer = L10n.tr("Localizable", "unableToConnectServer", fallback: #"Unable to connect to server"#) + /// Unable to find host + internal static let unableToFindHost = L10n.tr("Localizable", "unableToFindHost", fallback: #"Unable to find host"#) + /// Unaired + internal static let unaired = L10n.tr("Localizable", "unaired", fallback: #"Unaired"#) + /// Unauthorized + internal static let unauthorized = L10n.tr("Localizable", "unauthorized", fallback: #"Unauthorized"#) + /// Unauthorized user + internal static let unauthorizedUser = L10n.tr("Localizable", "unauthorizedUser", fallback: #"Unauthorized user"#) + /// Unknown + internal static let unknown = L10n.tr("Localizable", "unknown", fallback: #"Unknown"#) + /// Unknown Error + internal static let unknownError = L10n.tr("Localizable", "unknownError", fallback: #"Unknown Error"#) + /// URL + internal static let url = L10n.tr("Localizable", "url", fallback: #"URL"#) + /// User + internal static let user = L10n.tr("Localizable", "user", fallback: #"User"#) + /// User %s is already signed in + internal static func userAlreadySignedIn(_ p1: UnsafePointer) -> String { + return L10n.tr("Localizable", "userAlreadySignedIn", p1, fallback: #"User %s is already signed in"#) + } + /// Username + internal static let username = L10n.tr("Localizable", "username", fallback: #"Username"#) + /// Version + internal static let version = L10n.tr("Localizable", "version", fallback: #"Version"#) + /// Video Player + internal static let videoPlayer = L10n.tr("Localizable", "videoPlayer", fallback: #"Video Player"#) + /// Who's watching? + internal static let whosWatching = L10n.tr("Localizable", "WhosWatching", fallback: #"Who's watching?"#) + /// WIP + internal static let wip = L10n.tr("Localizable", "wip", fallback: #"WIP"#) + /// Your Favorites + internal static let yourFavorites = L10n.tr("Localizable", "yourFavorites", fallback: #"Your Favorites"#) +} +// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces + +// MARK: - Implementation Details + +extension L10n { + private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { + let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table) + return String(format: format, locale: Locale.current, arguments: args) + } +} + +// swiftlint:disable convenience_type +private final class BundleToken { + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() +} +// swiftlint:enable convenience_type diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index c6a4854b..be125f6f 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -10,6 +10,8 @@ import Defaults import Foundation import UIKit +// TODO: Refactor... + extension SwiftfinStore { enum Defaults { static let generalSuite: UserDefaults = .init(suiteName: "swiftfinstore-general-defaults")! @@ -39,6 +41,7 @@ extension Defaults.Keys { static let showPosterLabels = Key("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let showCastAndCrew = Key("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let showFlattenView = Key("showFlattenView", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let itemViewType = Key("itemViewType", default: .compactLogo, suite: SwiftfinStore.Defaults.generalSuite) // Video player / overlay settings static let overlayType = Key("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) @@ -116,5 +119,4 @@ extension Defaults.Keys { // tvos specific static let downActionShowsMenu = Key("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let confirmClose = Key("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite) - static let tvOSCinematicViews = Key("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite) } diff --git a/Shared/ViewModels/EpisodesRowManager.swift b/Shared/ViewModels/EpisodesRowManager.swift index 711988cc..684064b0 100644 --- a/Shared/ViewModels/EpisodesRowManager.swift +++ b/Shared/ViewModels/EpisodesRowManager.swift @@ -14,9 +14,11 @@ protocol EpisodesRowManager: ViewModel { var item: BaseItemDto { get } var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] { get set } var selectedSeason: BaseItemDto? { get set } - func retrieveSeasons() - func retrieveEpisodesForSeason(_ season: BaseItemDto) + + func getSeasons() + func getEpisodesForSeason(_ season: BaseItemDto) func select(season: BaseItemDto) + func select(seasonID: String) } extension EpisodesRowManager { @@ -25,10 +27,19 @@ extension EpisodesRowManager { Array(seasonsEpisodes.keys).sorted(by: { $0.indexNumber ?? 0 < $1.indexNumber ?? 0 }) } + var currentEpisodes: [BaseItemDto]? { + if let selectedSeason = selectedSeason { + return seasonsEpisodes[selectedSeason] + } else { + guard let firstSeason = seasonsEpisodes.keys.first else { return nil } + return seasonsEpisodes[firstSeason] + } + } + // Also retrieves the current season episodes if available - func retrieveSeasons() { + func getSeasons() { TvShowsAPI.getSeasons( - seriesId: item.seriesId ?? "", + seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id, isMissing: Defaults[.shouldShowMissingSeasons] ? nil : false ) @@ -36,26 +47,21 @@ extension EpisodesRowManager { self.handleAPIRequestError(completion: completion) } receiveValue: { response in let seasons = response.items ?? [] + seasons.forEach { season in self.seasonsEpisodes[season] = [] - - if season.id == self.item.seasonId ?? "" { - self.selectedSeason = season - self.retrieveEpisodesForSeason(season) - } else if season.id == self.item.id ?? "" { - self.selectedSeason = season - self.retrieveEpisodesForSeason(season) - } } + + self.selectedSeason = seasons.first } .store(in: &cancellables) } - func retrieveEpisodesForSeason(_ season: BaseItemDto) { + func getEpisodesForSeason(_ season: BaseItemDto) { guard let seasonID = season.id else { return } TvShowsAPI.getEpisodes( - seriesId: item.seriesId ?? "", + seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: seasonID, @@ -74,7 +80,12 @@ extension EpisodesRowManager { self.selectedSeason = season if seasonsEpisodes[season]!.isEmpty { - retrieveEpisodesForSeason(season) + getEpisodesForSeason(season) } } + + func select(seasonID: String) { + guard let selectedSeason = Array(seasonsEpisodes.keys).first(where: { $0.id == seasonID }) else { return } + select(season: selectedSeason) + } } diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 984ffad4..cc5a2f33 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -25,7 +25,7 @@ final class HomeViewModel: ViewModel { var libraries: [BaseItemDto] = [] // temp - var recentFilterSet = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded]) + static let recentFilterSet = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded]) override init() { super.init() @@ -139,7 +139,7 @@ final class HomeViewModel: ViewModel { includeItemTypes: [.movie, .series], enableImageTypes: [.primary, .backdrop, .thumb], enableUserData: true, - limit: 8 + limit: 20 ) .sink { completion in switch completion { @@ -161,7 +161,7 @@ final class HomeViewModel: ViewModel { private func refreshResumeItems() { ItemsAPI.getResumeItems( userId: SessionManager.main.currentLogin.user.id, - limit: 6, + limit: 20, fields: [ .primaryImageAspectRatio, .seriesPrimaryImage, @@ -210,7 +210,7 @@ final class HomeViewModel: ViewModel { private func refreshNextUpItems() { TvShowsAPI.getNextUp( userId: SessionManager.main.currentLogin.user.id, - limit: 6, + limit: 20, fields: [ .primaryImageAspectRatio, .seriesPrimaryImage, diff --git a/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift b/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift index a56f35e9..da0e9423 100644 --- a/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift @@ -25,7 +25,7 @@ final class CollectionItemViewModel: ItemViewModel { ItemsAPI.getItems( userId: SessionManager.main.currentLogin.user.id, parentId: item.id, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people] + fields: ItemFields.allCases ) .trackActivity(loading) .sink { [weak self] completion in diff --git a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift index e4e379cf..a40fa7c2 100644 --- a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift @@ -11,39 +11,22 @@ import Foundation import JellyfinAPI import Stinsen -final class EpisodeItemViewModel: ItemViewModel, EpisodesRowManager { +final class EpisodeItemViewModel: ItemViewModel { @RouterObject - var itemRouter: ItemCoordinator.Router? + private var itemRouter: ItemCoordinator.Router? @Published - var series: BaseItemDto? + var playButtonText: String = "" @Published - var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:] - @Published - var selectedSeason: BaseItemDto? + var mediaDetailItems: [[BaseItemDto.ItemDetail]] = [] override init(item: BaseItemDto) { super.init(item: item) - getEpisodeSeries() - retrieveSeasons() - } - - override func getItemDisplayName() -> String { - guard let episodeLocator = item.getEpisodeLocator() else { return item.name ?? "" } - return "\(episodeLocator)\n\(item.name ?? "")" - } - - func getEpisodeSeries() { - guard let id = item.seriesId else { return } - UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) - .trackActivity(loading) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] item in - self?.series = item - }) - .store(in: &cancellables) + $videoPlayerViewModels.sink(receiveValue: { newValue in + self.mediaDetailItems = self.createMediaDetailItems(viewModels: newValue) + }) + .store(in: &cancellables) } override func updateItem() { @@ -72,4 +55,30 @@ final class EpisodeItemViewModel: ItemViewModel, EpisodesRowManager { } .store(in: &cancellables) } + + private func createMediaDetailItems(viewModels: [VideoPlayerViewModel]) -> [[BaseItemDto.ItemDetail]] { + var fileMediaItems: [[BaseItemDto.ItemDetail]] = [] + + for viewModel in viewModels { + + let audioStreams = viewModel.audioStreams.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" } + .joined(separator: ", ") + + let subtitleStreams = viewModel.subtitleStreams + .compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" } + .joined(separator: ", ") + + let currentMediaItems: [BaseItemDto.ItemDetail] = [ + .init(title: "File", content: viewModel.filename ?? "--"), + .init(title: "Audio", content: audioStreams), + .init(title: "Subtitles", content: subtitleStreams), + ] + + fileMediaItems.append(currentMediaItems) + } + + // print(fileMediaItems) + + return fileMediaItems + } } diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index 2f73c129..954eb19c 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -31,15 +31,15 @@ class ItemViewModel: ViewModel { @Published var isFavorited = false @Published - var informationItems: [BaseItemDto.ItemDetail] - @Published var selectedVideoPlayerViewModel: VideoPlayerViewModel? + @Published var videoPlayerViewModels: [VideoPlayerViewModel] = [] init(item: BaseItemDto) { self.item = item + super.init() - switch item.itemType { + switch item.type { case .episode, .movie: if !item.missing && !item.unaired { self.playButtonItem = item @@ -47,17 +47,13 @@ class ItemViewModel: ViewModel { default: () } - informationItems = item.createInformationItems() - isFavorited = item.userData?.isFavorite ?? false isWatched = item.userData?.played ?? false - super.init() getSimilarItems() + refreshItemVideoPlayerViewModel(for: item) Notifications[.didSendStopReport].subscribe(self, selector: #selector(receivedStopReport(_:))) - - refreshItemVideoPlayerViewModel(for: item) } @objc @@ -74,7 +70,7 @@ class ItemViewModel: ViewModel { } func refreshItemVideoPlayerViewModel(for item: BaseItemDto) { - guard item.itemType == .episode || item.itemType == .movie else { return } + guard item.type == .episode || item.type == .movie else { return } guard !item.missing, !item.unaired else { return } item.createVideoPlayerViewModel() @@ -104,20 +100,12 @@ class ItemViewModel: ViewModel { return L10n.play } - func getItemDisplayName() -> String { - item.name ?? "" - } - - func shouldDisplayRuntime() -> Bool { - true - } - func getSimilarItems() { LibraryAPI.getSimilarItems( itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, - limit: 10, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people] + limit: 20, + fields: ItemFields.allCases ) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in @@ -128,54 +116,52 @@ class ItemViewModel: ViewModel { .store(in: &cancellables) } - func updateWatchState() { - if isWatched { - PlaystateAPI.markUnplayedItem( - userId: SessionManager.main.currentLogin.user.id, - itemId: item.id! - ) - .trackActivity(loading) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] _ in - self?.isWatched = false - }) - .store(in: &cancellables) + func toggleWatchState() { + let current = isWatched + isWatched.toggle() + let request: AnyPublisher + + if current { + request = PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) } else { - PlaystateAPI.markPlayedItem( - userId: SessionManager.main.currentLogin.user.id, - itemId: item.id! - ) + request = PlaystateAPI.markPlayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) + } + + request .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in + switch completion { + case .failure: + self?.isWatched = !current + case .finished: () + } self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] _ in - self?.isWatched = true - }) + }, receiveValue: { _ in }) .store(in: &cancellables) - } } - func updateFavoriteState() { - if isFavorited { - UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) - .trackActivity(loading) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] _ in - self?.isFavorited = false - }) - .store(in: &cancellables) + func toggleFavoriteState() { + let current = isFavorited + isFavorited.toggle() + let request: AnyPublisher + + if current { + request = UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) } else { - UserLibraryAPI.markFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) - .trackActivity(loading) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] _ in - self?.isFavorited = true - }) - .store(in: &cancellables) + request = UserLibraryAPI.markFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) } + + request + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + switch completion { + case .failure: + self?.isFavorited = !current + case .finished: () + } + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { _ in }) + .store(in: &cancellables) } // Overridden by subclasses diff --git a/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift index 777cc4f2..dd1a26fb 100644 --- a/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift @@ -14,11 +14,7 @@ import Stinsen final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager { @RouterObject - var itemRouter: ItemCoordinator.Router? - @Published - var episodes: [BaseItemDto] = [] - @Published - var seriesItem: BaseItemDto? + private var itemRouter: ItemCoordinator.Router? @Published var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:] @Published @@ -27,9 +23,8 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager { override init(item: BaseItemDto) { super.init(item: item) - getSeriesItem() selectedSeason = item - retrieveSeasons() +// getSeasons() requestEpisodes() } @@ -39,7 +34,7 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager { return L10n.unaired } - guard let playButtonItem = playButtonItem, let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play } + guard let playButtonItem = playButtonItem, let episodeLocator = playButtonItem.episodeLocator else { return L10n.play } return episodeLocator } @@ -57,8 +52,7 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager { self?.handleAPIRequestError(completion: completion) }, receiveValue: { [weak self] response in guard let self = self else { return } - self.episodes = response.items ?? [] - LogManager.log.debug("Retrieved \(String(self.episodes.count)) episodes") + self.seasonsEpisodes[self.item] = response.items ?? [] self.setNextUpInSeason() }) @@ -87,44 +81,29 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager { LogManager.log.debug("Nextup in season \(self.item.id!) (\(self.item.name!)): \(nextUpItem.id!)") } - if self.playButtonItem == nil && !self.episodes.isEmpty { - // Fallback to the old mechanism: - // Sets the play button item to the "Next up" in the season based upon - // the watched status of episodes in the season. - // Default to the first episode of the season if all have been watched. - var firstUnwatchedSearch: BaseItemDto? - - for episode in self.episodes { - guard let played = episode.userData?.played else { continue } - if !played { - firstUnwatchedSearch = episode - break - } - } - - if let firstUnwatched = firstUnwatchedSearch { - self.playButtonItem = firstUnwatched - } else { - guard let firstEpisode = self.episodes.first else { return } - self.playButtonItem = firstEpisode - } - } + // if self.playButtonItem == nil && !self.episodes.isEmpty { + // // Fallback to the old mechanism: + // // Sets the play button item to the "Next up" in the season based upon + // // the watched status of episodes in the season. + // // Default to the first episode of the season if all have been watched. + // var firstUnwatchedSearch: BaseItemDto? +// + // for episode in self.episodes { + // guard let played = episode.userData?.played else { continue } + // if !played { + // firstUnwatchedSearch = episode + // break + // } + // } +// + // if let firstUnwatched = firstUnwatchedSearch { + // self.playButtonItem = firstUnwatched + // } else { + // guard let firstEpisode = self.episodes.first else { return } + // self.playButtonItem = firstEpisode + // } + // } }) .store(in: &cancellables) } - - private func getSeriesItem() { - guard let seriesID = item.seriesId else { return } - UserLibraryAPI.getItem( - userId: SessionManager.main.currentLogin.user.id, - itemId: seriesID - ) - .trackActivity(loading) - .sink { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - } receiveValue: { [weak self] seriesItem in - self?.seriesItem = seriesItem - } - .store(in: &cancellables) - } } diff --git a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift index e035b8a0..d197c06c 100644 --- a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift @@ -11,16 +11,25 @@ import Defaults import Foundation import JellyfinAPI -final class SeriesItemViewModel: ItemViewModel { +final class SeriesItemViewModel: ItemViewModel, EpisodesRowManager { @Published - var seasons: [BaseItemDto] = [] + var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:] + @Published + var selectedSeason: BaseItemDto? override init(item: BaseItemDto) { super.init(item: item) - requestSeasons() + getSeasons() + + // The server won't have both a next up item + // and a resume item at the same time, so they + // control the button first. Also fetch first available + // item, which may be overwritten by next up or resume. getNextUp() + getResumeItem() + getFirstAvailableItem() } override func playButtonText() -> String { @@ -33,20 +42,16 @@ final class SeriesItemViewModel: ItemViewModel { return L10n.missing } - guard let playButtonItem = playButtonItem, let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play } + guard let playButtonItem = playButtonItem, + let episodeLocator = playButtonItem.seasonEpisodeLocator else { return L10n.play } + return episodeLocator } - override func shouldDisplayRuntime() -> Bool { - false - } - private func getNextUp() { - LogManager.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))") TvShowsAPI.getNextUp( userId: SessionManager.main.currentLogin.user.id, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true ) @@ -56,12 +61,64 @@ final class SeriesItemViewModel: ItemViewModel { }, receiveValue: { [weak self] response in if let nextUpItem = response.items?.first, !nextUpItem.unaired, !nextUpItem.missing { self?.playButtonItem = nextUpItem + + if let seasonID = nextUpItem.seasonId { + self?.select(seasonID: seasonID) + } } }) .store(in: &cancellables) } - private func getRunYears() -> String { + private func getResumeItem() { + ItemsAPI.getResumeItems( + userId: SessionManager.main.currentLogin.user.id, + limit: 1, + parentId: item.id + ) + .trackActivity(loading) + .sink { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + } receiveValue: { [weak self] response in + if let firstItem = response.items?.first { + self?.playButtonItem = firstItem + + if let seasonID = firstItem.seasonId { + self?.select(seasonID: seasonID) + } + } + } + .store(in: &cancellables) + } + + private func getFirstAvailableItem() { + ItemsAPI.getItemsByUserId( + userId: SessionManager.main.currentLogin.user.id, + limit: 2, + recursive: true, + sortOrder: [.ascending], + parentId: item.id, + includeItemTypes: [.episode] + ) + .trackActivity(loading) + .sink { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + } receiveValue: { [weak self] response in + if let firstItem = response.items?.first { + if self?.playButtonItem == nil { + // If other calls finish after this, it will be overwritten + self?.playButtonItem = firstItem + + if let seasonID = firstItem.seasonId { + self?.select(seasonID: seasonID) + } + } + } + } + .store(in: &cancellables) + } + + func getRunYears() -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy" @@ -78,23 +135,4 @@ final class SeriesItemViewModel: ItemViewModel { return "\(startYear ?? L10n.unknown) - \(endYear ?? L10n.present)" } - - private func requestSeasons() { - LogManager.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))") - TvShowsAPI.getSeasons( - seriesId: item.id ?? "", - userId: SessionManager.main.currentLogin.user.id, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - isMissing: Defaults[.shouldShowMissingSeasons] ? nil : false, - enableUserData: true - ) - .trackActivity(loading) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] response in - self?.seasons = response.items ?? [] - LogManager.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons") - }) - .store(in: &cancellables) - } } diff --git a/Shared/ViewModels/LatestMediaViewModel.swift b/Shared/ViewModels/LatestMediaViewModel.swift index d2f701d2..4ad903ef 100644 --- a/Shared/ViewModels/LatestMediaViewModel.swift +++ b/Shared/ViewModels/LatestMediaViewModel.swift @@ -29,14 +29,7 @@ final class LatestMediaViewModel: ViewModel { UserLibraryAPI.getLatestMedia( userId: SessionManager.main.currentLogin.user.id, parentId: library.id ?? "", - fields: [ - .primaryImageAspectRatio, - .seriesPrimaryImage, - .seasonUserData, - .overview, - .genres, - .people, - ], + fields: ItemFields.allCases, includeItemTypes: [.series, .movie], enableUserData: true, limit: 12 diff --git a/Shared/ViewModels/LibraryListViewModel.swift b/Shared/ViewModels/LibraryListViewModel.swift index 95536a8f..71df2a9a 100644 --- a/Shared/ViewModels/LibraryListViewModel.swift +++ b/Shared/ViewModels/LibraryListViewModel.swift @@ -6,6 +6,7 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // +import Defaults import Foundation import JellyfinAPI @@ -14,8 +15,18 @@ final class LibraryListViewModel: ViewModel { @Published var libraries: [BaseItemDto] = [] + var filteredLibraries: [BaseItemDto] { + var supportedLibraries = ["movies", "tvshows", "unknown"] + + if Defaults[.Experimental.liveTVAlphaEnabled] { + supportedLibraries.append("livetv") + } + + return libraries.filter { supportedLibraries.contains($0.collectionType ?? "unknown") } + } + // temp - var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) + let withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) override init() { super.init() diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 7d6a8caf..3e101c92 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -115,15 +115,7 @@ final class LibraryViewModel: ViewModel { searchTerm: nil, sortOrder: filters.sortOrder.compactMap { SortOrder(rawValue: $0.rawValue) }, parentId: parentID, - fields: [ - .primaryImageAspectRatio, - .seriesPrimaryImage, - .seasonUserData, - .overview, - .genres, - .people, - .chapters, - ], + fields: ItemFields.allCases, includeItemTypes: includeItemTypes, filters: filters.filters, sortBy: sortBy, diff --git a/Shared/ViewModels/MainTabViewModel.swift b/Shared/ViewModels/MainTabViewModel.swift deleted file mode 100644 index 4581cbf7..00000000 --- a/Shared/ViewModels/MainTabViewModel.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation -import JellyfinAPI - -final class MainTabViewModel: ViewModel { - @Published - var backgroundURL: URL? - @Published - var lastBackgroundURL: URL? - @Published - var backgroundBlurHash: String = "001fC^" - - override init() { - super.init() - - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(backgroundDidChange), name: Notification.Name("backgroundDidChange"), object: nil) - } - - @objc - func backgroundDidChange() { - self.lastBackgroundURL = self.backgroundURL - self.backgroundURL = BackgroundManager.current.backgroundURL - self.backgroundBlurHash = BackgroundManager.current.blurhash - } -} diff --git a/Shared/ViewModels/MovieLibrariesViewModel.swift b/Shared/ViewModels/MovieLibrariesViewModel.swift index 09ca6d4b..75d41c34 100644 --- a/Shared/ViewModels/MovieLibrariesViewModel.swift +++ b/Shared/ViewModels/MovieLibrariesViewModel.swift @@ -29,7 +29,7 @@ final class MovieLibrariesViewModel: ViewModel { private let columns: Int @RouterObject - var router: MovieLibrariesCoordinator.Router? + private var router: MovieLibrariesCoordinator.Router? init(columns: Int = 7) { self.columns = columns diff --git a/Shared/ViewModels/TVLibrariesViewModel.swift b/Shared/ViewModels/TVLibrariesViewModel.swift index 7109bfff..3d96ed09 100644 --- a/Shared/ViewModels/TVLibrariesViewModel.swift +++ b/Shared/ViewModels/TVLibrariesViewModel.swift @@ -29,7 +29,7 @@ final class TVLibrariesViewModel: ViewModel { private let columns: Int @RouterObject - var router: TVLibrariesCoordinator.Router? + private var router: TVLibrariesCoordinator.Router? init(columns: Int = 7) { self.columns = columns diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 8ff1bfd7..1eb6e0fd 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -14,7 +14,7 @@ import Stinsen final class UserSignInViewModel: ViewModel { @RouterObject - var router: UserSignInCoordinator.Router? + private var Router: UserSignInCoordinator.Router? @Published var publicUsers: [UserDto] = [] diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 99565b82..747f50db 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -123,6 +123,7 @@ final class VideoPlayerViewModel: ViewModel { let directStreamURL: URL let transcodedStreamURL: URL? let hlsStreamURL: URL + let videoStream: MediaStream let audioStreams: [MediaStream] let subtitleStreams: [MediaStream] let chapters: [ChapterInfo] @@ -220,6 +221,7 @@ final class VideoPlayerViewModel: ViewModel { hlsStreamURL: URL, streamType: ServerStreamType, response: PlaybackInfoResponse, + videoStream: MediaStream, audioStreams: [MediaStream], subtitleStreams: [MediaStream], chapters: [ChapterInfo], @@ -243,6 +245,7 @@ final class VideoPlayerViewModel: ViewModel { self.hlsStreamURL = hlsStreamURL self.streamType = streamType self.response = response + self.videoStream = videoStream self.audioStreams = audioStreams self.subtitleStreams = subtitleStreams self.chapters = chapters @@ -333,7 +336,7 @@ extension VideoPlayerViewModel { extension VideoPlayerViewModel { func getAdjacentEpisodes() { - guard let seriesID = item.seriesId, item.itemType == .episode else { return } + guard let seriesID = item.seriesId, item.type == .episode else { return } TvShowsAPI.getEpisodes( seriesId: seriesID, diff --git a/Swiftfin/Components/AppIcon.swift b/Shared/Views/AppIcon.swift similarity index 100% rename from Swiftfin/Components/AppIcon.swift rename to Shared/Views/AppIcon.swift diff --git a/Shared/Views/AttributeFillView.swift b/Shared/Views/AttributeFillView.swift new file mode 100644 index 00000000..21328ce5 --- /dev/null +++ b/Shared/Views/AttributeFillView.swift @@ -0,0 +1,32 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct AttributeFillView: View { + + let text: String + + var body: some View { + Text(text) + .font(.caption) + .fontWeight(.semibold) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .hidden() + .background { + Color(UIColor.lightGray) + .cornerRadius(2) + .inverseMask( + Text(text) + .font(.caption) + .fontWeight(.semibold) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + ) + } + } +} diff --git a/Shared/Views/AttributeOutlineView.swift b/Shared/Views/AttributeOutlineView.swift new file mode 100644 index 00000000..d982b294 --- /dev/null +++ b/Shared/Views/AttributeOutlineView.swift @@ -0,0 +1,26 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct AttributeOutlineView: View { + + let text: String + + var body: some View { + Text(text) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(Color(UIColor.lightGray)) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay( + RoundedRectangle(cornerRadius: 2) + .stroke(Color(UIColor.lightGray), lineWidth: 1) + ) + } +} diff --git a/Shared/Views/BlurHashView.swift b/Shared/Views/BlurHashView.swift index 65e94ca6..edc22b95 100644 --- a/Shared/Views/BlurHashView.swift +++ b/Shared/Views/BlurHashView.swift @@ -11,10 +11,16 @@ import UIKit struct BlurHashView: UIViewRepresentable { - let blurHash: String + private let blurHash: String + private let size: CGSize + + init(blurHash: String, size: CGSize = .Circle(radius: 12)) { + self.blurHash = blurHash + self.size = size + } func makeUIView(context: Context) -> UIBlurHashView { - UIBlurHashView(blurHash) + UIBlurHashView(blurHash, size: size) } func updateUIView(_ uiView: UIBlurHashView, context: Context) {} @@ -24,14 +30,14 @@ class UIBlurHashView: UIView { private let imageView: UIImageView - init(_ blurHash: String) { + init(_ blurHash: String, size: CGSize) { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false self.imageView = imageView super.init(frame: .zero) - computeBlurHashImageAsync(blurHash: blurHash) { [weak self] blurImage in + computeBlurHashImageAsync(blurHash: blurHash, size: size) { [weak self] blurImage in guard let self = self else { return } DispatchQueue.main.async { self.imageView.image = blurImage @@ -54,9 +60,9 @@ class UIBlurHashView: UIView { fatalError("init(coder:) has not been implemented") } - private func computeBlurHashImageAsync(blurHash: String, _ completion: @escaping (UIImage?) -> Void) { + private func computeBlurHashImageAsync(blurHash: String, size: CGSize, _ completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .utility).async { - let image = UIImage(blurHash: blurHash, size: .Circle(radius: 12)) + let image = UIImage(blurHash: blurHash, size: size) completion(image) } } diff --git a/Shared/Views/BlurView.swift b/Shared/Views/BlurView.swift new file mode 100644 index 00000000..b627f24a --- /dev/null +++ b/Shared/Views/BlurView.swift @@ -0,0 +1,29 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI +import UIKit + +struct BlurView: UIViewRepresentable { + + let style: UIBlurEffect.Style + + init(style: UIBlurEffect.Style = .regular) { + self.style = style + } + + func makeUIView(context: Context) -> UIVisualEffectView { + let view = UIVisualEffectView(effect: UIBlurEffect(style: style)) + view.translatesAutoresizingMaskIntoConstraints = false + return view + } + + func updateUIView(_ uiView: UIVisualEffectView, context: Context) { + uiView.effect = UIBlurEffect(style: style) + } +} diff --git a/Shared/Views/LazyView.swift b/Shared/Views/Divider.swift similarity index 70% rename from Shared/Views/LazyView.swift rename to Shared/Views/Divider.swift index e3eea6ec..d659fc49 100644 --- a/Shared/Views/LazyView.swift +++ b/Shared/Views/Divider.swift @@ -6,12 +6,13 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // -import Foundation import SwiftUI -struct LazyView: View { - var content: () -> Content +struct Divider: View { + var body: some View { - self.content() + Color.secondarySystemFill + .frame(height: 0.5) + .padding(.horizontal) } } diff --git a/Shared/Views/ImageView.swift b/Shared/Views/ImageView.swift index 31c98fc5..7e45c574 100644 --- a/Shared/Views/ImageView.swift +++ b/Shared/Views/ImageView.swift @@ -11,9 +11,7 @@ import NukeUI import SwiftUI import UIKit -// TODO: Fix 100+ inits - -struct ImageViewSource { +struct ImageSource { let url: URL? let blurHash: String? @@ -33,25 +31,38 @@ struct DefaultFailureView: View { struct ImageView: View { @State - private var sources: [ImageViewSource] + private var sources: [ImageSource] private var currentURL: URL? { sources.first?.url } private var currentBlurHash: String? { sources.first?.blurHash } - private var failureView: FailureView + private var failureView: () -> FailureView + private var resizingMode: ImageResizingMode - init(_ source: URL?, blurHash: String? = nil, @ViewBuilder failureView: () -> FailureView) { - let imageViewSource = ImageViewSource(url: source, blurHash: blurHash) - _sources = State(initialValue: [imageViewSource]) - self.failureView = failureView() + init( + _ source: URL?, + blurHash: String? = nil, + resizingMode: ImageResizingMode = .aspectFill, + @ViewBuilder failureView: @escaping () -> FailureView + ) { + let imageSource = ImageSource(url: source, blurHash: blurHash) + self.init(imageSource, resizingMode: resizingMode, failureView: failureView) } - init(_ source: ImageViewSource, @ViewBuilder failureView: () -> FailureView) { - _sources = State(initialValue: [source]) - self.failureView = failureView() + init( + _ source: ImageSource, + resizingMode: ImageResizingMode = .aspectFill, + @ViewBuilder failureView: @escaping () -> FailureView + ) { + self.init([source], resizingMode: resizingMode, failureView: failureView) } - init(_ sources: [ImageViewSource], @ViewBuilder failureView: () -> FailureView) { + init( + _ sources: [ImageSource], + resizingMode: ImageResizingMode = .aspectFill, + @ViewBuilder failureView: @escaping () -> FailureView + ) { _sources = State(initialValue: sources) - self.failureView = failureView() + self.resizingMode = resizingMode + self.failureView = failureView } @ViewBuilder @@ -60,18 +71,20 @@ struct ImageView: View { BlurHashView(blurHash: currentBlurHash) .id(currentBlurHash) } else { - Color.secondary + Color.clear } } var body: some View { - if let currentURL = currentURL { LazyImage(source: currentURL) { state in if let image = state.image { image + .resizingMode(resizingMode) } else if state.error != nil { - placeholderView.onAppear { sources.removeFirst() } + placeholderView.onAppear { + sources.removeFirst() + } } else { placeholderView } @@ -79,27 +92,27 @@ struct ImageView: View { .pipeline(ImagePipeline(configuration: .withDataCache)) .id(currentURL) } else { - failureView + failureView() } } } extension ImageView where FailureView == DefaultFailureView { - init(_ source: URL?, blurHash: String? = nil) { - let imageViewSource = ImageViewSource(url: source, blurHash: blurHash) - self.init(imageViewSource, failureView: { DefaultFailureView() }) + init(_ source: URL?, blurHash: String? = nil, resizingMode: ImageResizingMode = .aspectFill) { + let imageSource = ImageSource(url: source, blurHash: blurHash) + self.init([imageSource], resizingMode: resizingMode, failureView: { DefaultFailureView() }) } - init(_ source: ImageViewSource) { - self.init(source, failureView: { DefaultFailureView() }) + init(_ source: ImageSource, resizingMode: ImageResizingMode = .aspectFill) { + self.init([source], resizingMode: resizingMode, failureView: { DefaultFailureView() }) } - init(_ sources: [ImageViewSource]) { - self.init(sources, failureView: { DefaultFailureView() }) + init(_ sources: [ImageSource], resizingMode: ImageResizingMode = .aspectFill) { + self.init(sources, resizingMode: resizingMode, failureView: { DefaultFailureView() }) } - init(sources: [URL]) { - let imageViewSources = sources.compactMap { ImageViewSource(url: $0, blurHash: nil) } - self.init(imageViewSources, failureView: { DefaultFailureView() }) + init(sources: [URL], resizingMode: ImageResizingMode = .aspectFill) { + let imageSources = sources.compactMap { ImageSource(url: $0, blurHash: nil) } + self.init(imageSources, resizingMode: resizingMode, failureView: { DefaultFailureView() }) } } diff --git a/Shared/Views/ParallaxHeader.swift b/Shared/Views/ParallaxHeader.swift deleted file mode 100644 index 3fa72e7a..00000000 --- a/Shared/Views/ParallaxHeader.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation -import SwiftUI - -struct ParallaxHeaderScrollView: View { - var header: Header - var staticOverlayView: StaticOverlayView - var overlayAlignment: Alignment - var headerHeight: CGFloat - var content: () -> Content - - init( - header: Header, - staticOverlayView: StaticOverlayView, - overlayAlignment: Alignment = .center, - headerHeight: CGFloat, - content: @escaping () -> Content - ) { - self.header = header - self.staticOverlayView = staticOverlayView - self.overlayAlignment = overlayAlignment - self.headerHeight = headerHeight - self.content = content - } - - var body: some View { - ScrollView(showsIndicators: false) { - GeometryReader { proxy in - let yOffset = proxy.frame(in: .global).minY > 0 ? -proxy.frame(in: .global).minY : 0 - header - .frame(width: proxy.size.width, height: proxy.size.height - yOffset) - .overlay(staticOverlayView, alignment: overlayAlignment) - .offset(y: yOffset) - } - .frame(height: headerHeight) - - HStack { - content() - Spacer(minLength: 0) - } - } - } -} diff --git a/Shared/Views/TruncatedTextView.swift b/Shared/Views/TruncatedTextView.swift new file mode 100644 index 00000000..b590db09 --- /dev/null +++ b/Shared/Views/TruncatedTextView.swift @@ -0,0 +1,146 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension TruncatedTextView { + func font(_ font: Font) -> TruncatedTextView { + var result = self + result.font = font + return result + } + + func lineLimit(_ lineLimit: Int) -> TruncatedTextView { + var result = self + result.lineLimit = lineLimit + return result + } + + func foregroundColor(_ color: Color) -> TruncatedTextView { + var result = self + result.foregroundColor = color + return result + } +} + +extension String { + func heightOfString(usingFont font: UIFont) -> CGFloat { + let fontAttributes = [NSAttributedString.Key.font: font] + let textSize = self.size(withAttributes: fontAttributes) + return textSize.height + } + + func widthOfString(usingFont font: UIFont) -> CGFloat { + let fontAttributes = [NSAttributedString.Key.font: font] + let textSize = self.size(withAttributes: fontAttributes) + return textSize.width + } +} + +struct TruncatedTextView: View { + + @State + private var truncated: Bool = false + @State + private var fullSize: CGFloat = 0 + + private var font: Font = .body + private var lineLimit: Int = 3 + private var foregroundColor: Color = .primary + + let text: String + let seeMoreAction: () -> Void + let seeMoreText = "... \(L10n.seeMore)" + + public init(text: String, seeMoreAction: @escaping () -> Void) { + self.text = text + self.seeMoreAction = seeMoreAction + } + + public var body: some View { + ZStack(alignment: .bottomTrailing) { + Text(text) + .font(font) + .foregroundColor(foregroundColor) + .lineLimit(lineLimit) + .if(truncated) { text in + text.mask { + VStack(spacing: 0) { + Color.black + + HStack(spacing: 0) { + Color.black + + LinearGradient( + stops: [ + .init(color: .black, location: 0), + .init(color: .clear, location: 0.1), + ], + startPoint: .leading, + endPoint: .trailing + ) + .frame(width: seeMoreText.widthOfString(usingFont: font.toUIFont()) + 15) + } + .frame(height: seeMoreText.heightOfString(usingFont: font.toUIFont())) + } + } + } + + if truncated { + #if os(tvOS) + Text(seeMoreText) + .font(font) + .foregroundColor(.purple) + #else + Button { + seeMoreAction() + } label: { + Text(seeMoreText) + .font(font) + .foregroundColor(.purple) + } + #endif + } + } + .background { + ZStack { + if !truncated { + if fullSize != 0 { + Text(text) + .font(font) + .lineLimit(lineLimit) + .background { + GeometryReader { geo in + Color.clear + .onAppear { + if fullSize > geo.size.height { + self.truncated = true + } + } + } + } + } + + Text(text) + .font(font) + .lineLimit(10) + .fixedSize(horizontal: false, vertical: true) + .background { + GeometryReader { geo in + Color.clear + .onAppear { + self.fullSize = geo.size.height + } + } + } + } + } + .hidden() + } + } +} diff --git a/Swiftfin tvOS/Components/DotHStack.swift b/Swiftfin tvOS/Components/DotHStack.swift new file mode 100644 index 00000000..addab0a3 --- /dev/null +++ b/Swiftfin tvOS/Components/DotHStack.swift @@ -0,0 +1,234 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct DotHStack: View { + + private let items: [AnyView] + private let restItems: [AnyView] + private let alignment: HorizontalAlignment + + var body: some View { + HStack(spacing: 0) { + items.first + + ForEach(0 ..< restItems.count, id: \.self) { i in + + Circle() + .frame(width: 5, height: 5) + .padding(.horizontal) + + restItems[i] + } + } + } +} + +extension DotHStack { + + init( + _ data: Data, + id: KeyPath = \.self, + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: @escaping (Data.Element) -> Content + ) { + self.alignment = alignment + self.items = data.map { content($0[keyPath: id]).eraseToAnyView() } + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> A + ) { + self.alignment = alignment + self.items = [content().eraseToAnyView()] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B)> + ) { + self.alignment = alignment + let _content = content() + + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C)> + ) { + self.alignment = alignment + let _content = content() + + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C, D)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C, D, E)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F, G)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + _content.value.6.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () + -> TupleView<(A, B, C, D, E, F, G, H)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + _content.value.6.eraseToAnyView(), + _content.value.7.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () + -> TupleView<(A, B, C, D, E, F, G, H, I)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + _content.value.6.eraseToAnyView(), + _content.value.7.eraseToAnyView(), + _content.value.8.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init< + A: View, + B: View, + C: View, + D: View, + E: View, + F: View, + G: View, + H: View, + I: View, + J: View + >( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () + -> TupleView<( + A, + B, + C, + D, + E, + F, + G, + H, + I, + J + )> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + _content.value.6.eraseToAnyView(), + _content.value.7.eraseToAnyView(), + _content.value.8.eraseToAnyView(), + _content.value.9.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } +} diff --git a/Swiftfin tvOS/Components/EpisodesRowView/EpisodesRowCard.swift b/Swiftfin tvOS/Components/EpisodesRowView/EpisodesRowCard.swift deleted file mode 100644 index 1fa33862..00000000 --- a/Swiftfin tvOS/Components/EpisodesRowView/EpisodesRowCard.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct EpisodeRowCard: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - let viewModel: EpisodesRowManager - let episode: BaseItemDto - - var body: some View { - VStack { - Button { - itemRouter.route(to: \.item, episode) - } label: { - ImageView( - episode.getBackdropImage(maxWidth: 550), - blurHash: episode.getBackdropImageBlurHash() - ) - .mask(Rectangle().frame(width: 550, height: 308)) - .frame(width: 550, height: 308) - } - .buttonStyle(CardButtonStyle()) - - VStack(alignment: .leading) { - - VStack(alignment: .leading) { - Text(episode.getEpisodeLocator() ?? "") - .font(.caption) - .foregroundColor(.secondary) - Text(episode.name ?? "") - .font(.footnote) - .padding(.bottom, 1) - - if episode.unaired { - Text(episode.airDateLabel ?? L10n.noOverviewAvailable) - .font(.caption) - .foregroundColor(.secondary) - .fontWeight(.light) - .lineLimit(3) - } else { - Text(episode.overview ?? "") - .font(.caption) - .fontWeight(.light) - .lineLimit(4) - .fixedSize(horizontal: false, vertical: true) - } - } - - Spacer() - } - .padding() - .frame(width: 550) - } - .focusSection() - } -} diff --git a/Swiftfin tvOS/Components/EpisodesRowView/EpisodesRowView.swift b/Swiftfin tvOS/Components/EpisodesRowView/EpisodesRowView.swift deleted file mode 100644 index c834e6d6..00000000 --- a/Swiftfin tvOS/Components/EpisodesRowView/EpisodesRowView.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct EpisodesRowView: View where RowManager: EpisodesRowManager { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: RowManager - let onlyCurrentSeason: Bool - - var body: some View { - VStack(alignment: .leading) { - - Text(viewModel.selectedSeason?.name ?? L10n.episodes) - .font(.title3) - .padding(.horizontal, 50) - - ScrollView(.horizontal) { - ScrollViewReader { reader in - HStack(alignment: .top) { - if viewModel.isLoading { - VStack(alignment: .leading) { - - ZStack { - Color.secondary.ignoresSafeArea() - - ProgressView() - } - .mask(Rectangle().frame(width: 500, height: 280)) - .frame(width: 500, height: 280) - - VStack(alignment: .leading) { - Text("S-E-") - .font(.caption) - .foregroundColor(.secondary) - Text("--") - .font(.footnote) - .padding(.bottom, 1) - Text("--") - .font(.caption) - .fontWeight(.light) - .lineLimit(4) - } - .padding(.horizontal) - - Spacer() - } - .frame(width: 500) - .focusable() - } else if let selectedSeason = viewModel.selectedSeason { - if let seasonEpisodes = viewModel.seasonsEpisodes[selectedSeason] { - if seasonEpisodes.isEmpty { - VStack(alignment: .leading) { - - Color.secondary - .mask(Rectangle().frame(width: 500, height: 280)) - .frame(width: 500, height: 280) - - VStack(alignment: .leading) { - Text("--") - .font(.caption) - .foregroundColor(.secondary) - L10n.noEpisodesAvailable.text - .font(.footnote) - .padding(.bottom, 1) - } - .padding(.horizontal) - - Spacer() - } - .frame(width: 500) - .focusable() - } else { - ForEach(viewModel.seasonsEpisodes[selectedSeason]!, id: \.self) { episode in - EpisodeRowCard(viewModel: viewModel, episode: episode) - .id(episode.id) - } - } - } - } - } - .padding(.horizontal, 50) - .padding(.vertical) - .onChange(of: viewModel.selectedSeason) { _ in - if viewModel.selectedSeason?.id == viewModel.item.seasonId { - reader.scrollTo(viewModel.item.id) - } - } - .onChange(of: viewModel.seasonsEpisodes) { _ in - if viewModel.selectedSeason?.id == viewModel.item.seasonId { - reader.scrollTo(viewModel.item.id) - } - } - } - .edgesIgnoringSafeArea(.horizontal) - } - } - } -} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift index 7baf9608..9b580cdf 100644 --- a/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift +++ b/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift @@ -28,10 +28,10 @@ class DynamicCinematicBackgroundViewModel: ObservableObject { let backdropImage: URL - if item.itemType == .episode { - backdropImage = item.getSeriesBackdropImage(maxWidth: 1920) + if item.type == .episode { + backdropImage = item.seriesImageURL(.backdrop, maxWidth: 1920) } else { - backdropImage = item.getBackdropImage(maxWidth: 1920) + backdropImage = item.imageURL(.backdrop, maxWidth: 1920) } let options = ImageLoadingOptions(transition: .fadeIn(duration: 0.2)) diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift index 7d41417f..1da14e2d 100644 --- a/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift +++ b/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift @@ -12,27 +12,27 @@ import SwiftUI struct CinematicNextUpCardView: View { @EnvironmentObject - var homeRouter: HomeCoordinator.Router + private var homeRouter: HomeCoordinator.Router let item: BaseItemDto let showOverlay: Bool var body: some View { VStack(alignment: .leading) { Button { - homeRouter.route(to: \.modalItem, item) + homeRouter.route(to: \.item, item) } label: { ZStack(alignment: .bottomLeading) { - if item.itemType == .episode { - ImageView(sources: [ - item.getSeriesThumbImage(maxWidth: 350), - item.getSeriesBackdropImage(maxWidth: 350), + if item.type == .episode { + ImageView([ + item.seriesImageSource(.thumb, maxWidth: 350), + item.seriesImageSource(.backdrop, maxWidth: 350), ]) .frame(width: 350, height: 210) } else { ImageView([ - .init(url: item.getThumbImage(maxWidth: 350)), - .init(url: item.getBackdropImage(maxWidth: 350), blurHash: item.getBackdropImageBlurHash()), + item.imageSource(.thumb, maxWidth: 350), + item.imageSource(.backdrop, maxWidth: 350), ]) .frame(width: 350, height: 210) } diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift index 8b325750..b068d985 100644 --- a/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift +++ b/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift @@ -12,7 +12,7 @@ import SwiftUI struct CinematicResumeCardView: View { @EnvironmentObject - var homeRouter: HomeCoordinator.Router + private var homeRouter: HomeCoordinator.Router @ObservedObject var viewModel: HomeViewModel let item: BaseItemDto @@ -20,20 +20,20 @@ struct CinematicResumeCardView: View { var body: some View { VStack(alignment: .leading) { Button { - homeRouter.route(to: \.modalItem, item) + homeRouter.route(to: \.item, item) } label: { ZStack(alignment: .bottom) { - if item.itemType == .episode { - ImageView(sources: [ - item.getSeriesThumbImage(maxWidth: 350), - item.getSeriesBackdropImage(maxWidth: 350), + if item.type == .episode { + ImageView([ + item.seriesImageSource(.thumb, maxWidth: 350), + item.seriesImageSource(.backdrop, maxWidth: 350), ]) .frame(width: 350, height: 210) } else { ImageView([ - .init(url: item.getThumbImage(maxWidth: 350)), - .init(url: item.getBackdropImage(maxWidth: 350), blurHash: item.getBackdropImageBlurHash()), + item.imageSource(.thumb, maxWidth: 350), + item.imageSource(.backdrop, maxWidth: 350), ]) .frame(width: 350, height: 210) } @@ -54,7 +54,7 @@ struct CinematicResumeCardView: View { .foregroundColor(.white) HStack { - Color(UIColor.systemPurple) + Color.jellyfinPurple .frame(width: 350 * (item.userData?.playedPercentage ?? 0) / 100, height: 7) Spacer(minLength: 0) diff --git a/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift b/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift index f32d594e..54d234f8 100644 --- a/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift +++ b/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift @@ -54,7 +54,7 @@ struct HomeCinematicView: View { ZStack(alignment: .bottom) { CinematicBackgroundView(viewModel: backgroundViewModel) - .frame(height: UIScreen.main.bounds.height - 10) + .frame(height: UIScreen.main.bounds.height - 50) LinearGradient( stops: [ @@ -77,8 +77,8 @@ struct HomeCinematicView: View { .fontWeight(.medium) .foregroundColor(Color.secondary) } else { - if updatedSelectedItem?.itemType == .episode { - Text(updatedSelectedItem?.getEpisodeLocator() ?? "") + if updatedSelectedItem?.type == .episode { + Text(updatedSelectedItem?.episodeLocator ?? "") .font(.callout) .fontWeight(.medium) .foregroundColor(Color.secondary) diff --git a/Swiftfin tvOS/Components/ItemDetailsView.swift b/Swiftfin tvOS/Components/ItemDetailsView.swift index e31d3fb1..8804e4e4 100644 --- a/Swiftfin tvOS/Components/ItemDetailsView.swift +++ b/Swiftfin tvOS/Components/ItemDetailsView.swift @@ -8,6 +8,8 @@ import SwiftUI +// TODO: Replace and remove + struct ItemDetailsView: View { @ObservedObject @@ -28,9 +30,9 @@ struct ItemDetailsView: View { .font(.title3) .padding(.bottom, 5) - ForEach(viewModel.informationItems, id: \.self.title) { informationItem in - ItemDetail(title: informationItem.title, content: informationItem.content) - } + // ForEach(viewModel.informationItems, id: \.self.title) { informationItem in + // ItemDetail(title: informationItem.title, content: informationItem.content) + // } } Spacer() diff --git a/Swiftfin tvOS/Components/LandscapeItemElement.swift b/Swiftfin tvOS/Components/LandscapeItemElement.swift index 7a030398..9bef8c91 100644 --- a/Swiftfin tvOS/Components/LandscapeItemElement.swift +++ b/Swiftfin tvOS/Components/LandscapeItemElement.swift @@ -54,65 +54,61 @@ struct LandscapeItemElement: View { var body: some View { VStack { - ImageView( - item.type == .episode && !(inSeasonView ?? false) ? item.getSeriesBackdropImage(maxWidth: 445) : item - .getBackdropImage(maxWidth: 445), - blurHash: item.type == .episode ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash() - ) - .frame(width: 445, height: 250) - .cornerRadius(10) - .ignoresSafeArea() - .overlay( - ZStack { - if item.userData?.played ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - Image(systemName: "checkmark.circle.fill") - .foregroundColor(Color(.systemBlue)) - } - }.padding(2) - .opacity(1), - alignment: .topTrailing - ).opacity(1) - .overlay(ZStack(alignment: .leading) { - if focused && item.userData?.playedPercentage != nil { - Rectangle() - .fill(LinearGradient( - gradient: Gradient(colors: [.black, .clear]), - startPoint: .bottom, - endPoint: .top - )) - .frame(width: 445, height: 90) - .mask(CutOffShadow()) - VStack(alignment: .leading) { - Text("CONTINUE • \(item.getItemProgressString() ?? "")") - .font(.caption) - .fontWeight(.medium) - .offset(y: 5) - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 6) - .fill(Color.gray) - .opacity(0.4) - .frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12) - RoundedRectangle(cornerRadius: 6) - .fill(Color.jellyfinPurple) - .frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 4.45 - 0.16), height: 12) + ImageView(item.imageSource(.backdrop, maxWidth: 445)) + .frame(width: 445, height: 250) + .cornerRadius(10) + .ignoresSafeArea() + .overlay( + ZStack { + if item.userData?.played ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color(.systemBlue)) } - }.padding(12) - } else { - EmptyView() - } - }, alignment: .bottomLeading) - .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) - .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) + }.padding(2) + .opacity(1), + alignment: .topTrailing + ).opacity(1) + .overlay(ZStack(alignment: .leading) { + if focused && item.userData?.playedPercentage != nil { + Rectangle() + .fill(LinearGradient( + gradient: Gradient(colors: [.black, .clear]), + startPoint: .bottom, + endPoint: .top + )) + .frame(width: 445, height: 90) + .mask(CutOffShadow()) + VStack(alignment: .leading) { + Text("CONTINUE • \(item.getItemProgressString() ?? "")") + .font(.caption) + .fontWeight(.medium) + .offset(y: 5) + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 6) + .fill(Color.gray) + .opacity(0.4) + .frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12) + RoundedRectangle(cornerRadius: 6) + .fill(Color.jellyfinPurple) + .frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 4.45 - 0.16), height: 12) + } + }.padding(12) + } else { + EmptyView() + } + }, alignment: .bottomLeading) + .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) + .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) if inSeasonView ?? false { - Text("\(item.getEpisodeLocator() ?? "") • \(item.name ?? "")") + Text("\(item.episodeLocator ?? "") • \(item.name ?? "")") .font(.callout) .fontWeight(.semibold) .lineLimit(1) .frame(width: 445) } else { - Text(item.type == .episode ? "\(item.seriesName ?? "") • \(item.getEpisodeLocator() ?? "")" : item.name ?? "") + Text(item.type == .episode ? "\(item.seriesName ?? "") • \(item.episodeLocator ?? "")" : item.name ?? "") .font(.callout) .fontWeight(.semibold) .lineLimit(1) @@ -123,16 +119,6 @@ struct LandscapeItemElement: View { withAnimation(.linear(duration: 0.15)) { self.focused = envFocus } - - if envFocus == true { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - // your code here - if focused == true { - backgroundURL = item.getBackdropImage(maxWidth: 1080) - BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash()) - } - } - } } .scaleEffect(focused ? 1.1 : 1) } diff --git a/Swiftfin tvOS/Components/MediaPlayButtonRowView.swift b/Swiftfin tvOS/Components/MediaPlayButtonRowView.swift deleted file mode 100644 index 5ef2baeb..00000000 --- a/Swiftfin tvOS/Components/MediaPlayButtonRowView.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct MediaPlayButtonRowView: View { - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: ItemViewModel - @State - var wrappedScrollView: UIScrollView? - - var body: some View { - HStack { - VStack { - Button { - itemRouter.route(to: \.videoPlayer, viewModel.selectedVideoPlayerViewModel!) - } label: { - MediaViewActionButton(icon: "play.fill", scrollView: $wrappedScrollView) - } - - Text((viewModel.item.getItemProgressString() != nil) ? "\(viewModel.item.getItemProgressString() ?? "") left" : L10n.play) - .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) - } - VStack { - Button { - viewModel.updateFavoriteState() - } label: { - MediaViewActionButton( - icon: "heart.fill", - scrollView: $wrappedScrollView, - iconColor: viewModel.isFavorited ? .red : .white - ) - } - Text(viewModel.isFavorited ? "Unfavorite" : "Favorite") - .font(.caption) - } - Spacer() - } - } -} diff --git a/Swiftfin tvOS/Components/MediaViewActionButton.swift b/Swiftfin tvOS/Components/MediaViewActionButton.swift deleted file mode 100644 index 2b696273..00000000 --- a/Swiftfin tvOS/Components/MediaViewActionButton.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct MediaViewActionButton: View { - @Environment(\.isFocused) - var envFocused: Bool - @State - var focused: Bool = false - var icon: String - var scrollView: Binding? - var iconColor: Color? - - var body: some View { - Image(systemName: icon) - .foregroundColor(focused ? .black : iconColor ?? .white) - .onChange(of: envFocused) { envFocus in - if envFocus == true { - scrollView?.wrappedValue?.scrollToTop() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - scrollView?.wrappedValue?.scrollToTop() - } - } - - withAnimation(.linear(duration: 0.15)) { - self.focused = envFocus - } - } - .font(.system(size: 40)) - .padding(.vertical, 12).padding(.horizontal, 20) - } -} diff --git a/Swiftfin tvOS/Components/PlainLinkButton.swift b/Swiftfin tvOS/Components/PlainLinkButton.swift deleted file mode 100644 index 236882ad..00000000 --- a/Swiftfin tvOS/Components/PlainLinkButton.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct PlainLinkButton: View { - @Environment(\.isFocused) - var envFocused: Bool - @State - var focused: Bool = false - @State - var label: String - - var body: some View { - Text(label) - .fontWeight(focused ? .bold : .regular) - .foregroundColor(.blue) - .onChange(of: envFocused) { envFocus in - withAnimation(.linear(duration: 0.15)) { - self.focused = envFocus - } - } - .scaleEffect(focused ? 1.1 : 1) - } -} diff --git a/Swiftfin tvOS/Components/PortraitButton.swift b/Swiftfin tvOS/Components/PortraitButton.swift new file mode 100644 index 00000000..8f001216 --- /dev/null +++ b/Swiftfin tvOS/Components/PortraitButton.swift @@ -0,0 +1,58 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI +import SwiftUICollection + +struct PortraitButton: View { + + let item: Item + let selectedAction: (Item) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 15) { + Button { + selectedAction(item) + } label: { + ImageView( + item.portraitPosterImageSource(maxWidth: 300), + failureView: { + InitialFailureView(item.title.initials) + } + ) + .frame(width: 270, height: 405) + } + .buttonStyle(CardButtonStyle()) + + VStack(alignment: .leading) { + if item.showTitle { + HStack { + Text(item.title) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + .lineLimit(2) + .frame(width: 250) + + Spacer() + } + } + + if let subtitle = item.subtitle { + Text(subtitle) + .font(.caption) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + .zIndex(-1) + .frame(maxWidth: .infinity) + } + .focusSection() + } +} diff --git a/Swiftfin tvOS/Components/PortraitItemElement.swift b/Swiftfin tvOS/Components/PortraitItemElement.swift index 2981c214..b3d280df 100644 --- a/Swiftfin tvOS/Components/PortraitItemElement.swift +++ b/Swiftfin tvOS/Components/PortraitItemElement.swift @@ -9,7 +9,9 @@ import JellyfinAPI import SwiftUI +// TODO: Transition to `PortraitButton` struct PortraitItemElement: View { + @Environment(\.isFocused) var envFocused: Bool @State @@ -21,49 +23,46 @@ struct PortraitItemElement: View { var body: some View { VStack { - ImageView( - item.type == .episode ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200), - blurHash: item.type == .episode ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash() - ) - .frame(width: 200, height: 300) - .cornerRadius(10) - .shadow(radius: focused ? 10.0 : 0) - .shadow(radius: focused ? 10.0 : 0) - .overlay( - ZStack { - if item.userData?.isFavorite ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - .opacity(0.6) - Image(systemName: "heart.fill") - .foregroundColor(Color(.systemRed)) - .font(.system(size: 10)) - } - } - .padding(2) - .opacity(1), - alignment: .bottomLeading - ) - .overlay( - ZStack { - if item.userData?.played ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - Image(systemName: "checkmark.circle.fill") - .foregroundColor(Color(.systemBlue)) - } else { - if item.userData?.unplayedItemCount != nil { + ImageView(item.type == .episode ? item.seriesImageSource(.primary, maxWidth: 200) : item.imageSource(.primary, maxWidth: 200)) + .frame(width: 200, height: 300) + .cornerRadius(10) + .shadow(radius: focused ? 10.0 : 0) + .shadow(radius: focused ? 10.0 : 0) + .overlay( + ZStack { + if item.userData?.isFavorite ?? false { Image(systemName: "circle.fill") - .foregroundColor(Color(.systemBlue)) - Text(String(item.userData!.unplayedItemCount ?? 0)) .foregroundColor(.white) - .font(.caption2) + .opacity(0.6) + Image(systemName: "heart.fill") + .foregroundColor(Color(.systemRed)) + .font(.system(size: 10)) } } - }.padding(2) + .padding(2) .opacity(1), - alignment: .topTrailing - ).opacity(1) + alignment: .bottomLeading + ) + .overlay( + ZStack { + if item.userData?.played ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color(.systemBlue)) + } else { + if item.userData?.unplayedItemCount != nil { + Image(systemName: "circle.fill") + .foregroundColor(Color(.systemBlue)) + Text(String(item.userData!.unplayedItemCount ?? 0)) + .foregroundColor(.white) + .font(.caption2) + } + } + }.padding(2) + .opacity(1), + alignment: .topTrailing + ).opacity(1) Text(item.title) .frame(width: 200, height: 30, alignment: .center) if item.type == .movie || item.type == .series { @@ -87,16 +86,6 @@ struct PortraitItemElement: View { withAnimation(.linear(duration: 0.15)) { self.focused = envFocus } - - if envFocus == true { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - // your code here - if focused == true { - backgroundURL = item.getBackdropImage(maxWidth: 1080) - BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash()) - } - } - } } .scaleEffect(focused ? 1.1 : 1) } diff --git a/Swiftfin tvOS/Components/PortraitItemsRowView.swift b/Swiftfin tvOS/Components/PortraitItemsRowView.swift deleted file mode 100644 index 9cfa6cd9..00000000 --- a/Swiftfin tvOS/Components/PortraitItemsRowView.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct PortraitItemsRowView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - - let rowTitle: String - let items: [BaseItemDto] - let showItemTitles: Bool - let selectedAction: (BaseItemDto) -> Void - - init( - rowTitle: String, - items: [BaseItemDto], - showItemTitles: Bool = true, - selectedAction: @escaping (BaseItemDto) -> Void - ) { - self.rowTitle = rowTitle - self.items = items - self.showItemTitles = showItemTitles - self.selectedAction = selectedAction - } - - var body: some View { - VStack(alignment: .leading) { - - Text(rowTitle) - .font(.title3) - .padding(.horizontal, 50) - - ScrollView(.horizontal) { - HStack(alignment: .top) { - ForEach(items, id: \.self) { item in - - VStack(spacing: 15) { - Button { - selectedAction(item) - } label: { - ImageView(item.portraitHeaderViewURL(maxWidth: 257)) - .frame(width: 257, height: 380) - } - .frame(height: 380) - .buttonStyle(PlainButtonStyle()) - - if showItemTitles { - Text(item.title) - .lineLimit(2) - .frame(width: 257) - } - } - } - } - .padding(.horizontal, 50) - .padding(.vertical) - } - .edgesIgnoringSafeArea(.horizontal) - } - .focusSection() - } -} diff --git a/Swiftfin tvOS/Components/PortraitPosterHStack.swift b/Swiftfin tvOS/Components/PortraitPosterHStack.swift new file mode 100644 index 00000000..749646ac --- /dev/null +++ b/Swiftfin tvOS/Components/PortraitPosterHStack.swift @@ -0,0 +1,89 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI +import SwiftUICollection +import TVUIKit + +struct PortraitPosterHStack: View { + + private let loading: Bool + private let title: String + private let items: [Item] + private let selectedAction: (Item) -> Void + private let trailingContent: () -> TrailingContent + + init( + loading: Bool = false, + title: String, + items: [Item], + @ViewBuilder trailingContent: @escaping () -> TrailingContent, + selectedAction: @escaping (Item) -> Void + ) { + self.loading = loading + self.title = title + self.items = items + self.trailingContent = trailingContent + self.selectedAction = selectedAction + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + + Text(title) + .font(.title3) + .fontWeight(.semibold) + .padding(.leading, 50) + + ScrollView(.horizontal) { + HStack(alignment: .top, spacing: 0) { + if loading { + ForEach(0 ..< 10) { _ in + PortraitButton( + item: BaseItemDto.placeHolder, + selectedAction: { _ in } + ) + .redacted(reason: .placeholder) + } + } else if items.isEmpty { + PortraitButton( + item: BaseItemDto.noResults, + selectedAction: { _ in } + ) + } else { + ForEach(items, id: \.hashValue) { item in + PortraitButton(item: item) { item in + selectedAction(item) + } + } + } + + trailingContent() + } + .padding(.horizontal, 50) + .padding2(.vertical) + } + } + } +} + +extension PortraitPosterHStack where TrailingContent == EmptyView { + init( + loading: Bool = false, + title: String, + items: [Item], + selectedAction: @escaping (Item) -> Void + ) { + self.loading = loading + self.title = title + self.items = items + self.trailingContent = { EmptyView() } + self.selectedAction = selectedAction + } +} diff --git a/Swiftfin tvOS/Components/PublicUserButton.swift b/Swiftfin tvOS/Components/PublicUserButton.swift deleted file mode 100644 index af8af7e3..00000000 --- a/Swiftfin tvOS/Components/PublicUserButton.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import CoreMedia -import JellyfinAPI -import SwiftUI - -struct PublicUserButton: View { - @Environment(\.isFocused) - var envFocused: Bool - @State - var focused: Bool = false - var publicUser: UserDto - - var body: some View { - VStack { - if publicUser.primaryImageTag != nil { - ImageView( - URL( - string: "\(SessionManager.main.currentLogin.server.currentURI)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)" - )! - ) - .frame(width: 250, height: 250) - .cornerRadius(125.0) - } else { - Image(systemName: "person.fill") - .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) - .font(.system(size: 35)) - .frame(width: 250, height: 250) - .background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255)) - .cornerRadius(125.0) - .shadow(radius: 6) - } - if focused { - Text(publicUser.name ?? "").font(.headline).fontWeight(.semibold) - } else { - Spacer().frame(height: 60) - } - }.onChange(of: envFocused) { envFocus in - withAnimation(.linear(duration: 0.15)) { - self.focused = envFocus - } - }.scaleEffect(focused ? 1.1 : 1) - } -} diff --git a/Swiftfin tvOS/Objects/FocusGuide.swift b/Swiftfin tvOS/Objects/FocusGuide.swift new file mode 100644 index 00000000..f0c2898f --- /dev/null +++ b/Swiftfin tvOS/Objects/FocusGuide.swift @@ -0,0 +1,149 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct FocusGuideModifier: ViewModifier { + + @FocusState + var focusDirection: FocusDirection? + @EnvironmentObject + var focusGuide: FocusGuide + + let focusConstructor: FocusConstructor + let onContentFocus: (() -> Void)? + + let debug = false + + func body(content: Content) -> some View { + VStack(spacing: 0) { + + Color(debug ? .red : .clear) + .frame(height: 1) + .if(focusConstructor.topTarget != nil, transform: { boundary in + boundary.focusable() + }) + .focused($focusDirection, equals: .top) + + HStack(spacing: 0) { + Color(debug ? .red : .clear) + .frame(width: 1) + .if(focusConstructor.leftTarget != nil, transform: { boundary in + boundary.focusable() + }) + .focused($focusDirection, equals: .left) + + content + .focused($focusDirection, equals: .content) + + Color(debug ? .red : .clear) + .frame(width: 1) + .if(focusConstructor.rightTarget != nil, transform: { boundary in + boundary.focusable() + }) + .focused($focusDirection, equals: .right) + } + + Color(debug ? .red : .clear) + .frame(height: 1) + .if(focusConstructor.bottomTarget != nil, transform: { boundary in + boundary.focusable() + }) + .focused($focusDirection, equals: .bottom) + } + .onChange(of: focusDirection) { focusDirection in + guard let focusDirection = focusDirection else { return } + switch focusDirection { + case .top: + focusGuide.transition(to: focusConstructor.topTarget!) + case .bottom: + focusGuide.transition(to: focusConstructor.bottomTarget!) + case .left: + focusGuide.transition(to: focusConstructor.leftTarget!) + case .right: + focusGuide.transition(to: focusConstructor.rightTarget!) + case .content: () + } + } + .onChange(of: focusGuide.focusedTag) { newTag in + if newTag == focusConstructor.tag { + if let onContentFocus = onContentFocus { + onContentFocus() + } else { + focusDirection = .content + } + } + } + } +} + +extension View { + func focusGuide( + _ focusGuide: FocusGuide, + tag: String, + onContentFocus: (() -> Void)? = nil, + top: String? = nil, + bottom: String? = nil, + left: String? = nil, + right: String? = nil + ) -> some View { + let focusConstructor = FocusConstructor( + tag: tag, + topTarget: top, + bottomTarget: bottom, + leftTarget: left, + rightTarget: right + ) + return modifier(FocusGuideModifier(focusConstructor: focusConstructor, onContentFocus: onContentFocus)) + .environmentObject(focusGuide) + } +} + +enum FocusDirection: String { + case top + case bottom + case content + case left + case right +} + +struct FocusConstructor { + + let tag: String + let topTarget: String? + let bottomTarget: String? + let leftTarget: String? + let rightTarget: String? + + init( + tag: String, + topTarget: String?, + bottomTarget: String?, + leftTarget: String?, + rightTarget: String? + ) { + self.tag = tag + self.topTarget = topTarget + self.bottomTarget = bottomTarget + self.leftTarget = leftTarget + self.rightTarget = rightTarget + } +} + +class FocusGuide: ObservableObject { + + @Published + private(set) var focusedTag: String? + + private(set) var lastFocusedTag: String? + + func transition(to tag: String?) { + lastFocusedTag = focusedTag + focusedTag = tag + } +} diff --git a/Swiftfin tvOS/Views/AboutView.swift b/Swiftfin tvOS/Views/AboutAppView.swift similarity index 92% rename from Swiftfin tvOS/Views/AboutView.swift rename to Swiftfin tvOS/Views/AboutAppView.swift index fbe2fa7a..0fa86c4c 100644 --- a/Swiftfin tvOS/Views/AboutView.swift +++ b/Swiftfin tvOS/Views/AboutAppView.swift @@ -8,7 +8,7 @@ import SwiftUI -struct AboutView: View { +struct AboutAppView: View { var body: some View { Text("dud") diff --git a/Swiftfin tvOS/Views/BasicAppSettingsView.swift b/Swiftfin tvOS/Views/BasicAppSettingsView.swift index 9f111845..e1174ea3 100644 --- a/Swiftfin tvOS/Views/BasicAppSettingsView.swift +++ b/Swiftfin tvOS/Views/BasicAppSettingsView.swift @@ -13,7 +13,7 @@ import SwiftUI struct BasicAppSettingsView: View { @EnvironmentObject - var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router + private var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router @ObservedObject var viewModel: BasicAppSettingsViewModel @State diff --git a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift b/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift index 560e3232..f7d35a9f 100644 --- a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift +++ b/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift @@ -12,21 +12,24 @@ import SwiftUI struct ContinueWatchingCard: View { @EnvironmentObject - var homeRouter: HomeCoordinator.Router + private var homeRouter: HomeCoordinator.Router let item: BaseItemDto var body: some View { VStack(alignment: .leading) { Button { - homeRouter.route(to: \.modalItem, item) + homeRouter.route(to: \.item, item) } label: { ZStack(alignment: .bottom) { - if item.itemType == .episode { - ImageView(item.getSeriesBackdropImage(maxWidth: 500)) - .frame(width: 500, height: 281.25) + if item.type == .episode { + ImageView([ + item.seriesImageSource(.thumb, maxWidth: 500), + item.imageSource(.primary, maxWidth: 500), + ]) + .frame(width: 500, height: 281.25) } else { - ImageView(item.getBackdropImage(maxWidth: 500)) + ImageView(item.imageURL(.backdrop, maxWidth: 500)) .frame(width: 500, height: 281.25) } @@ -66,8 +69,8 @@ struct ContinueWatchingCard: View { .lineLimit(1) .frame(width: 500, alignment: .leading) - if item.itemType == .episode { - Text(item.getEpisodeLocator() ?? "") + if item.type == .episode { + Text(item.episodeLocator ?? "--") .font(.callout) .fontWeight(.medium) .foregroundColor(.secondary) diff --git a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingView.swift b/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingView.swift index 84138a66..389e7225 100644 --- a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingView.swift +++ b/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingView.swift @@ -14,7 +14,7 @@ import SwiftUI struct ContinueWatchingView: View { @EnvironmentObject - var homeRouter: HomeCoordinator.Router + private var homeRouter: HomeCoordinator.Router let items: [BaseItemDto] var body: some View { diff --git a/Swiftfin tvOS/Views/HomeView.swift b/Swiftfin tvOS/Views/HomeView.swift index 8a41da9b..5183e114 100644 --- a/Swiftfin tvOS/Views/HomeView.swift +++ b/Swiftfin tvOS/Views/HomeView.swift @@ -8,20 +8,16 @@ import Defaults import Foundation +import Introspect import JellyfinAPI import SwiftUI struct HomeView: View { @EnvironmentObject - var homeRouter: HomeCoordinator.Router - @StateObject - var viewModel = HomeViewModel() - @Default(.showPosterLabels) - var showPosterLabels - - @State - var showingSettings = false + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: HomeViewModel var body: some View { if viewModel.isLoading { @@ -39,8 +35,12 @@ struct HomeView: View { ) if !viewModel.nextUpItems.isEmpty { - NextUpView(items: viewModel.nextUpItems) - .focusSection() + PortraitPosterHStack( + title: L10n.nextUp, + items: viewModel.nextUpItems + ) { item in + router.route(to: \.item, item) + } } } else { HomeCinematicView( @@ -49,38 +49,27 @@ struct HomeView: View { ) if !viewModel.nextUpItems.isEmpty { - NextUpView(items: viewModel.nextUpItems) - .focusSection() + PortraitPosterHStack( + title: L10n.nextUp, + items: viewModel.nextUpItems + ) { item in + router.route(to: \.item, item) + } } - PortraitItemsRowView( - rowTitle: L10n.recentlyAdded, - items: viewModel.latestAddedItems, - showItemTitles: showPosterLabels - ) { item in - homeRouter.route(to: \.modalItem, item) + if !viewModel.latestAddedItems.isEmpty { + PortraitPosterHStack( + title: L10n.recentlyAdded, + items: viewModel.latestAddedItems + ) { item in + router.route(to: \.item, item) + } } } ForEach(viewModel.libraries, id: \.self) { library in - LatestMediaView(viewModel: LatestMediaViewModel(library: library)) - .focusSection() + LatestInLibraryView(viewModel: LatestMediaViewModel(library: library)) } - - Spacer(minLength: 100) - - HStack { - Spacer() - - Button { - viewModel.refresh() - } label: { - L10n.refresh.text - } - - Spacer() - } - .focusSection() } } .edgesIgnoringSafeArea(.top) diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicCollectionItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift similarity index 61% rename from Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicCollectionItemView.swift rename to Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift index f2dbace1..d9501d88 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicCollectionItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift @@ -46,28 +46,24 @@ struct CinematicCollectionItemView: View { Color.black.ignoresSafeArea() - VStack(alignment: .leading, spacing: 20) { - - CinematicItemAboutView(viewModel: viewModel) - - PortraitItemsRowView( - rowTitle: L10n.items, - items: viewModel.collectionItems - ) { item in - itemRouter.route(to: \.item, item) - } - - if !viewModel.similarItems.isEmpty { - PortraitItemsRowView( - rowTitle: L10n.recommended, - items: viewModel.similarItems, - showItemTitles: showPosterLabels - ) { item in - itemRouter.route(to: \.item, item) - } - } - } - .padding(.vertical, 50) + // VStack(alignment: .leading, spacing: 20) { +// + // CinematicItemAboutView(viewModel: viewModel) +// + // PortraitImageHStack(rowTitle: L10n.items, + // items: viewModel.collectionItems) { item in + // itemRouter.route(to: \.item, item) + // } +// + // if !viewModel.similarItems.isEmpty { + // PortraitImageHStack(rowTitle: L10n.recommended, + // items: viewModel.similarItems, + // showItemTitles: showPosterLabels) { item in + // itemRouter.route(to: \.item, item) + // } + // } + // } + // .padding(.vertical, 50) } } } diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicEpisodeItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift similarity index 66% rename from Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicEpisodeItemView.swift rename to Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift index cd924aee..b1f8f599 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicEpisodeItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift @@ -22,7 +22,7 @@ struct CinematicEpisodeItemView: View { var showPosterLabels func generateSubtitle() -> String? { - guard let seriesName = viewModel.item.seriesName, let episodeLocator = viewModel.item.getEpisodeLocator() else { + guard let seriesName = viewModel.item.seriesName, let episodeLocator = viewModel.item.episodeLocator else { return nil } @@ -60,27 +60,23 @@ struct CinematicEpisodeItemView: View { CinematicItemAboutView(viewModel: viewModel) - EpisodesRowView(viewModel: viewModel, onlyCurrentSeason: true) - .focusSection() + // EpisodesRowView(viewModel: viewModel, onlyCurrentSeason: true) + // .focusSection() - if let seriesItem = viewModel.series { - PortraitItemsRowView( - rowTitle: L10n.series, - items: [seriesItem] - ) { seriesItem in - itemRouter.route(to: \.item, seriesItem) - } - } + // if let seriesItem = viewModel.series { + // PortraitItemsRowView(rowTitle: L10n.series, + // items: [seriesItem]) { seriesItem in + // itemRouter.route(to: \.item, seriesItem) + // } + // } - if !viewModel.similarItems.isEmpty { - PortraitItemsRowView( - rowTitle: L10n.recommended, - items: viewModel.similarItems, - showItemTitles: showPosterLabels - ) { item in - itemRouter.route(to: \.item, item) - } - } + // if !viewModel.similarItems.isEmpty { + // PortraitImageHStack(rowTitle: L10n.recommended, + // items: viewModel.similarItems, + // showItemTitles: showPosterLabels) { item in + // itemRouter.route(to: \.item, item) + // } + // } ItemDetailsView(viewModel: viewModel) } diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemAboutView.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift similarity index 100% rename from Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemAboutView.swift rename to Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRowButton.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRowButton.swift deleted file mode 100644 index 16566153..00000000 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRowButton.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct CinematicItemViewTopRowButton: View { - @Environment(\.isFocused) - var envFocused: Bool - @State - var focused: Bool = false - @State - var wrappedScrollView: UIScrollView? - var content: () -> Content - - @FocusState - private var buttonFocused: Bool - - var body: some View { - content() - .focused($buttonFocused) - .onChange(of: envFocused) { envFocus in - if envFocus == true { - wrappedScrollView?.scrollToTop() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - wrappedScrollView?.scrollToTop() - } - } - - withAnimation(.linear(duration: 0.15)) { - self.focused = envFocus - } - } - .onChange(of: buttonFocused) { newValue in - if newValue { - wrappedScrollView?.scrollToTop() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - wrappedScrollView?.scrollToTop() - } - - withAnimation(.linear(duration: 0.15)) { - self.focused = newValue - } - } - } - } -} diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicMovieItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicMovieItemView.swift deleted file mode 100644 index b6ca0df4..00000000 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicMovieItemView.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import Introspect -import SwiftUI - -struct CinematicMovieItemView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: MovieItemViewModel - @State - var wrappedScrollView: UIScrollView? - @Default(.showPosterLabels) - var showPosterLabels - - var body: some View { - ZStack { - - ImageView( - viewModel.item.getBackdropImage(maxWidth: 1920), - blurHash: viewModel.item.getBackdropImageBlurHash() - ) - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 0) { - - CinematicItemViewTopRow( - viewModel: viewModel, - wrappedScrollView: wrappedScrollView, - title: viewModel.item.name ?? "", - subtitle: nil - ) - .focusSection() - .frame(height: UIScreen.main.bounds.height - 10) - - ZStack(alignment: .topLeading) { - - Color.black.ignoresSafeArea() - - VStack(alignment: .leading, spacing: 20) { - - CinematicItemAboutView(viewModel: viewModel) - - if !viewModel.similarItems.isEmpty { - PortraitItemsRowView( - rowTitle: L10n.recommended, - items: viewModel.similarItems, - showItemTitles: showPosterLabels - ) { item in - itemRouter.route(to: \.item, item) - } - } - - ItemDetailsView(viewModel: viewModel) - } - .padding(.top, 50) - } - } - } - .introspectScrollView { scrollView in - wrappedScrollView = scrollView - } - .ignoresSafeArea() - } - } -} diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicSeasonItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicSeasonItemView.swift deleted file mode 100644 index 582ba340..00000000 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicSeasonItemView.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import SwiftUI - -struct CinematicSeasonItemView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: SeasonItemViewModel - @State - var wrappedScrollView: UIScrollView? - @Default(.showPosterLabels) - var showPosterLabels - - var body: some View { - ZStack { - - ImageView(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash()) - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 0) { - - if let seriesItem = viewModel.seriesItem { - CinematicItemViewTopRow( - viewModel: viewModel, - wrappedScrollView: wrappedScrollView, - title: viewModel.item.name ?? "", - subtitle: seriesItem.name - ) - .focusSection() - .frame(height: UIScreen.main.bounds.height - 10) - } else { - CinematicItemViewTopRow( - viewModel: viewModel, - wrappedScrollView: wrappedScrollView, - title: viewModel.item.name ?? "" - ) - .focusSection() - .frame(height: UIScreen.main.bounds.height - 10) - } - - ZStack(alignment: .topLeading) { - - Color.black.ignoresSafeArea() - .frame(minHeight: UIScreen.main.bounds.height) - - VStack(alignment: .leading, spacing: 20) { - - CinematicItemAboutView(viewModel: viewModel) - - EpisodesRowView(viewModel: viewModel, onlyCurrentSeason: true) - .focusSection() - - if let seriesItem = viewModel.seriesItem { - PortraitItemsRowView( - rowTitle: L10n.series, - items: [seriesItem] - ) { seriesItem in - itemRouter.route(to: \.item, seriesItem) - } - } - - if !viewModel.similarItems.isEmpty { - PortraitItemsRowView( - rowTitle: L10n.recommended, - items: viewModel.similarItems, - showItemTitles: showPosterLabels - ) { item in - itemRouter.route(to: \.item, item) - } - } - } - .padding(.vertical, 50) - } - } - } - .introspectScrollView { scrollView in - wrappedScrollView = scrollView - } - .ignoresSafeArea() - } - } -} diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicSeriesItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicSeriesItemView.swift deleted file mode 100644 index 7be04155..00000000 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicSeriesItemView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import SwiftUI - -struct CinematicSeriesItemView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: SeriesItemViewModel - @State - var wrappedScrollView: UIScrollView? - @Default(.showPosterLabels) - var showPosterLabels - - var body: some View { - ZStack { - - ImageView(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash()) - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 0) { - - CinematicItemViewTopRow( - viewModel: viewModel, - wrappedScrollView: wrappedScrollView, - title: viewModel.item.name ?? "", - subtitle: nil - ) - .focusSection() - .frame(height: UIScreen.main.bounds.height - 10) - - ZStack(alignment: .topLeading) { - - Color.black.ignoresSafeArea() - .frame(minHeight: UIScreen.main.bounds.height) - - VStack(alignment: .leading, spacing: 20) { - - CinematicItemAboutView(viewModel: viewModel) - - PortraitItemsRowView( - rowTitle: L10n.seasons, - items: viewModel.seasons, - showItemTitles: showPosterLabels - ) { season in - itemRouter.route(to: \.item, season) - } - - if !viewModel.similarItems.isEmpty { - PortraitItemsRowView( - rowTitle: L10n.recommended, - items: viewModel.similarItems, - showItemTitles: showPosterLabels - ) { item in - itemRouter.route(to: \.item, item) - } - } - } - .padding(.vertical, 50) - } - } - } - .introspectScrollView { scrollView in - wrappedScrollView = scrollView - } - .ignoresSafeArea() - } - } -} diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift similarity index 63% rename from Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift rename to Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift index 7a031001..553e6e2c 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift @@ -57,49 +57,9 @@ struct CinematicItemViewTopRow: View { HStack(alignment: .bottom) { VStack(alignment: .leading) { - HStack(alignment: .PlayInformationAlignmentGuide) { - - // MARK: Play - - Button { - if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { - itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) - } else { - LogManager.log.error("Attempted to play item but no playback information available") - } - } label: { - HStack(spacing: 15) { - Image(systemName: "play.fill") - .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black) - .font(.title3) - Text(playButtonText) - .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black) - .fontWeight(.semibold) - } - .frame(width: 230, height: 100) - .background(viewModel.playButtonItem == nil ? Color.secondarySystemFill : Color.white) - .cornerRadius(10) - } - .buttonStyle(CardButtonStyle()) - .contextMenu { - if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 { - Button { - if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { - selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true) - itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) - } else { - LogManager.log.error("Attempted to play item but no playback information available") - } - } label: { - Label(L10n.playFromBeginning, systemImage: "gobackward") - } - - Button(role: .cancel) {} label: { - L10n.cancel.text - } - } - } - } + // HStack(alignment: .PlayInformationAlignmentGuide) { +// + // } } VStack(alignment: .leading, spacing: 5) { @@ -111,7 +71,7 @@ struct CinematicItemViewTopRow: View { Text(subtitle) } - HStack(alignment: .PlayInformationAlignmentGuide, spacing: 20) { + HStack(spacing: 20) { if showDetails { if viewModel.item.itemType == .series { @@ -191,14 +151,3 @@ struct CinematicItemViewTopRow: View { } } } - -extension VerticalAlignment { - - private struct PlayInformationAlignment: AlignmentID { - static func defaultValue(in context: ViewDimensions) -> CGFloat { - context[VerticalAlignment.bottom] - } - } - - static let PlayInformationAlignmentGuide = VerticalAlignment(PlayInformationAlignment.self) -} diff --git a/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift new file mode 100644 index 00000000..485a654c --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift @@ -0,0 +1,84 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +struct CinematicSeasonItemView: View { + + @EnvironmentObject + var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: SeasonItemViewModel + @State + var wrappedScrollView: UIScrollView? + @Default(.showPosterLabels) + var showPosterLabels + + var body: some View { + ZStack { + + ImageView(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash()) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 0) { + + // if let seriesItem = viewModel.seriesItem { + // CinematicItemViewTopRow(viewModel: viewModel, + // wrappedScrollView: wrappedScrollView, + // title: viewModel.item.name ?? "", + // subtitle: seriesItem.name) + // .focusSection() + // .frame(height: UIScreen.main.bounds.height - 10) + // } else { + // CinematicItemViewTopRow(viewModel: viewModel, + // wrappedScrollView: wrappedScrollView, + // title: viewModel.item.name ?? "") + // .focusSection() + // .frame(height: UIScreen.main.bounds.height - 10) + // } + + ZStack(alignment: .topLeading) { + + Color.black.ignoresSafeArea() + .frame(minHeight: UIScreen.main.bounds.height) + + VStack(alignment: .leading, spacing: 20) { + + CinematicItemAboutView(viewModel: viewModel) + + // EpisodesRowView(viewModel: viewModel, onlyCurrentSeason: true) + // .focusSection() + + // if let seriesItem = viewModel.seriesItem { + // PortraitItemsRowView(rowTitle: L10n.series, + // items: [seriesItem]) { seriesItem in + // itemRouter.route(to: \.item, seriesItem) + // } + // } + + // if !viewModel.similarItems.isEmpty { + // PortraitImageHStack(rowTitle: L10n.recommended, + // items: viewModel.similarItems, + // showItemTitles: showPosterLabels) { item in + // itemRouter.route(to: \.item, item) + // } + // } + } + .padding(.vertical, 50) + } + } + } + .introspectScrollView { scrollView in + wrappedScrollView = scrollView + } + .ignoresSafeArea() + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift new file mode 100644 index 00000000..01d6debe --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift @@ -0,0 +1,111 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension CollectionItemView { + + struct ContentView: View { + + @ObservedObject + var viewModel: CollectionItemViewModel + @State + var scrollViewProxy: ScrollViewProxy + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + private var focusGuide = FocusGuide() + @State + private var showLogo: Bool = false + + var body: some View { + VStack(spacing: 0) { + + ItemView.CinematicHeaderView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "mediaButtons", bottom: "items") + .frame(height: UIScreen.main.bounds.height - 150) + .padding(.bottom, 50) + + VStack(spacing: 0) { + + Color.clear + .frame(height: 0.5) + .id("topContentDivider") + + if showLogo { + ImageView( + viewModel.item.imageSource(.logo, maxWidth: 500), + resizingMode: .aspectFit, + failureView: { + Text(viewModel.item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + .lineLimit(2) + .multilineTextAlignment(.leading) + .foregroundColor(.white) + } + ) + .frame(width: 500, height: 150) + .padding(.top, 5) + } + + PortraitPosterHStack( + title: L10n.items, + items: viewModel.collectionItems + ) { item in + itemRouter.route(to: \.item, item) + } + .focusGuide(focusGuide, tag: "items", top: "mediaButtons", bottom: "about") + + ItemView.AboutView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "about", top: "items") + + Spacer() + } + .frame(minHeight: UIScreen.main.bounds.height) + } + .background { + BlurView(style: .dark) + .mask { + VStack(spacing: 0) { + LinearGradient( + stops: [ + .init(color: .clear, location: 0.5), + .init(color: .white.opacity(0.8), location: 0.7), + .init(color: .white.opacity(0.8), location: 0.95), + .init(color: .white, location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: UIScreen.main.bounds.height - 150) + + Color.white + } + } + } + .onChange(of: focusGuide.focusedTag) { newTag in + if newTag == "items" && !showLogo { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation(.easeIn(duration: 0.35)) { + scrollViewProxy.scrollTo("topContentDivider") + } + } + withAnimation { + self.showLogo = true + } + } else if newTag == "mediaButtons" { + withAnimation { + self.showLogo = false + } + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift new file mode 100644 index 00000000..0cf9e67a --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift @@ -0,0 +1,21 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct CollectionItemView: View { + + @ObservedObject + var viewModel: CollectionItemViewModel + + var body: some View { + ItemView.CinematicScrollView(viewModel: viewModel) { scrollViewProxy in + ContentView(viewModel: viewModel, scrollViewProxy: scrollViewProxy) + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/CompactItemView/EpisodeItemView.swift b/Swiftfin tvOS/Views/ItemView/CompactItemView/EpisodeItemView.swift deleted file mode 100644 index 36bb84cc..00000000 --- a/Swiftfin tvOS/Views/ItemView/CompactItemView/EpisodeItemView.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct EpisodeItemView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: EpisodeItemViewModel - - @State - var actors: [BaseItemPerson] = [] - @State - var studio: String? - @State - var director: String? - - 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(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash()) - .opacity(0.4) - .ignoresSafeArea() - 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) - } - if let runtime = viewModel.item.getItemRuntime() { - Text(runtime).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) - ) - } - Spacer() - }.padding(.top, -15) - - HStack(alignment: .top) { - VStack(alignment: .trailing) { - if studio != nil { - L10n.studio.text - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - Text(studio!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(.bottom, 40) - } - - if director != nil { - L10n.director.text - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - Text(director!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(.bottom, 40) - } - - if !actors.isEmpty { - L10n.cast.text - .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) { - Text(viewModel.item.overview ?? "") - .font(.body) - .fontWeight(.medium) - .foregroundColor(.primary) - - MediaPlayButtonRowView(viewModel: viewModel) - .environmentObject(itemRouter) - } - }.padding(.top, 50) - - if !viewModel.similarItems.isEmpty { - L10n.moreLikeThis.text - .font(.headline) - .fontWeight(.semibold) - ScrollView(.horizontal) { - LazyHStack { - Spacer().frame(width: 45) - ForEach(viewModel.similarItems, id: \.id) { similarItem in - Button { - itemRouter.route(to: \.item, similarItem) - } label: { - PortraitItemElement(item: similarItem) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - } - Spacer().frame(width: 45) - } - }.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90)) - .frame(height: 360) - } - Spacer() - Spacer() - }.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90)) - }.onAppear(perform: onAppear) - } -} diff --git a/Swiftfin tvOS/Views/ItemView/CompactItemView/MovieItemView.swift b/Swiftfin tvOS/Views/ItemView/CompactItemView/MovieItemView.swift deleted file mode 100644 index c30c247c..00000000 --- a/Swiftfin tvOS/Views/ItemView/CompactItemView/MovieItemView.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct MovieItemView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: MovieItemViewModel - - @State - var actors: [BaseItemPerson] = [] - @State - var studio: String? - @State - var director: String? - @State - var wrappedScrollView: UIScrollView? - - @Namespace - private var namespace - - 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(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash()) - .opacity(0.4) - .ignoresSafeArea() - ScrollView { - LazyVStack(alignment: .leading) { - Text(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 let runtime = viewModel.item.getItemRuntime() { - Text(runtime).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) - ) - } - } - - HStack { - VStack(alignment: .trailing) { - if studio != nil { - L10n.studio.text - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - Text(studio!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(.bottom, 40) - } - - if director != nil { - L10n.director.text - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - Text(director!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(.bottom, 40) - } - - if !actors.isEmpty { - L10n.cast.text - .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) - - MediaPlayButtonRowView(viewModel: viewModel, wrappedScrollView: wrappedScrollView) - .padding(.top, 15) - } - }.padding(.top, 50) - - if !viewModel.similarItems.isEmpty { - L10n.moreLikeThis.text - .font(.headline) - .fontWeight(.semibold) - ScrollView(.horizontal) { - LazyHStack { - Spacer().frame(width: 45) - ForEach(viewModel.similarItems, id: \.id) { similarItem in - Button { - itemRouter.route(to: \.item, similarItem) - } label: { - PortraitItemElement(item: similarItem) - } - .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: 0, trailing: 90)) - }.introspectScrollView { scrollView in - wrappedScrollView = scrollView - } - }.onAppear(perform: onAppear) - .focusScope(namespace) - } -} - -extension UIScrollView { - func scrollToTop() { - let desiredOffset = CGPoint(x: 0, y: 0) - setContentOffset(desiredOffset, animated: true) - } -} diff --git a/Swiftfin tvOS/Views/ItemView/CompactItemView/SeasonItemView.swift b/Swiftfin tvOS/Views/ItemView/CompactItemView/SeasonItemView.swift deleted file mode 100644 index 4c05691a..00000000 --- a/Swiftfin tvOS/Views/ItemView/CompactItemView/SeasonItemView.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct SeasonItemView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: SeasonItemViewModel - @State - var wrappedScrollView: UIScrollView? - - @Environment(\.resetFocus) - var resetFocus - @Namespace - private var namespace - - var body: some View { - ZStack { - ImageView(viewModel.item.getSeriesBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getSeriesBackdropImageBlurHash()) - .opacity(0.4) - .ignoresSafeArea() - 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 { - L10n.episodes.text - .font(.headline) - .fontWeight(.semibold) - ScrollView(.horizontal) { - LazyHStack { - Spacer().frame(width: 45) - - ForEach(viewModel.episodes, id: \.id) { episode in - - Button { - itemRouter.route(to: \.item, episode) - } label: { - 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/Swiftfin tvOS/Views/ItemView/CompactItemView/SeriesItemView.swift b/Swiftfin tvOS/Views/ItemView/CompactItemView/SeriesItemView.swift deleted file mode 100644 index 86886313..00000000 --- a/Swiftfin tvOS/Views/ItemView/CompactItemView/SeriesItemView.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct SeriesItemView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: SeriesItemViewModel - - @State - var actors: [BaseItemPerson] = [] - @State - var studio: String? - @State - var director: String? - - @State - var wrappedScrollView: UIScrollView? - - @Environment(\.resetFocus) - var resetFocus - @Namespace - private var namespace - - 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(viewModel.item.getBackdropImage(maxWidth: 1920), blurHash: viewModel.item.getBackdropImageBlurHash()) - .opacity(0.4) - .ignoresSafeArea() - ScrollView { - LazyVStack(alignment: .leading) { - Text(viewModel.item.name ?? "") - .font(.title) - .fontWeight(.bold) - .foregroundColor(.primary) - HStack { - 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) - } - } - } - - HStack { - VStack(alignment: .trailing) { - if studio != nil { - L10n.studio.text - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - Text(studio!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(.bottom, 40) - } - - if director != nil { - L10n.director.text - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - Text(director!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(.bottom, 40) - } - - if !actors.isEmpty { - L10n.cast.text - .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) - - MediaPlayButtonRowView(viewModel: viewModel, wrappedScrollView: wrappedScrollView) - .padding(.top, 15) - Spacer() - } - }.padding(.top, 50) - if !viewModel.seasons.isEmpty { - L10n.seasons.text - .font(.headline) - .fontWeight(.semibold) - ScrollView(.horizontal) { - LazyHStack { - Spacer().frame(width: 45) - - ForEach(viewModel.seasons, id: \.id) { season in - Button { - itemRouter.route(to: \.item, season) - } label: { - PortraitItemElement(item: season) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - } - Spacer().frame(width: 45) - } - }.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90)) - .frame(height: 360) - } - - if !viewModel.similarItems.isEmpty { - L10n.moreLikeThis.text - .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) - } - }.padding(EdgeInsets(top: 90, leading: 90, bottom: 45, trailing: 90)) - }.focusScope(namespace) - .introspectScrollView { scrollView in - wrappedScrollView = scrollView - } - }.onAppear(perform: onAppear) - } -} diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift new file mode 100644 index 00000000..d527f26c --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift @@ -0,0 +1,108 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct AboutView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: ItemViewModel + + @State + private var presentOverviewAlert = false + @State + private var presentSubtitlesAlert = false + @State + private var presentAudioAlert = false + + var body: some View { + VStack(alignment: .leading) { + + L10n.about.text + .font(.title3) + .fontWeight(.semibold) + .padding(.leading, 50) + + ScrollView(.horizontal) { + HStack { + ImageView( + viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel.item + .imageSource(.primary, maxWidth: 300), + failureView: { + InitialFailureView(viewModel.item.title.initials) + } + ) + .portraitPoster(width: 270) + + AboutViewCard( + isShowingAlert: $presentOverviewAlert, + title: viewModel.item.displayName, + text: viewModel.item.overview ?? L10n.noOverviewAvailable + ) + + if let subtitleStreams = viewModel.playButtonItem?.subtitleStreams, !subtitleStreams.isEmpty { + AboutViewCard( + isShowingAlert: $presentSubtitlesAlert, + title: L10n.subtitles, + text: subtitleStreams.compactMap(\.displayTitle).joined(separator: ", ") + ) + } + + if let audioStreams = viewModel.playButtonItem?.audioStreams, !audioStreams.isEmpty { + AboutViewCard( + isShowingAlert: $presentAudioAlert, + title: L10n.audio, + text: audioStreams.compactMap(\.displayTitle).joined(separator: ", ") + ) + } + } + .padding(.horizontal, 50) + .padding(.top) + .padding(.bottom, 100) + } + } + .alert(viewModel.item.displayName, isPresented: $presentOverviewAlert) { + Button { + presentOverviewAlert = false + } label: { + L10n.close.text + } + } message: { + if let overview = viewModel.item.overview { + overview.text + } else { + L10n.noOverviewAvailable.text + } + } + .alert(L10n.subtitles, isPresented: $presentSubtitlesAlert) { + Button { + presentSubtitlesAlert = false + } label: { + L10n.close.text + } + } message: { + viewModel.item.subtitleStreams.compactMap(\.displayTitle).joined(separator: ", ") + .text + } + .alert(L10n.audio, isPresented: $presentAudioAlert) { + Button { + presentAudioAlert = false + } label: { + L10n.close.text + } + } message: { + viewModel.item.audioStreams.compactMap(\.displayTitle).joined(separator: ", ") + .text + } + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutViewCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutViewCard.swift new file mode 100644 index 00000000..82e58290 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutViewCard.swift @@ -0,0 +1,43 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView.AboutView { + + struct AboutViewCard: View { + + @Binding + var isShowingAlert: Bool + + let title: String + let text: String + + var body: some View { + Button { + isShowingAlert = true + } label: { + VStack(alignment: .leading) { + title.text + .font(.title3) + .fontWeight(.semibold) + .lineLimit(2) + + Spacer() + + TruncatedTextView(text: text, seeMoreAction: {}) + .font(.subheadline) + .lineLimit(4) + } + .padding2() + .frame(width: 700, height: 405) + } + .buttonStyle(CardButtonStyle()) + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtonHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtonHStack.swift new file mode 100644 index 00000000..553ffd27 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtonHStack.swift @@ -0,0 +1,56 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct ActionButtonHStack: View { + + @ObservedObject + var viewModel: ItemViewModel + + var body: some View { + HStack { + Button { + viewModel.toggleWatchState() + } label: { + Group { + if viewModel.isWatched { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.jellyfinPurple) + } else { + Image(systemName: "checkmark.circle") + } + } + .font(.title3) + .frame(height: 100) + .frame(maxWidth: .infinity) + } + .buttonStyle(PlainButtonStyle()) + + Button { + viewModel.toggleFavoriteState() + } label: { + Group { + if viewModel.isFavorited { + Image(systemName: "heart.fill") + .foregroundColor(.red) + } else { + Image(systemName: "heart") + } + } + .font(.title3) + .frame(height: 100) + .frame(maxWidth: .infinity) + } + .buttonStyle(PlainButtonStyle()) + } + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift new file mode 100644 index 00000000..52613c2d --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift @@ -0,0 +1,54 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct AttributesHStack: View { + + @ObservedObject + var viewModel: ItemViewModel + + var body: some View { + HStack(spacing: 0) { + if let officialRating = viewModel.item.officialRating { + AttributeOutlineView(text: officialRating) + .padding(.trailing) + } + + if let selectedPlayerViewModel = viewModel.selectedVideoPlayerViewModel { + if selectedPlayerViewModel.item.isHD ?? false { + AttributeFillView(text: "HD") + .padding(.trailing) + } + + if (selectedPlayerViewModel.videoStream.width ?? 0) > 3800 { + AttributeFillView(text: "4K") + .padding(.trailing) + } + + if selectedPlayerViewModel.audioStreams.contains(where: { $0.channelLayout == "5.1" }) { + AttributeFillView(text: "5.1") + .padding(.trailing) + } + + if selectedPlayerViewModel.audioStreams.contains(where: { $0.channelLayout == "7.1" }) { + AttributeFillView(text: "7.1") + .padding(.trailing) + } + + if !selectedPlayerViewModel.subtitleStreams.isEmpty { + AttributeOutlineView(text: "CC") + } + } + } + .foregroundColor(Color(UIColor.darkGray)) + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeItemView/EpisodeItemContentView.swift new file mode 100644 index 00000000..dc4d11fd --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeItemView/EpisodeItemContentView.swift @@ -0,0 +1,187 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension EpisodeItemView { + + struct ContentView: View { + + @ObservedObject + var viewModel: EpisodeItemViewModel + @State + var scrollViewProxy: ScrollViewProxy + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + private var focusGuide = FocusGuide() + @State + private var showName: Bool = false + + var body: some View { + VStack { + Self.EpisodeCinematicHeaderView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "mediaButtons", bottom: "recommended") + .frame(height: UIScreen.main.bounds.height - 150) + .padding(.bottom, 50) + + VStack(spacing: 0) { + + Color.clear + .frame(height: 0.5) + .id("topContentDivider") + + if showName { + Text(viewModel.item.displayName) + .font(.title2) + .fontWeight(.semibold) + .lineLimit(1) + .foregroundColor(.white) + } + + PortraitPosterHStack( + title: L10n.recommended, + items: viewModel.similarItems + ) { item in + itemRouter.route(to: \.item, item) + } + .focusGuide(focusGuide, tag: "recommended", top: "mediaButtons", bottom: "about") + + ItemView.AboutView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "about", top: "recommended") + + Spacer() + } + .frame(minHeight: UIScreen.main.bounds.height) + } + .background { + BlurView() + .mask { + VStack(spacing: 0) { + LinearGradient(gradient: Gradient(stops: [ + .init(color: .white, location: 0), + .init(color: .white.opacity(0.5), location: 0.6), + .init(color: .white.opacity(0), location: 1), + ]), startPoint: .bottom, endPoint: .top) + .frame(height: UIScreen.main.bounds.height - 150) + + Color.white + } + } + } + .onChange(of: focusGuide.focusedTag) { newTag in + if newTag == "recommended" && !showName { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation(.easeIn(duration: 0.35)) { + scrollViewProxy.scrollTo("topContentDivider") + } + } + withAnimation { + self.showName = true + } + } else if newTag == "mediaButtons" { + withAnimation { + self.showName = false + } + } + } + } + } +} + +extension EpisodeItemView.ContentView { + + struct EpisodeCinematicHeaderView: View { + + enum CinematicHeaderFocusLayer: Hashable { + case top + case playButton + } + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @FocusState + private var focusedLayer: CinematicHeaderFocusLayer? + @EnvironmentObject + private var focusGuide: FocusGuide + @ObservedObject + var viewModel: EpisodeItemViewModel + + var body: some View { + VStack(alignment: .leading) { + + Color.clear + .focusable() + .focused($focusedLayer, equals: .top) + + HStack(alignment: .bottom) { + + VStack(alignment: .leading, spacing: 20) { + + if let seriesName = viewModel.item.seriesName { + Text(seriesName) + .font(.headline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + } + + Text(viewModel.item.displayName) + .font(.title2) + .fontWeight(.semibold) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(.white) + + if let overview = viewModel.item.overview { + Text(overview) + .font(.subheadline) + .lineLimit(4) + } else { + L10n.noOverviewAvailable.text + } + + HStack { + DotHStack { + if let premiereYear = viewModel.item.premiereDateYear { + premiereYear.text + } + + if let playButtonitem = viewModel.playButtonItem, let runtime = playButtonitem.getItemRuntime() { + runtime.text + } + } + .font(.caption) + .foregroundColor(Color(UIColor.lightGray)) + + ItemView.AttributesHStack(viewModel: viewModel) + } + } + + Spacer() + + VStack { + ItemView.PlayButton(viewModel: viewModel) + .focused($focusedLayer, equals: .playButton) + + ItemView.ActionButtonHStack(viewModel: viewModel) + .frame(width: 400) + } + .frame(width: 450) + .padding(.leading, 150) + } + } + .padding(.horizontal, 50) + .onChange(of: focusedLayer) { layer in + if layer == .top { + focusedLayer = .playButton + } + } + } + } +} diff --git a/Shared/Extensions/ImageExtensions.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeItemView/EpisodeItemView.swift similarity index 50% rename from Shared/Extensions/ImageExtensions.swift rename to Swiftfin tvOS/Views/ItemView/Components/EpisodeItemView/EpisodeItemView.swift index 6870fc36..9fb69b5b 100644 --- a/Shared/Extensions/ImageExtensions.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeItemView/EpisodeItemView.swift @@ -6,17 +6,16 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // -import Foundation import SwiftUI -extension Image { - func centerCropped() -> some View { - GeometryReader { geo in - self - .resizable() - .scaledToFill() - .frame(width: geo.size.width, height: geo.size.height) - .clipped() +struct EpisodeItemView: View { + + @ObservedObject + var viewModel: EpisodeItemViewModel + + var body: some View { + ItemView.CinematicScrollView(viewModel: viewModel) { scrollViewProxy in + ContentView(viewModel: viewModel, scrollViewProxy: scrollViewProxy) } } } diff --git a/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift b/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift new file mode 100644 index 00000000..a7cf99f5 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift @@ -0,0 +1,71 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct PlayButton: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: ItemViewModel + @FocusState + var isFocused: Bool + + var body: some View { + Button { + if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { + itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) + } else { + LogManager.log.error("Attempted to play item but no playback information available") + } + } label: { + HStack(spacing: 15) { + Image(systemName: "play.fill") + .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black) + .font(.title3) + Text(viewModel.playButtonText()) + .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black) + .fontWeight(.semibold) + } + .frame(width: 400, height: 100) + .background { + if isFocused { + viewModel.playButtonItem == nil ? Color.secondarySystemFill : Color.white + } else { + Color.white + .opacity(0.5) + } + } + .cornerRadius(10) + } + .focused($isFocused) + .buttonStyle(CardButtonStyle()) + .contextMenu { + if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 { + Button { + if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { + selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true) + itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) + } else { + LogManager.log.error("Attempted to play item but no playback information available") + } + } label: { + Label(L10n.playFromBeginning, systemImage: "gobackward") + } + + Button(role: .cancel) {} label: { + L10n.cancel.text + } + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/ItemView.swift b/Swiftfin tvOS/Views/ItemView/ItemView.swift index 5f51f3e3..46d3c04c 100644 --- a/Swiftfin tvOS/Views/ItemView/ItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/ItemView.swift @@ -11,8 +11,8 @@ import Introspect import JellyfinAPI import SwiftUI -// Useless view necessary in tvOS because of iOS's implementation -struct ItemNavigationView: View { +struct ItemView: View { + private let item: BaseItemDto init(item: BaseItemDto) { @@ -20,53 +20,17 @@ struct ItemNavigationView: View { } var body: some View { - ItemView(item: item) - } -} - -struct ItemView: View { - - @Default(.tvOSCinematicViews) - var tvOSCinematicViews - - private var item: BaseItemDto - - init(item: BaseItemDto) { - self.item = item - } - - var body: some View { - Group { - switch item.itemType { - case .movie: - if tvOSCinematicViews { - CinematicMovieItemView(viewModel: MovieItemViewModel(item: item)) - } else { - MovieItemView(viewModel: MovieItemViewModel(item: item)) - } - case .episode: - if tvOSCinematicViews { - CinematicEpisodeItemView(viewModel: EpisodeItemViewModel(item: item)) - } else { - EpisodeItemView(viewModel: EpisodeItemViewModel(item: item)) - } - case .season: - if tvOSCinematicViews { - CinematicSeasonItemView(viewModel: SeasonItemViewModel(item: item)) - } else { - SeasonItemView(viewModel: .init(item: item)) - } - case .series: - if tvOSCinematicViews { - CinematicSeriesItemView(viewModel: SeriesItemViewModel(item: item)) - } else { - SeriesItemView(viewModel: SeriesItemViewModel(item: item)) - } - case .boxset, .folder: - CinematicCollectionItemView(viewModel: CollectionItemViewModel(item: item)) - default: - Text(L10n.notImplementedYetWithType(item.type ?? "")) - } + switch item.type { + case .movie: + MovieItemView(viewModel: .init(item: item)) + case .episode: + EpisodeItemView(viewModel: .init(item: item)) + case .series: + SeriesItemView(viewModel: .init(item: item)) + case .boxSet: + CollectionItemView(viewModel: .init(item: item)) + default: + Text(L10n.notImplementedYetWithType(item.type ?? "--")) } } } diff --git a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift new file mode 100644 index 00000000..c9bd0d60 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift @@ -0,0 +1,111 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension MovieItemView { + + struct ContentView: View { + + @ObservedObject + var viewModel: MovieItemViewModel + @State + var scrollViewProxy: ScrollViewProxy + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + private var focusGuide = FocusGuide() + @State + private var showLogo: Bool = false + + var body: some View { + VStack(spacing: 0) { + + ItemView.CinematicHeaderView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "mediaButtons", bottom: "recommended") + .frame(height: UIScreen.main.bounds.height - 150) + .padding(.bottom, 50) + + VStack(spacing: 0) { + + Color.clear + .frame(height: 0.5) + .id("topContentDivider") + + if showLogo { + ImageView( + viewModel.item.imageSource(.logo, maxWidth: 500), + resizingMode: .aspectFit, + failureView: { + Text(viewModel.item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + .lineLimit(2) + .multilineTextAlignment(.leading) + .foregroundColor(.white) + } + ) + .frame(width: 500, height: 150) + .padding(.top, 5) + } + + PortraitPosterHStack( + title: L10n.recommended, + items: viewModel.similarItems + ) { item in + itemRouter.route(to: \.item, item) + } + .focusGuide(focusGuide, tag: "recommended", top: "mediaButtons", bottom: "about") + + ItemView.AboutView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "about", top: "recommended") + + Spacer() + } + .frame(minHeight: UIScreen.main.bounds.height) + } + .background { + BlurView(style: .dark) + .mask { + VStack(spacing: 0) { + LinearGradient( + stops: [ + .init(color: .clear, location: 0.5), + .init(color: .white.opacity(0.8), location: 0.7), + .init(color: .white.opacity(0.8), location: 0.95), + .init(color: .white, location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: UIScreen.main.bounds.height - 150) + + Color.white + } + } + } + .onChange(of: focusGuide.focusedTag) { newTag in + if newTag == "recommended" && !showLogo { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation(.easeIn(duration: 0.35)) { + scrollViewProxy.scrollTo("topContentDivider") + } + } + withAnimation { + self.showLogo = true + } + } else if newTag == "mediaButtons" { + withAnimation { + self.showLogo = false + } + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift new file mode 100644 index 00000000..5394bb36 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift @@ -0,0 +1,21 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct MovieItemView: View { + + @ObservedObject + var viewModel: MovieItemViewModel + + var body: some View { + ItemView.CinematicScrollView(viewModel: viewModel) { scrollViewProxy in + ContentView(viewModel: viewModel, scrollViewProxy: scrollViewProxy) + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift b/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift new file mode 100644 index 00000000..5d5473dc --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift @@ -0,0 +1,129 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +extension ItemView { + + struct CinematicScrollView: View { + + @ObservedObject + var viewModel: ItemViewModel + + let content: (ScrollViewProxy) -> Content + + var body: some View { + ZStack { + if viewModel.item.type == .episode { + ImageView(viewModel.item.imageSource(.primary, maxWidth: 1920)) + } else { + ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 1920)) + } + + ScrollView(.vertical, showsIndicators: false) { + ScrollViewReader { scrollViewProxy in + content(scrollViewProxy) + } + } + } + .ignoresSafeArea() + } + } +} + +extension ItemView { + + struct CinematicHeaderView: View { + + enum CinematicHeaderFocusLayer: Hashable { + case top + case playButton + } + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: ItemViewModel + @EnvironmentObject + var focusGuide: FocusGuide + @FocusState + private var focusedLayer: CinematicHeaderFocusLayer? + + var body: some View { + VStack(alignment: .leading) { + + Color.clear + .focusable() + .focused($focusedLayer, equals: .top) + + HStack(alignment: .bottom) { + + VStack(alignment: .leading, spacing: 20) { + + ImageView( + viewModel.item.imageSource(.logo, maxWidth: 500), + resizingMode: .aspectFit, + failureView: { + Text(viewModel.item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + .lineLimit(2) + .multilineTextAlignment(.leading) + .foregroundColor(.white) + } + ) + .frame(maxWidth: 500, maxHeight: 200) + + Text(viewModel.item.overview ?? L10n.noOverviewAvailable) + .font(.subheadline) + .lineLimit(3) + + HStack { + DotHStack { + if let firstGenre = viewModel.item.genres?.first { + firstGenre.text + } + + if let premiereYear = viewModel.item.premiereDateYear { + premiereYear.text + } + + if let playButtonitem = viewModel.playButtonItem, let runtime = playButtonitem.getItemRuntime() { + runtime.text + } + } + .font(.caption) + .foregroundColor(Color(UIColor.lightGray)) + + ItemView.AttributesHStack(viewModel: viewModel) + } + } + + Spacer() + + VStack { + ItemView.PlayButton(viewModel: viewModel) + .focused($focusedLayer, equals: .playButton) + + ItemView.ActionButtonHStack(viewModel: viewModel) + .frame(width: 400) + } + .frame(width: 450) + .padding(.leading, 150) + } + } + .padding(.horizontal, 50) + .onChange(of: focusedLayer) { layer in + if layer == .top { + focusedLayer = .playButton + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/EpisodeCard.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/EpisodeCard.swift new file mode 100644 index 00000000..64c1b7ac --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/EpisodeCard.swift @@ -0,0 +1,84 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +struct EpisodeCard: View { + + @EnvironmentObject + private var router: ItemCoordinator.Router + @State + private var cancellables = Set() + + let episode: BaseItemDto + + var body: some View { + VStack(alignment: .center, spacing: 20) { + Button { + // TODO: Figure out ad-hoc video player view model creation + episode.createVideoPlayerViewModel() + .sink(receiveCompletion: { _ in }) { viewModels in + guard !viewModels.isEmpty else { return } + self.router.route(to: \.videoPlayer, viewModels[0]) + } + .store(in: &cancellables) + } label: { + ImageView( + episode.imageSource(.primary, maxWidth: 600) + ) { + InitialFailureView(episode.title.initials) + } + .frame(width: 550, height: 308) + } + .buttonStyle(CardButtonStyle()) + + Button { + router.route(to: \.item, episode) + } label: { + VStack(alignment: .leading) { + + VStack(alignment: .leading, spacing: 0) { + Color.clear + .frame(height: 0.01) + .frame(maxWidth: .infinity) + + Text(episode.episodeLocator ?? L10n.unknown) + .font(.caption) + .foregroundColor(.secondary) + } + + Text(episode.displayName) + .font(.footnote) + .padding(.bottom, 1) + + if episode.unaired { + Text(episode.airDateLabel ?? L10n.noOverviewAvailable) + .font(.caption) + .lineLimit(1) + } else { + Text(episode.overview ?? L10n.noOverviewAvailable) + .font(.caption) + .lineLimit(3) + } + + Spacer(minLength: 0) + + L10n.seeMore.text + .font(.caption) + .fontWeight(.medium) + .foregroundColor(Color(UIColor.systemCyan)) + } + .frame(width: 510, height: 220) + .padding() + } + .buttonStyle(CardButtonStyle()) + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodesView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodesView.swift new file mode 100644 index 00000000..39470ea1 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodesView.swift @@ -0,0 +1,153 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Introspect +import JellyfinAPI +import SwiftUI + +struct SeriesEpisodesView: View { + + @ObservedObject + var viewModel: SeriesItemViewModel + @FocusState + private var isFocused: Bool + @EnvironmentObject + private var parentFocusGuide: FocusGuide + + var body: some View { + VStack(spacing: 0) { + SeasonsHStack(viewModel: viewModel) + .environmentObject(parentFocusGuide) + + EpisodesHStack(viewModel: viewModel) + .environmentObject(parentFocusGuide) + } + } +} + +extension SeriesEpisodesView { + + // MARK: SeasonsHStack + + struct SeasonsHStack: View { + + @ObservedObject + var viewModel: SeriesItemViewModel + @EnvironmentObject + private var focusGuide: FocusGuide + @FocusState + private var focusedSeason: BaseItemDto? + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(viewModel.sortedSeasons, id: \.self) { season in + Button {} label: { + Text(season.displayName) + .fontWeight(.semibold) + .fixedSize() + .padding(.vertical, 10) + .padding(.horizontal, 20) + .if(viewModel.selectedSeason == season) { text in + text + .background(Color.white) + .foregroundColor(.black) + } + } + .buttonStyle(PlainButtonStyle()) + .id(season) + .focused($focusedSeason, equals: season) + } + } + .focusGuide( + focusGuide, + tag: "seasons", + onContentFocus: { focusedSeason = viewModel.selectedSeason }, + top: "mediaButtons", + bottom: "episodes" + ) + .frame(height: 70) + .padding(.horizontal, 50) + .padding(.top) + .padding(.bottom, 45) + } + .onChange(of: focusedSeason) { season in + guard let season = season else { return } + viewModel.select(season: season) + } + } + } +} + +extension SeriesEpisodesView { + + // MARK: EpisodesHStack + + struct EpisodesHStack: View { + + @ObservedObject + var viewModel: SeriesItemViewModel + + @EnvironmentObject + private var focusGuide: FocusGuide + @FocusState + private var focusedEpisodeID: String? + @State + private var lastFocusedEpisodeID: String? + @State + private var currentEpisodes: [BaseItemDto] = [] + @State + private var wrappedScrollView: UIScrollView? + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 40) { + if !currentEpisodes.isEmpty { + ForEach(currentEpisodes, id: \.self) { episode in + EpisodeCard(episode: episode) + .focused($focusedEpisodeID, equals: episode.id) + } + } else { + ForEach(1 ..< 10) { i in + EpisodeCard(episode: .placeHolder) + .redacted(reason: .placeholder) + .focused($focusedEpisodeID, equals: "\(i)") + } + } + } + .padding(.horizontal, 50) + .padding(.bottom, 50) + .padding(.top) + } + .focusGuide( + focusGuide, + tag: "episodes", + onContentFocus: { focusedEpisodeID = lastFocusedEpisodeID }, + top: "seasons", + bottom: "recommended" + ) + .transition(.opacity) + .introspectScrollView { scrollView in + wrappedScrollView = scrollView + } + .onChange(of: viewModel.selectedSeason) { _ in + currentEpisodes = viewModel.currentEpisodes ?? [] + lastFocusedEpisodeID = currentEpisodes.first?.id + wrappedScrollView?.scrollToTop(animated: false) + } + .onChange(of: focusedEpisodeID) { episodeIndex in + guard let episodeIndex = episodeIndex else { return } + lastFocusedEpisodeID = episodeIndex + } + .onChange(of: viewModel.seasonsEpisodes) { _ in + currentEpisodes = viewModel.currentEpisodes ?? [] + lastFocusedEpisodeID = currentEpisodes.first?.id + } + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift new file mode 100644 index 00000000..a49c1ad6 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift @@ -0,0 +1,121 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation +import SwiftUI + +extension SeriesItemView { + + struct ContentView: View { + + @ObservedObject + var viewModel: SeriesItemViewModel + @State + var scrollViewProxy: ScrollViewProxy + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + private var focusGuide = FocusGuide() + @State + private var showLogo: Bool = false + + var body: some View { + VStack(spacing: 0) { + + ItemView.CinematicHeaderView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "mediaButtons", bottom: "seasons") + .frame(height: UIScreen.main.bounds.height - 150) + .padding(.bottom, 50) + + VStack(spacing: 0) { + + Color.clear + .frame(height: 0.5) + .id("topContentDivider") + + if showLogo { + ImageView( + viewModel.item.imageSource(.logo, maxWidth: 500), + resizingMode: .aspectFit, + failureView: { + Text(viewModel.item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + .lineLimit(2) + .multilineTextAlignment(.leading) + .foregroundColor(.white) + } + ) + .frame(width: 500, height: 150) + .padding(.top, 5) + } + + SeriesEpisodesView(viewModel: viewModel) + .environmentObject(focusGuide) + + Color.clear + .frame(height: 0.5) + .id("seasonsRecommendedContentDivider") + + PortraitPosterHStack( + title: L10n.recommended, + items: viewModel.similarItems + ) { item in + itemRouter.route(to: \.item, item) + } + .focusGuide(focusGuide, tag: "recommended", top: "seasons", bottom: "about") + + ItemView.AboutView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "about", top: "recommended") + + Spacer() + } + .frame(minHeight: UIScreen.main.bounds.height) + } + .background { + BlurView(style: .dark) + .mask { + VStack(spacing: 0) { + LinearGradient(gradient: Gradient(stops: [ + .init(color: .white, location: 0), + .init(color: .white.opacity(0.7), location: 0.4), + .init(color: .white.opacity(0), location: 1), + ]), startPoint: .bottom, endPoint: .top) + .frame(height: UIScreen.main.bounds.height - 150) + + Color.white + } + } + } + .onChange(of: focusGuide.focusedTag) { newTag in + if newTag == "seasons" && !showLogo { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation(.easeIn(duration: 0.35)) { + scrollViewProxy.scrollTo("topContentDivider") + } + } + withAnimation { + self.showLogo = true + } + } else if newTag == "mediaButtons" { + withAnimation { + self.showLogo = false + } + } else if newTag == "recommended" && focusGuide.lastFocusedTag == "episodes" { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation(.easeIn(duration: 0.35)) { + scrollViewProxy.scrollTo("seasonsRecommendedContentDivider") + } + } + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift new file mode 100644 index 00000000..3a816e25 --- /dev/null +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift @@ -0,0 +1,21 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct SeriesItemView: View { + + @ObservedObject + var viewModel: SeriesItemViewModel + + var body: some View { + ItemView.CinematicScrollView(viewModel: viewModel) { scrollViewProxy in + ContentView(viewModel: viewModel, scrollViewProxy: scrollViewProxy) + } + } +} diff --git a/Swiftfin tvOS/Views/LatestInLibraryView.swift b/Swiftfin tvOS/Views/LatestInLibraryView.swift new file mode 100644 index 00000000..c90f8cd6 --- /dev/null +++ b/Swiftfin tvOS/Views/LatestInLibraryView.swift @@ -0,0 +1,56 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct LatestInLibraryView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @StateObject + var viewModel: LatestMediaViewModel + + var body: some View { + PortraitPosterHStack( + title: L10n.latestWithString(viewModel.library.displayName), + items: viewModel.items + ) { + Button { + router.route(to: \.library, ( + viewModel: .init( + parentID: viewModel.library.id!, + filters: LibraryFilters( + filters: [], + sortOrder: [.descending], + sortBy: [.dateAdded] + ) + ), + title: viewModel.library.displayName + )) + } label: { + ZStack { + Color(UIColor.darkGray) + .opacity(0.5) + + VStack(spacing: 20) { + Image(systemName: "chevron.right") + .font(.title) + + L10n.seeAll.text + .font(.title3) + } + } + } + .frame(width: 257, height: 380) + .buttonStyle(PlainButtonStyle()) + } selectedAction: { item in + router.route(to: \.item, item) + } + } +} diff --git a/Swiftfin tvOS/Views/LatestMediaView.swift b/Swiftfin tvOS/Views/LatestMediaView.swift deleted file mode 100644 index 0f1d9b53..00000000 --- a/Swiftfin tvOS/Views/LatestMediaView.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import JellyfinAPI -import SwiftUI - -struct LatestMediaView: View { - - @EnvironmentObject - var homeRouter: HomeCoordinator.Router - @StateObject - var viewModel: LatestMediaViewModel - @Default(.showPosterLabels) - var showPosterLabels - - var body: some View { - VStack(alignment: .leading) { - - L10n.latestWithString(viewModel.library.name ?? "").text - .font(.title3) - .padding(.horizontal, 50) - - ScrollView(.horizontal) { - HStack(alignment: .top) { - ForEach(viewModel.items, id: \.self) { item in - - VStack(spacing: 15) { - Button { - homeRouter.route(to: \.modalItem, item) - } label: { - ImageView(item.portraitHeaderViewURL(maxWidth: 257)) - .frame(width: 257, height: 380) - } - .frame(height: 380) - .buttonStyle(PlainButtonStyle()) - - if showPosterLabels { - Text(item.title) - .lineLimit(2) - .frame(width: 257) - } - } - } - - Button { - homeRouter.route(to: \.library, ( - viewModel: .init( - parentID: viewModel.library.id!, - filters: LibraryFilters( - filters: [], - sortOrder: [.descending], - sortBy: [.dateAdded] - ) - ), - title: viewModel.library.name ?? "" - )) - } label: { - ZStack { - Color(UIColor.darkGray) - .opacity(0.5) - - VStack(spacing: 20) { - Image(systemName: "chevron.right") - .font(.title) - - L10n.seeAll.text - .font(.title3) - } - } - } - .frame(width: 257, height: 380) - .buttonStyle(PlainButtonStyle()) - } - .padding(.horizontal, 50) - .padding(.vertical) - } - .edgesIgnoringSafeArea(.horizontal) - } - .focusSection() - } -} diff --git a/Swiftfin tvOS/Views/LibraryFilterView.swift b/Swiftfin tvOS/Views/LibraryFilterView.swift index be385783..76b28f1e 100644 --- a/Swiftfin tvOS/Views/LibraryFilterView.swift +++ b/Swiftfin tvOS/Views/LibraryFilterView.swift @@ -13,7 +13,7 @@ import SwiftUI struct LibraryFilterView: View { @EnvironmentObject - var filterRouter: FilterCoordinator.Router + private var filterRouter: FilterCoordinator.Router @Binding var filters: LibraryFilters var parentId: String = "" diff --git a/Swiftfin tvOS/Views/LibraryListView.swift b/Swiftfin tvOS/Views/LibraryListView.swift index 0dc81467..855787cd 100644 --- a/Swiftfin tvOS/Views/LibraryListView.swift +++ b/Swiftfin tvOS/Views/LibraryListView.swift @@ -6,7 +6,6 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // -import Defaults import Foundation import JellyfinAPI import Stinsen @@ -16,34 +15,18 @@ struct LibraryListView: View { @EnvironmentObject var mainCoordinator: MainCoordinator.Router @EnvironmentObject - var libraryListRouter: LibraryListCoordinator.Router + private var libraryListRouter: LibraryListCoordinator.Router @StateObject var viewModel = LibraryListViewModel() - @Default(.Experimental.liveTVAlphaEnabled) - var liveTVAlphaEnabled - - var supportedCollectionTypes: [BaseItemDto.ItemType] { - if liveTVAlphaEnabled { - return [.movie, .season, .series, .liveTV, .boxset, .unknown] - } else { - return [.movie, .season, .series, .boxset, .unknown] - } - } - var body: some View { ScrollView { LazyVStack { if !viewModel.isLoading { - ForEach(viewModel.libraries.filter { [self] library in - let collectionType = library.collectionType ?? "other" - let itemType = BaseItemDto.ItemType(rawValue: collectionType) ?? .unknown - return self.supportedCollectionTypes.contains(itemType) - }, id: \.id) { library in + ForEach(viewModel.filteredLibraries, id: \.id) { library in Button { - let itemType = BaseItemDto.ItemType(rawValue: library.collectionType ?? "other") ?? .unknown - if itemType == .liveTV { + if library.collectionType == "livetv" { self.mainCoordinator.root(\.liveTV) } else { self.libraryListRouter.route( diff --git a/Swiftfin tvOS/Views/LibraryView.swift b/Swiftfin tvOS/Views/LibraryView.swift index 23db4df4..6d1ee4c0 100644 --- a/Swiftfin tvOS/Views/LibraryView.swift +++ b/Swiftfin tvOS/Views/LibraryView.swift @@ -12,7 +12,7 @@ import SwiftUICollection struct LibraryView: View { @EnvironmentObject - var libraryRouter: LibraryCoordinator.Router + private var libraryRouter: LibraryCoordinator.Router @StateObject var viewModel: LibraryViewModel var title: String @@ -67,7 +67,7 @@ struct LibraryView: View { GeometryReader { _ in if let item = cell.item { Button { - libraryRouter.route(to: \.modalItem, item) + libraryRouter.route(to: \.item, item) } label: { PortraitItemElement(item: item) } diff --git a/Swiftfin tvOS/Views/LiveTVChannelItemElement.swift b/Swiftfin tvOS/Views/LiveTVChannelItemElement.swift index 38c5d61c..5aa16125 100644 --- a/Swiftfin tvOS/Views/LiveTVChannelItemElement.swift +++ b/Swiftfin tvOS/Views/LiveTVChannelItemElement.swift @@ -74,7 +74,7 @@ struct LiveTVChannelItemElement: View { GeometryReader { gp in VStack { - ImageView(channel.getPrimaryImage(maxWidth: 192)) + ImageView(channel.imageSource(.primary, maxWidth: 192)) .aspectRatio(contentMode: .fit) .frame(width: 192, alignment: .center) } diff --git a/Swiftfin tvOS/Views/LiveTVChannelsView.swift b/Swiftfin tvOS/Views/LiveTVChannelsView.swift index f2aa5efa..036c1f12 100644 --- a/Swiftfin tvOS/Views/LiveTVChannelsView.swift +++ b/Swiftfin tvOS/Views/LiveTVChannelsView.swift @@ -15,7 +15,7 @@ typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String) struct LiveTVChannelsView: View { @EnvironmentObject - var router: LiveTVChannelsCoordinator.Router + private var router: LiveTVChannelsCoordinator.Router @StateObject var viewModel = LiveTVChannelsViewModel() diff --git a/Swiftfin tvOS/Views/LiveTVProgramsView.swift b/Swiftfin tvOS/Views/LiveTVProgramsView.swift index 6fadf822..f40d3e89 100644 --- a/Swiftfin tvOS/Views/LiveTVProgramsView.swift +++ b/Swiftfin tvOS/Views/LiveTVProgramsView.swift @@ -11,7 +11,7 @@ import SwiftUI struct LiveTVProgramsView: View { @EnvironmentObject - var programsRouter: LiveTVProgramsCoordinator.Router + private var programsRouter: LiveTVProgramsCoordinator.Router @StateObject var viewModel = LiveTVProgramsViewModel() diff --git a/Swiftfin tvOS/Views/MovieLibrariesView.swift b/Swiftfin tvOS/Views/MovieLibrariesView.swift index b378a9d8..f989c222 100644 --- a/Swiftfin tvOS/Views/MovieLibrariesView.swift +++ b/Swiftfin tvOS/Views/MovieLibrariesView.swift @@ -12,7 +12,7 @@ import SwiftUICollection struct MovieLibrariesView: View { @EnvironmentObject - var movieLibrariesRouter: MovieLibrariesCoordinator.Router + private var movieLibrariesRouter: MovieLibrariesCoordinator.Router @StateObject var viewModel: MovieLibrariesViewModel var title: String diff --git a/Swiftfin tvOS/Views/NextUpView/NextUpCard.swift b/Swiftfin tvOS/Views/NextUpView/NextUpCard.swift deleted file mode 100644 index 50a4324b..00000000 --- a/Swiftfin tvOS/Views/NextUpView/NextUpCard.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct NextUpCard: View { - - @EnvironmentObject - var homeRouter: HomeCoordinator.Router - let item: BaseItemDto - - var body: some View { - VStack(alignment: .leading) { - Button { - homeRouter.route(to: \.modalItem, item) - } label: { - if item.itemType == .episode { - ImageView(item.getSeriesBackdropImage(maxWidth: 500)) - .frame(width: 500, height: 281.25) - } else { - ImageView(item.getBackdropImage(maxWidth: 500)) - .frame(width: 500, height: 281.25) - } - } - .buttonStyle(CardButtonStyle()) - .padding(.top) - - VStack(alignment: .leading) { - Text("\(item.seriesName ?? item.name ?? "")") - .font(.callout) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - - if item.itemType == .episode { - Text(item.getEpisodeLocator() ?? "") - .font(.callout) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - } - } - } -} diff --git a/Swiftfin tvOS/Views/NextUpView/NextUpView.swift b/Swiftfin tvOS/Views/NextUpView/NextUpView.swift deleted file mode 100644 index a814f713..00000000 --- a/Swiftfin tvOS/Views/NextUpView/NextUpView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Combine -import JellyfinAPI -import Stinsen -import SwiftUI - -struct NextUpView: View { - var items: [BaseItemDto] - - var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve() - - var body: some View { - VStack(alignment: .leading) { - - L10n.nextUp.text - .font(.title3) - .fontWeight(.semibold) - .padding(.leading, 50) - - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack { - ForEach(items, id: \.id) { item in - NextUpCard(item: item) - } - } - .padding(.horizontal, 50) - } - } - } -} diff --git a/Swiftfin tvOS/Views/ServerListView.swift b/Swiftfin tvOS/Views/ServerListView.swift index 81f3dbbf..71b23731 100644 --- a/Swiftfin tvOS/Views/ServerListView.swift +++ b/Swiftfin tvOS/Views/ServerListView.swift @@ -12,7 +12,7 @@ import SwiftUI struct ServerListView: View { @EnvironmentObject - var serverListRouter: ServerListCoordinator.Router + private var serverListRouter: ServerListCoordinator.Router @ObservedObject var viewModel: ServerListViewModel diff --git a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift index 5972c47a..ccbce55f 100644 --- a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift @@ -14,7 +14,7 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject - var settingsRouter: SettingsCoordinator.Router + private var settingsRouter: SettingsCoordinator.Router @ObservedObject var viewModel: SettingsViewModel @@ -28,8 +28,6 @@ struct SettingsView: View { var downActionShowsMenu @Default(.confirmClose) var confirmClose - @Default(.tvOSCinematicViews) - var tvOSCinematicViews @Default(.showPosterLabels) var showPosterLabels @Default(.resumeOffset) @@ -124,12 +122,6 @@ struct SettingsView: View { } } - Section { - Toggle(L10n.cinematicViews, isOn: $tvOSCinematicViews) - } header: { - L10n.appearance.text - } - Section(header: L10n.accessibility.text) { Button { settingsRouter.route(to: \.customizeViewsSettings) diff --git a/Swiftfin tvOS/Views/TVLibrariesView.swift b/Swiftfin tvOS/Views/TVLibrariesView.swift index 5fb17aae..5e5179c4 100644 --- a/Swiftfin tvOS/Views/TVLibrariesView.swift +++ b/Swiftfin tvOS/Views/TVLibrariesView.swift @@ -12,7 +12,7 @@ import SwiftUICollection struct TVLibrariesView: View { @EnvironmentObject - var tvLibrariesRouter: TVLibrariesCoordinator.Router + private var tvLibrariesRouter: TVLibrariesCoordinator.Router @StateObject var viewModel: TVLibrariesViewModel var title: String diff --git a/Swiftfin tvOS/Views/UserListView.swift b/Swiftfin tvOS/Views/UserListView.swift index 4e42bd7f..447baa55 100644 --- a/Swiftfin tvOS/Views/UserListView.swift +++ b/Swiftfin tvOS/Views/UserListView.swift @@ -11,7 +11,7 @@ import SwiftUI struct UserListView: View { @EnvironmentObject - var userListRouter: UserListCoordinator.Router + private var userListRouter: UserListCoordinator.Router @ObservedObject var viewModel: UserListViewModel diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSLiveTVOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSLiveTVOverlay.swift index 3839a716..84ef35db 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSLiveTVOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSLiveTVOverlay.swift @@ -149,6 +149,7 @@ struct tvOSLiveTVOverlay_Previews: PreviewProvider { hlsStreamURL: URL(string: "www.apple.com")!, streamType: .direct, response: PlaybackInfoResponse(), + videoStream: MediaStream(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], subtitleStreams: [MediaStream(displayTitle: "None", index: -1)], chapters: [], diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift index 1857cbe5..d4a2e433 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift @@ -149,6 +149,7 @@ struct tvOSVLCOverlay_Previews: PreviewProvider { hlsStreamURL: URL(string: "www.apple.com")!, streamType: .direct, response: PlaybackInfoResponse(), + videoStream: MediaStream(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], subtitleStreams: [MediaStream(displayTitle: "None", index: -1)], chapters: [], diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 323c071c..6ea201a1 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -12,106 +12,66 @@ 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; - 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53116A16268B919A003024C9 /* SeriesItemView.swift */; }; - 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53116A18268B947A003024C9 /* PlainLinkButton.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; - 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EE267ABF72005D8AB9 /* NextUpView.swift */; }; 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */; }; - 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; }; 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; - 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 628B95212670CABD0091AF3B /* WidgetKit.framework */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; 5321753E2671DE9C005491E6 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; }; - 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */; }; - 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */; }; - 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272538268C20100035FBF1 /* EpisodeItemView.swift */; }; 53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; }; 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; }; 534D4FF026A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF126A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; - 534D4FF226A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF326A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FE726A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF426A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FE726A7D7CC000A7A48 /* Localizable.strings */; }; - 534D4FF526A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FE726A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF626A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEB26A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF726A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEB26A7D7CC000A7A48 /* Localizable.strings */; }; - 534D4FF826A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEB26A7D7CC000A7A48 /* Localizable.strings */; }; 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */; }; 535870672669D21700D05A09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870662669D21700D05A09 /* Assets.xcassets */; }; 5358706A2669D21700D05A09 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870692669D21700D05A09 /* Preview Assets.xcassets */; }; 5358707E2669D64F00D05A09 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; 535870912669D7A800D05A09 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 535870902669D7A800D05A09 /* Introspect */; }; - 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; - 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; - 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; - 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; }; 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; - 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; }; 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; }; - 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D73267BA8170004248C /* BackgroundManager.swift */; }; - 536D3D76267BA9BB0004248C /* MainTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */; }; 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; }; - 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; }; - 536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 536D3D7C267BD5F90004248C /* ActivityIndicator */; }; - 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D7E267BDF100004248C /* LatestMediaView.swift */; }; 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D80267BDFC60004248C /* PortraitItemElement.swift */; }; - 536D3D84267BEA550004248C /* ParallaxView in Frameworks */ = {isa = PBXBuildFile; productRef = 536D3D83267BEA550004248C /* ParallaxView */; }; - 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D87267C17350004248C /* PublicUserButton.swift */; }; 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; }; 5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; 5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; }; - 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; }; - 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; }; 53913BEF26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; }; 53913BF026D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; }; - 53913BF126D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; }; 53913BF226D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCC26D323FE00EB3286 /* Localizable.strings */; }; 53913BF326D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCC26D323FE00EB3286 /* Localizable.strings */; }; - 53913BF426D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCC26D323FE00EB3286 /* Localizable.strings */; }; 53913BF526D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCF26D323FE00EB3286 /* Localizable.strings */; }; 53913BF626D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCF26D323FE00EB3286 /* Localizable.strings */; }; - 53913BF726D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BCF26D323FE00EB3286 /* Localizable.strings */; }; 53913BF826D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD226D323FE00EB3286 /* Localizable.strings */; }; 53913BF926D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD226D323FE00EB3286 /* Localizable.strings */; }; - 53913BFA26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD226D323FE00EB3286 /* Localizable.strings */; }; 53913BFB26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD526D323FE00EB3286 /* Localizable.strings */; }; 53913BFC26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD526D323FE00EB3286 /* Localizable.strings */; }; - 53913BFD26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD526D323FE00EB3286 /* Localizable.strings */; }; 53913BFE26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD826D323FE00EB3286 /* Localizable.strings */; }; 53913BFF26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD826D323FE00EB3286 /* Localizable.strings */; }; - 53913C0026D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BD826D323FE00EB3286 /* Localizable.strings */; }; 53913C0126D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BDB26D323FE00EB3286 /* Localizable.strings */; }; 53913C0226D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BDB26D323FE00EB3286 /* Localizable.strings */; }; - 53913C0326D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BDB26D323FE00EB3286 /* Localizable.strings */; }; 53913C0426D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BDE26D323FE00EB3286 /* Localizable.strings */; }; 53913C0526D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BDE26D323FE00EB3286 /* Localizable.strings */; }; - 53913C0626D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BDE26D323FE00EB3286 /* Localizable.strings */; }; 53913C0726D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE126D323FE00EB3286 /* Localizable.strings */; }; 53913C0826D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE126D323FE00EB3286 /* Localizable.strings */; }; - 53913C0926D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE126D323FE00EB3286 /* Localizable.strings */; }; 53913C0A26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE426D323FE00EB3286 /* Localizable.strings */; }; 53913C0B26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE426D323FE00EB3286 /* Localizable.strings */; }; - 53913C0C26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE426D323FE00EB3286 /* Localizable.strings */; }; 53913C0D26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE726D323FE00EB3286 /* Localizable.strings */; }; 53913C0E26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE726D323FE00EB3286 /* Localizable.strings */; }; - 53913C0F26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BE726D323FE00EB3286 /* Localizable.strings */; }; 53913C1026D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BEA26D323FE00EB3286 /* Localizable.strings */; }; 53913C1126D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BEA26D323FE00EB3286 /* Localizable.strings */; }; - 53913C1226D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BEA26D323FE00EB3286 /* Localizable.strings */; }; 53913C1326D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BED26D323FE00EB3286 /* Localizable.strings */; }; 53913C1426D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BED26D323FE00EB3286 /* Localizable.strings */; }; - 53913C1526D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BED26D323FE00EB3286 /* Localizable.strings */; }; 5398514526B64DA100101B49 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5398514426B64DA100101B49 /* SettingsView.swift */; }; - 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; - 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */; }; 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A83C32268A309300DF3D92 /* LibraryView.swift */; }; 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53ABFDDB267972BF00886593 /* TVServices.framework */; }; @@ -122,27 +82,21 @@ 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ABFDEA2679753200886593 /* ConnectToServerView.swift */; }; 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 53ABFDEC26799D7700886593 /* ActivityIndicator */; }; - 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CD2A3F268A49C2002ABD4E /* ItemView.swift */; }; - 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */; }; 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; }; 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; }; - 53F866442687A45F00DCD1D7 /* PortraitItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitItemButton.swift */; }; - 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; }; + 53F866442687A45F00DCD1D7 /* PortraitPosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitPosterButton.swift */; }; 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; }; 5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; }; - 5D1603FE278A40DC00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; }; 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */; }; 5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */; }; 5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */; }; 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; }; 621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; - 621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; - 6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */; }; 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */; }; 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */; }; @@ -151,20 +105,15 @@ 6220D0C626D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift */; }; 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 6220D0C826D63F3700B8E046 /* Stinsen */; }; 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; }; - 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 62400C4B287ED19600F6AD3D /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; 62400C4C287ED19600F6AD3D /* UDPBroadcast.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; - 62553429282190A00087FE20 /* PanDirectionGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */; }; - 625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; }; 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; }; 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */; }; 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 625CB5792678C4A400530A6E /* ActivityIndicator */; }; - 6264E88A27384A6F0081A12A /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; 6264E88C273850380081A12A /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6264E88B273850380081A12A /* Strings.swift */; }; 6264E88D273850380081A12A /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6264E88B273850380081A12A /* Strings.swift */; }; - 6264E88E273850380081A12A /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6264E88B273850380081A12A /* Strings.swift */; }; 62666DF727E5012C00EC0ECD /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; }; 62666DF827E5012C00EC0ECD /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */; }; @@ -197,24 +146,11 @@ 62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2D27E5021400EC0ECD /* Security.framework */; }; 62666E3027E5021800EC0ECD /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E2F27E5021800EC0ECD /* VideoToolbox.framework */; }; 62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62666E3127E5021E00EC0ECD /* UIKit.framework */; }; - 62666E3427E5027F00EC0ECD /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62666E3327E5027F00EC0ECD /* Defaults+Workaround.swift */; }; - 62666E3527E5027F00EC0ECD /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62666E3327E5027F00EC0ECD /* Defaults+Workaround.swift */; }; - 62666E3627E5027F00EC0ECD /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62666E3327E5027F00EC0ECD /* Defaults+Workaround.swift */; }; 62666E3927E502CE00EC0ECD /* SwizzleSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 62666E3827E502CE00EC0ECD /* SwizzleSwift */; }; 62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5362E4BE267D40E4000E2F71 /* MediaAccessibility.framework */; }; 62666E3F27E5040300EC0ECD /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5362E4C6267D40F4000E2F71 /* SystemConfiguration.framework */; }; 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; - 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; - 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D92671138200A7371D /* ImageExtensions.swift */; }; - 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D92671138200A7371D /* ImageExtensions.swift */; }; - 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D92671138200A7371D /* ImageExtensions.swift */; }; - 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 628B95232670CABD0091AF3B /* SwiftUI.framework */; }; - 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628B95262670CABD0091AF3B /* NextUpWidget.swift */; }; - 628B95292670CABE0091AF3B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 628B95282670CABE0091AF3B /* Assets.xcassets */; }; - 628B952D2670CABE0091AF3B /* Swiftfin Widget.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628B95362670CB800091AF3B /* JellyfinWidget.swift */; }; - 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 62C29E9B26D0FE4200C1D2E7 /* Stinsen */; }; 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* iOSMainCoordinator.swift */; }; 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA026D102A500C1D2E7 /* iOSMainTabCoordinator.swift */; }; @@ -224,7 +160,6 @@ 62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C83B07288C6A630004ED0C /* FontPicker.swift */; }; 62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; - 62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; @@ -245,7 +180,6 @@ 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; - 62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; 631759CF2879DB6A00A621AD /* PublicUserSignInCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */; }; @@ -280,7 +214,6 @@ C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; }; C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; }; C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; }; - C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */; }; C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; }; C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; }; C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */; }; @@ -301,41 +234,42 @@ E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; }; E1002B682793CFBA00E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B672793CFBA00E47059 /* Algorithms */; }; E1002B6B2793E36600E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B6A2793E36600E47059 /* Algorithms */; }; - E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */; }; E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */; }; E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */; }; E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */; }; E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */; }; E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */; }; - E1047E2127E584AF00CB0D4A /* BlurHashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */; }; E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* InitialFailureView.swift */; }; - E1047E2427E5880000CB0D4A /* InitialFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* InitialFailureView.swift */; }; E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; - E107BB972788104100354E07 /* CinematicCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB952788104100354E07 /* CinematicCollectionItemView.swift */; }; - E10C0941278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */; }; - E10C0942278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */; }; - E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */; }; - E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */; }; - E10D87DE278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; }; - E10D87DF278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; }; - E10D87E0278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; }; + E10D87DC2784EC5200BD264C /* SeriesEpisodesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DB2784EC5200BD264C /* SeriesEpisodesView.swift */; }; E10D87E227852FD000BD264C /* EpisodesRowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87E127852FD000BD264C /* EpisodesRowManager.swift */; }; E10D87E327852FD000BD264C /* EpisodesRowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87E127852FD000BD264C /* EpisodesRowManager.swift */; }; E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* JellyfinAPI */; }; - E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; }; E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; }; E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; }; E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; }; - E10EAA51277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; }; E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E1101177281B1E8A006A3584 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1101176281B1E8A006A3584 /* Puppy */; }; E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */; }; + E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; }; + E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; }; + E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */; }; + E11895AA289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */; }; + E11895AC289383EE0042947B /* NavBarOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */; }; + E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895AE2893840F0042947B /* NavBarOffsetView.swift */; }; + E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */; }; + E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */; }; E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; - E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; + E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */; }; + E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */; }; + E11CEB8D28999B4A003E74C7 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8C28999B4A003E74C7 /* FontExtensions.swift */; }; + E11CEB8E28999B4A003E74C7 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8C28999B4A003E74C7 /* FontExtensions.swift */; }; + E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8F28999D84003E74C7 /* EpisodeItemView.swift */; }; + E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB9328999D9E003E74C7 /* EpisodeItemContentView.swift */; }; E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; E11D83AF278FA998006E9776 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E11D83AE278FA998006E9776 /* NukeUI */; }; @@ -347,11 +281,12 @@ E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; E1347DB2279E3C6200BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB1279E3C6200BC6161 /* Puppy */; }; - E1347DB4279E3C9E00BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB3279E3C9E00BC6161 /* Puppy */; }; E1347DB6279E3CA500BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB5279E3CA500BC6161 /* Puppy */; }; E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1361DA6278FA7A300BEC523 /* NukeUI */; }; E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; + E1399474289B1EA900401ABC /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1399473289B1EA900401ABC /* Defaults+Workaround.swift */; }; + E1399475289B1EA900401ABC /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1399473289B1EA900401ABC /* Defaults+Workaround.swift */; }; E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */; }; E13AD7302798C60F00FDCEE8 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */; }; E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; }; @@ -360,15 +295,10 @@ E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3C52716499E009D4DAF /* CoreStore */; }; E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */; }; E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */; }; - E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; - E13DD3CB27164BA8009D4DAF /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */; }; E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3CC27164CA7009D4DAF /* CoreStore */; }; - E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3CE27164E1F009D4DAF /* CoreStore */; }; E13DD3D327168E65009D4DAF /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3D227168E65009D4DAF /* Defaults */; }; E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; E13DD3D6271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; - E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; - E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3DC27175CE3009D4DAF /* Defaults */; }; E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */; }; E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */; }; E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E427177D15009D4DAF /* ServerListView.swift */; }; @@ -379,37 +309,87 @@ E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EE27178F87009D4DAF /* SwiftfinNotificationCenter.swift */; }; E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */; }; E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F4271793BB009D4DAF /* UserSignInView.swift */; }; - E13DD3F72717E87D009D4DAF /* SwiftfinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EE27178F87009D4DAF /* SwiftfinNotificationCenter.swift */; }; E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; }; E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; }; E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.swift */; }; E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */; }; - E13F26AF278754E300DF4761 /* CinematicSeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */; }; - E13F26B12787589300DF4761 /* CinematicSeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */; }; - E14B4141279354770016CBE5 /* LocalizedLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14B4140279354770016CBE5 /* LocalizedLookup.swift */; }; - E14B4142279354770016CBE5 /* LocalizedLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14B4140279354770016CBE5 /* LocalizedLookup.swift */; }; - E14B4143279354770016CBE5 /* LocalizedLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14B4140279354770016CBE5 /* LocalizedLookup.swift */; }; - E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */; }; - E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */; }; + E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546776289AF46E00087E35 /* CollectionItemView.swift */; }; + E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546779289AF48200087E35 /* CollectionItemContentView.swift */; }; + E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.swift */; }; + E168BD11289A4162001A6922 /* HomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD09289A4162001A6922 /* HomeContentView.swift */; }; + E168BD12289A4162001A6922 /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0C289A4162001A6922 /* ContinueWatchingCard.swift */; }; + E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */; }; + E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */; }; + E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0F289A4162001A6922 /* HomeErrorView.swift */; }; E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; }; E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; - E176DE6D278E30D2001EFD8D /* EpisodeRowCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E176DE6C278E30D2001EFD8D /* EpisodeRowCard.swift */; }; + E176DE6D278E30D2001EFD8D /* EpisodeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E176DE6C278E30D2001EFD8D /* EpisodeCard.swift */; }; E176DE70278E369F001EFD8D /* MissingItemsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */; }; E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E178857C278037FD0094FBCF /* JellyfinAPI */; }; E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859A2780F1F40094FBCF /* tvOSSlider.swift */; }; E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; }; E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */; }; E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A3278105170094FBCF /* SFSymbolButton.swift */; }; - E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; }; - E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; }; - E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; }; - E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; }; + E184C160288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */; }; + E184C161288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */; }; + E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */; }; + E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */; }; + E18E01A9288746AF0022598C /* PortraitPosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */; }; + E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A4288746AF0022598C /* RefreshableScrollView.swift */; }; + E18E01AB288746AF0022598C /* PillHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A5288746AF0022598C /* PillHStack.swift */; }; + E18E01AD288746AF0022598C /* DotHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A7288746AF0022598C /* DotHStack.swift */; }; + E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01B6288747230022598C /* iPadOSEpisodeContentView.swift */; }; + E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01B7288747230022598C /* iPadOSEpisodeItemView.swift */; }; + E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */; }; + E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01BB288747230022598C /* iPadOSSeriesItemContentView.swift */; }; + E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01BC288747230022598C /* iPadOSSeriesItemView.swift */; }; + E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01BE288747230022598C /* iPadOSMovieItemView.swift */; }; + E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */; }; + E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C2288747230022598C /* EpisodeItemContentView.swift */; }; + E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C3288747230022598C /* EpisodeItemView.swift */; }; + E18E01E3288747230022598C /* CompactPortraitScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C5288747230022598C /* CompactPortraitScrollView.swift */; }; + E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C6288747230022598C /* CompactLogoScrollView.swift */; }; + E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C7288747230022598C /* CinematicScrollView.swift */; }; + E18E01E6288747230022598C /* CollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C9288747230022598C /* CollectionItemView.swift */; }; + E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CA288747230022598C /* CollectionItemContentView.swift */; }; + E18E01E8288747230022598C /* SeriesItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CC288747230022598C /* SeriesItemContentView.swift */; }; + E18E01E9288747230022598C /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CD288747230022598C /* SeriesItemView.swift */; }; + E18E01EA288747230022598C /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CF288747230022598C /* MovieItemView.swift */; }; + E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D0288747230022598C /* MovieItemContentView.swift */; }; + E18E01EE288747230022598C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D5288747230022598C /* AboutView.swift */; }; + E18E01EF288747230022598C /* ListDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D6288747230022598C /* ListDetailsView.swift */; }; + E18E01F0288747230022598C /* AttributeHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D7288747230022598C /* AttributeHStack.swift */; }; + E18E01F1288747230022598C /* PlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D8288747230022598C /* PlayButton.swift */; }; + E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D9288747230022598C /* ActionButtonHStack.swift */; }; + E18E01FA288747580022598C /* AboutAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01F3288747580022598C /* AboutAppView.swift */; }; + E18E0204288749200022598C /* Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01FF288749200022598C /* Divider.swift */; }; + E18E0205288749200022598C /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0200288749200022598C /* AppIcon.swift */; }; + E18E0206288749200022598C /* AttributeFillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0201288749200022598C /* AttributeFillView.swift */; }; + E18E0207288749200022598C /* AttributeOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0202288749200022598C /* AttributeOutlineView.swift */; }; + E18E0208288749200022598C /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0203288749200022598C /* BlurView.swift */; }; + E18E021A2887492B0022598C /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0200288749200022598C /* AppIcon.swift */; }; + E18E021B2887492B0022598C /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; + E18E021C2887492B0022598C /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0203288749200022598C /* BlurView.swift */; }; + E18E021D2887492B0022598C /* AttributeOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0202288749200022598C /* AttributeOutlineView.swift */; }; + E18E021E2887492B0022598C /* Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01FF288749200022598C /* Divider.swift */; }; + E18E021F2887492B0022598C /* InitialFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* InitialFailureView.swift */; }; + E18E02202887492B0022598C /* AttributeFillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0201288749200022598C /* AttributeFillView.swift */; }; + E18E02212887492B0022598C /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; + E18E02222887492B0022598C /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; + E18E02232887492B0022598C /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; + E18E02242887492B0022598C /* BlurHashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */; }; + E18E02252887492B0022598C /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; }; + E18E023A288749540022598C /* UIScrollViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0239288749540022598C /* UIScrollViewExtensions.swift */; }; + E18E023C288749540022598C /* UIScrollViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0239288749540022598C /* UIScrollViewExtensions.swift */; }; E19169CE272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; }; E19169CF272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; }; - E19169D0272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; }; - E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */; }; - E193D4D927193CAC00900D82 /* PortraitImageStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */; }; + E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */; }; + E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */; }; + E1937A3E288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */; }; + E1937A3F288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */; }; + E1937A61288F32DB00CB80AA /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; }; + E1937A62288F32DB00CB80AA /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; }; E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4DA27193CCA00900D82 /* PillStackable.swift */; }; E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4DA27193CCA00900D82 /* PillStackable.swift */; }; E193D53227193F7B00900D82 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; }; @@ -431,40 +411,49 @@ E193D5502719430400900D82 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54F2719430400900D82 /* ServerDetailView.swift */; }; E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; }; - E1A2C151279A7008005EC829 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C150279A7008005EC829 /* AboutView.swift */; }; + E19E550428972B97003CE330 /* ColourSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54F828972B97003CE330 /* ColourSpace.swift */; }; + E19E550528972B97003CE330 /* ColourSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54F828972B97003CE330 /* ColourSpace.swift */; }; + E19E550628972B97003CE330 /* BlurHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54F928972B97003CE330 /* BlurHash.swift */; }; + E19E550728972B97003CE330 /* BlurHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54F928972B97003CE330 /* BlurHash.swift */; }; + E19E550828972B97003CE330 /* StringCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FA28972B97003CE330 /* StringCoding.swift */; }; + E19E550928972B97003CE330 /* StringCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FA28972B97003CE330 /* StringCoding.swift */; }; + E19E550A28972B97003CE330 /* FromString.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FB28972B97003CE330 /* FromString.swift */; }; + E19E550B28972B97003CE330 /* FromString.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FB28972B97003CE330 /* FromString.swift */; }; + E19E550C28972B97003CE330 /* EscapeSequences.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FC28972B97003CE330 /* EscapeSequences.swift */; }; + E19E550D28972B97003CE330 /* EscapeSequences.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FC28972B97003CE330 /* EscapeSequences.swift */; }; + E19E550E28972B97003CE330 /* ColourProbes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FD28972B97003CE330 /* ColourProbes.swift */; }; + E19E550F28972B97003CE330 /* ColourProbes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FD28972B97003CE330 /* ColourProbes.swift */; }; + E19E551028972B97003CE330 /* ToString.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FE28972B97003CE330 /* ToString.swift */; }; + E19E551128972B97003CE330 /* ToString.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FE28972B97003CE330 /* ToString.swift */; }; + E19E551228972B97003CE330 /* ToUIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FF28972B97003CE330 /* ToUIImage.swift */; }; + E19E551328972B97003CE330 /* ToUIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E54FF28972B97003CE330 /* ToUIImage.swift */; }; + E19E551428972B97003CE330 /* FromUIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E550028972B97003CE330 /* FromUIImage.swift */; }; + E19E551528972B97003CE330 /* FromUIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E550028972B97003CE330 /* FromUIImage.swift */; }; + E19E551828972B97003CE330 /* Generation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E550228972B97003CE330 /* Generation.swift */; }; + E19E551928972B97003CE330 /* Generation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E550228972B97003CE330 /* Generation.swift */; }; + E19E551A28972B97003CE330 /* TupleMaths.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E550328972B97003CE330 /* TupleMaths.swift */; }; + E19E551B28972B97003CE330 /* TupleMaths.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E550328972B97003CE330 /* TupleMaths.swift */; }; + E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */; }; + E1A16C9D2889AF1E00EA4679 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A16C9C2889AF1E00EA4679 /* AboutView.swift */; }; + E1A16CA1288A7CFD00EA4679 /* AboutViewCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A16CA0288A7CFD00EA4679 /* AboutViewCard.swift */; }; E1A2C154279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C153279A7D5A005EC829 /* UIApplicationExtensions.swift */; }; - E1A2C155279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C153279A7D5A005EC829 /* UIApplicationExtensions.swift */; }; E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C153279A7D5A005EC829 /* UIApplicationExtensions.swift */; }; E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C157279A7D76005EC829 /* BundleExtensions.swift */; }; - E1A2C159279A7D76005EC829 /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C157279A7D76005EC829 /* BundleExtensions.swift */; }; E1A2C15A279A7D76005EC829 /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C157279A7D76005EC829 /* BundleExtensions.swift */; }; - E1A2C15C279A7D9F005EC829 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15B279A7D9F005EC829 /* AppIcon.swift */; }; - E1A2C15D279A7D9F005EC829 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15B279A7D9F005EC829 /* AppIcon.swift */; }; - E1A2C15E279A7D9F005EC829 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15B279A7D9F005EC829 /* AppIcon.swift */; }; - E1A2C160279A7DCA005EC829 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15F279A7DCA005EC829 /* AboutView.swift */; }; - E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButtonView.swift */; }; + E1A2C160279A7DCA005EC829 /* AboutAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15F279A7DCA005EC829 /* AboutAppView.swift */; }; + E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButton.swift */; }; E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; E1AA33202782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; - E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA33212782648000F6439C /* OverlaySliderColor.swift */; }; - E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA33212782648000F6439C /* OverlaySliderColor.swift */; }; - E1AA332427829B5200F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; - E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; - E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */; }; - E1AD105926D9A543003E4A08 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; - E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; }; E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; - E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */; }; E1AE8E7C2789135A00FBDDAA /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1AE8E7B2789135A00FBDDAA /* Nuke */; }; E1AE8E7E2789136D00FBDDAA /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1AE8E7D2789136D00FBDDAA /* Nuke */; }; E1B2AB9928808E150072B3B9 /* GoogleCast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1B2AB9628808CDF0072B3B9 /* GoogleCast.xcframework */; }; E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */; }; - E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD82786AE4600A5287E /* NextUpCard.swift */; }; E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE7271A23780015B715 /* CombineExt */; }; E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE9271A23880015B715 /* SwiftyJSON */; }; E1BDE359278E9ED2004E4022 /* MissingItemsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDE358278E9ED2004E4022 /* MissingItemsSettingsView.swift */; }; - E1BDE35B278EA3A3004E4022 /* EpisodesRowCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDE35A278EA3A3004E4022 /* EpisodesRowCard.swift */; }; E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */; }; E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */; }; @@ -477,6 +466,24 @@ E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */; }; E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812D0277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift */; }; E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */; }; + E1C925F428875037002A7A66 /* ItemViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F328875037002A7A66 /* ItemViewType.swift */; }; + E1C925F528875037002A7A66 /* ItemViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F328875037002A7A66 /* ItemViewType.swift */; }; + E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */; }; + E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F828875647002A7A66 /* LatestInLibraryView.swift */; }; + E1C9260B2887565C002A7A66 /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925FB2887565C002A7A66 /* MovieItemView.swift */; }; + E1C9260C2887565C002A7A66 /* MovieItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925FC2887565C002A7A66 /* MovieItemContentView.swift */; }; + E1C9260D2887565C002A7A66 /* CinematicScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925FE2887565C002A7A66 /* CinematicScrollView.swift */; }; + E1C9260F2887565C002A7A66 /* AttributeHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926012887565C002A7A66 /* AttributeHStack.swift */; }; + E1C926102887565C002A7A66 /* PlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926022887565C002A7A66 /* PlayButton.swift */; }; + E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926032887565C002A7A66 /* ActionButtonHStack.swift */; }; + E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926052887565C002A7A66 /* SeriesItemContentView.swift */; }; + E1C926132887565C002A7A66 /* SeriesEpisodesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926072887565C002A7A66 /* SeriesEpisodesView.swift */; }; + E1C926142887565C002A7A66 /* FocusGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926082887565C002A7A66 /* FocusGuide.swift */; }; + E1C926152887565C002A7A66 /* EpisodeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926092887565C002A7A66 /* EpisodeCard.swift */; }; + E1C926162887565C002A7A66 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C9260A2887565C002A7A66 /* SeriesItemView.swift */; }; + E1C9261A288756BD002A7A66 /* PortraitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92617288756BD002A7A66 /* PortraitButton.swift */; }; + E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92618288756BD002A7A66 /* DotHStack.swift */; }; + E1C9261C288756BD002A7A66 /* PortraitPosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92619288756BD002A7A66 /* PortraitPosterHStack.swift */; }; E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */; }; E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */; }; E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */; }; @@ -490,22 +497,11 @@ E1D4BF882719D27100A11E64 /* Bitrates.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF862719D27100A11E64 /* Bitrates.swift */; }; E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */; }; E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */; }; - E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; }; - E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; }; - E1D7E5A827892566009D0EF7 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1D7E5A727892566009D0EF7 /* Nuke */; }; E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; }; E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; }; - E1E00A37278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; }; E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; }; - E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5362783A52C00692DFE /* CinematicEpisodeItemView.swift */; }; - E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */; }; - E1E5D53B2783A80900692DFE /* CinematicItemViewTopRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */; }; - E1E5D53E2783B05200692DFE /* CinematicMovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D53D2783B05200692DFE /* CinematicMovieItemView.swift */; }; - E1E5D5402783B0C000692DFE /* CinematicItemViewTopRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D53F2783B0C000692DFE /* CinematicItemViewTopRowButton.swift */; }; - E1E5D5422783B33900692DFE /* PortraitItemsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */; }; E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */; }; - E1E5D5462783C28100692DFE /* CinematicItemAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5452783C28100692DFE /* CinematicItemAboutView.swift */; }; E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */; }; E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */; }; E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */; }; @@ -515,26 +511,18 @@ E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */; }; E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */; }; E1EBCB4A278BE443009FE6E9 /* ItemOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */; }; + E1EF473A289A0F610034046B /* TruncatedTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */; }; E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */; }; + E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */; }; + E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */; }; E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; }; E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; }; - E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 6264E888273848760081A12A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 5377CBE9263B596A003A4E83 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 628B951F2670CABD0091AF3B; - remoteInfo = WidgetExtension; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 5302F8322658B74800647A2E /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -574,7 +562,6 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 628B952D2670CABE0091AF3B /* Swiftfin Widget.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -584,19 +571,13 @@ /* Begin PBXFileReference section */ 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = ""; }; 09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = ""; }; - 53116A16268B919A003024C9 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; - 53116A18268B947A003024C9 /* PlainLinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainLinkButton.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; - 531690EE267ABF72005D8AB9 /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapeItemElement.swift; sourceTree = ""; }; 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = ""; }; 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = ""; }; 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 = ""; }; 5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 534D4FE826A7D7CC000A7A48 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable.strings; sourceTree = ""; }; 534D4FEC26A7D7CC000A7A48 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = Localizable.strings; sourceTree = ""; }; @@ -627,18 +608,12 @@ 5362E4C8267D40F7000E2F71 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 53649AB0269CFB1900A2D8B7 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemPersonExtensions.swift; sourceTree = ""; }; - 536D3D73267BA8170004248C /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = ""; }; - 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabViewModel.swift; sourceTree = ""; }; - 536D3D7E267BDF100004248C /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = ""; }; 536D3D80267BDFC60004248C /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = ""; }; - 536D3D87267C17350004248C /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = ""; }; 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Swiftfin iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = ""; }; 5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 5377CC02263B596B003A4E83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5389276D263C25100035E14B /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; - 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; 53913BCA26D323FE00EB3286 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable.strings; sourceTree = ""; }; 53913BCD26D323FE00EB3286 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Localizable.strings; sourceTree = ""; }; 53913BD026D323FE00EB3286 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = Localizable.strings; sourceTree = ""; }; @@ -658,21 +633,18 @@ 53ABFDDB267972BF00886593 /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.0.sdk/System/Library/Frameworks/TVServices.framework; sourceTree = DEVELOPER_DIR; }; 53ABFDEA2679753200886593 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 53CD2A3F268A49C2002ABD4E /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; - 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = ""; }; 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; 53DE4BD1267098F300739748 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = ""; }; 53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = ""; }; 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = ""; }; 53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; - 53F866432687A45F00DCD1D7 /* PortraitItemButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemButton.swift; sourceTree = ""; }; - 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = ""; }; + 53F866432687A45F00DCD1D7 /* PortraitPosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitPosterButton.swift; sourceTree = ""; }; 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSize.swift; sourceTree = ""; }; 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VLCPlayer+subtitles.swift"; sourceTree = ""; }; 5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingSwizzling.swift; sourceTree = ""; }; 6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; - 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = ""; }; 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = ""; }; 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryCoordinator.swift; sourceTree = ""; }; @@ -681,10 +653,7 @@ 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = ""; }; 6220D0C526D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSVideoPlayerCoordinator.swift; sourceTree = ""; }; 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.swift; sourceTree = ""; }; - 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = ""; }; - 62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanDirectionGestureRecognizer.swift; sourceTree = ""; }; - 625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerViewModel.swift; sourceTree = ""; }; @@ -717,17 +686,10 @@ 62666E2D27E5021400EC0ECD /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; 62666E2F27E5021800EC0ECD /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/VideoToolbox.framework; sourceTree = DEVELOPER_DIR; }; 62666E3127E5021E00EC0ECD /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; - 62666E3327E5027F00EC0ECD /* Defaults+Workaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Defaults+Workaround.swift"; sourceTree = ""; }; 62666E3A27E503E400EC0ECD /* GoogleCastSDK.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GoogleCastSDK.xcframework; path = Frameworks/GoogleCastSDK.xcframework; sourceTree = ""; }; 6267B3D526710B8900A7371D /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = ""; }; - 6267B3D92671138200A7371D /* ImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageExtensions.swift; sourceTree = ""; }; - 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Swiftfin Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 628B95212670CABD0091AF3B /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 628B95232670CABD0091AF3B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; - 628B95262670CABD0091AF3B /* NextUpWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpWidget.swift; sourceTree = ""; }; - 628B95282670CABE0091AF3B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = ""; }; 62C29E9E26D1016600C1D2E7 /* iOSMainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSMainCoordinator.swift; sourceTree = ""; }; 62C29EA026D102A500C1D2E7 /* iOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSMainTabCoordinator.swift; sourceTree = ""; }; 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerCoodinator.swift; sourceTree = ""; }; @@ -772,7 +734,6 @@ C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesCoordinator.swift; sourceTree = ""; }; C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesViewModel.swift; sourceTree = ""; }; C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesView.swift; sourceTree = ""; }; - C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = ""; }; C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsCoordinator.swift; sourceTree = ""; }; C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = ""; }; C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsViewModel.swift; sourceTree = ""; }; @@ -787,7 +748,6 @@ C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = ""; }; E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerChapterOverlayView.swift; sourceTree = ""; }; E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfoExtensions.swift; sourceTree = ""; }; - E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICinematicBackgroundView.swift; sourceTree = ""; }; E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicBackgroundView.swift; sourceTree = ""; }; E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCinematicView.swift; sourceTree = ""; }; @@ -796,21 +756,27 @@ E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashView.swift; sourceTree = ""; }; E1047E2227E5880000CB0D4A /* InitialFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialFailureView.swift; sourceTree = ""; }; E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = ""; }; - E107BB952788104100354E07 /* CinematicCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicCollectionItemView.swift; sourceTree = ""; }; - E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemSize.swift; sourceTree = ""; }; - E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = ""; }; - E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = ""; }; - E10D87DD278510E300BD264C /* PosterSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterSize.swift; sourceTree = ""; }; + E10D87DB2784EC5200BD264C /* SeriesEpisodesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesEpisodesView.swift; sourceTree = ""; }; E10D87E127852FD000BD264C /* EpisodesRowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowManager.swift; sourceTree = ""; }; E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = ""; }; E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = ""; }; E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectBottomScrollView.swift; sourceTree = ""; }; + E118959C289312020042947B /* BaseItemPerson+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemPerson+Poster.swift"; sourceTree = ""; }; + E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewOffsetModifier.swift; sourceTree = ""; }; + E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarOffsetModifier.swift; sourceTree = ""; }; + E11895AE2893840F0042947B /* NavBarOffsetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarOffsetView.swift; sourceTree = ""; }; + E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundParallaxHeaderModifier.swift; sourceTree = ""; }; E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = ""; }; + E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSViewExtensions.swift; sourceTree = ""; }; + E11CEB8C28999B4A003E74C7 /* FontExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtensions.swift; sourceTree = ""; }; + E11CEB8F28999D84003E74C7 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; + E11CEB9328999D9E003E74C7 /* EpisodeItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemContentView.swift; sourceTree = ""; }; E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = ""; }; E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStreamExtension.swift; sourceTree = ""; }; E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = ""; }; E126F740278A656C00A522BF /* ServerStreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStreamType.swift; sourceTree = ""; }; E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = ""; }; + E1399473289B1EA900401ABC /* Defaults+Workaround.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Defaults+Workaround.swift"; sourceTree = ""; }; E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = ""; }; @@ -828,25 +794,68 @@ E13DD3F82717E961009D4DAF /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = ""; }; E13DD3FB2717EAE8009D4DAF /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = ""; }; E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListCoordinator.swift; sourceTree = ""; }; - E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicSeriesItemView.swift; sourceTree = ""; }; - E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicSeasonItemView.swift; sourceTree = ""; }; - E14B4140279354770016CBE5 /* LocalizedLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedLookup.swift; sourceTree = ""; }; - E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitMainView.swift; sourceTree = ""; }; - E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeMainView.swift; sourceTree = ""; }; + E1546776289AF46E00087E35 /* CollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemView.swift; sourceTree = ""; }; + E1546779289AF48200087E35 /* CollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = ""; }; + E168BD08289A4162001A6922 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + E168BD09289A4162001A6922 /* HomeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeContentView.swift; sourceTree = ""; }; + E168BD0C289A4162001A6922 /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = ""; }; + E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; + E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LatestInLibraryView.swift; sourceTree = ""; }; + E168BD0F289A4162001A6922 /* HomeErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = ""; }; E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = ""; }; - E176DE6C278E30D2001EFD8D /* EpisodeRowCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeRowCard.swift; sourceTree = ""; }; + E176DE6C278E30D2001EFD8D /* EpisodeCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCard.swift; sourceTree = ""; }; E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissingItemsSettingsView.swift; sourceTree = ""; }; E178859A2780F1F40094FBCF /* tvOSSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSSlider.swift; sourceTree = ""; }; E178859D2780F53B0094FBCF /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = ""; }; E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSVLCOverlay.swift; sourceTree = ""; }; E17885A3278105170094FBCF /* SFSymbolButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSymbolButton.swift; sourceTree = ""; }; - E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Stackable.swift"; sourceTree = ""; }; - E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = ""; }; - E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = ""; }; + E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilderExtensions.swift; sourceTree = ""; }; + E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Poster.swift"; sourceTree = ""; }; + E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortraitPosterHStack.swift; sourceTree = ""; }; + E18E01A4288746AF0022598C /* RefreshableScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; + E18E01A5288746AF0022598C /* PillHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillHStack.swift; sourceTree = ""; }; + E18E01A7288746AF0022598C /* DotHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotHStack.swift; sourceTree = ""; }; + E18E01B6288747230022598C /* iPadOSEpisodeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSEpisodeContentView.swift; sourceTree = ""; }; + E18E01B7288747230022598C /* iPadOSEpisodeItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSEpisodeItemView.swift; sourceTree = ""; }; + E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSCinematicScrollView.swift; sourceTree = ""; }; + E18E01BB288747230022598C /* iPadOSSeriesItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSSeriesItemContentView.swift; sourceTree = ""; }; + E18E01BC288747230022598C /* iPadOSSeriesItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSSeriesItemView.swift; sourceTree = ""; }; + E18E01BE288747230022598C /* iPadOSMovieItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSMovieItemView.swift; sourceTree = ""; }; + E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSMovieItemContentView.swift; sourceTree = ""; }; + E18E01C2288747230022598C /* EpisodeItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemContentView.swift; sourceTree = ""; }; + E18E01C3288747230022598C /* EpisodeItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; + E18E01C5288747230022598C /* CompactPortraitScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactPortraitScrollView.swift; sourceTree = ""; }; + E18E01C6288747230022598C /* CompactLogoScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactLogoScrollView.swift; sourceTree = ""; }; + E18E01C7288747230022598C /* CinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CinematicScrollView.swift; sourceTree = ""; }; + E18E01C9288747230022598C /* CollectionItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionItemView.swift; sourceTree = ""; }; + E18E01CA288747230022598C /* CollectionItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = ""; }; + E18E01CC288747230022598C /* SeriesItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemContentView.swift; sourceTree = ""; }; + E18E01CD288747230022598C /* SeriesItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; + E18E01CF288747230022598C /* MovieItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = ""; }; + E18E01D0288747230022598C /* MovieItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemContentView.swift; sourceTree = ""; }; + E18E01D5288747230022598C /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; + E18E01D6288747230022598C /* ListDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDetailsView.swift; sourceTree = ""; }; + E18E01D7288747230022598C /* AttributeHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeHStack.swift; sourceTree = ""; }; + E18E01D8288747230022598C /* PlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayButton.swift; sourceTree = ""; }; + E18E01D9288747230022598C /* ActionButtonHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButtonHStack.swift; sourceTree = ""; }; + E18E01F3288747580022598C /* AboutAppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutAppView.swift; sourceTree = ""; }; + E18E01F5288747580022598C /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + E18E01F6288747580022598C /* HomeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeContentView.swift; sourceTree = ""; }; + E18E01F8288747580022598C /* LatestInLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LatestInLibraryView.swift; sourceTree = ""; }; + E18E01F9288747580022598C /* HomeErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = ""; }; + E18E01FF288749200022598C /* Divider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Divider.swift; sourceTree = ""; }; + E18E0200288749200022598C /* AppIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; + E18E0201288749200022598C /* AttributeFillView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeFillView.swift; sourceTree = ""; }; + E18E0202288749200022598C /* AttributeOutlineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeOutlineView.swift; sourceTree = ""; }; + E18E0203288749200022598C /* BlurView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = ""; }; + E18E0239288749540022598C /* UIScrollViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIScrollViewExtensions.swift; sourceTree = ""; }; E19169CD272514760085832A /* HTTPScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPScheme.swift; sourceTree = ""; }; - E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitImageStackable.swift; sourceTree = ""; }; + E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Images.swift"; sourceTree = ""; }; + E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScreenExtensions.swift; sourceTree = ""; }; + E1937A60288F32DB00CB80AA /* Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poster.swift; sourceTree = ""; }; + E1937A63288F683300CB80AA /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = ""; }; E193D4DA27193CCA00900D82 /* PillStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillStackable.swift; sourceTree = ""; }; E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainCoordinator.swift; sourceTree = ""; }; E193D546271941C500900D82 /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = ""; }; @@ -855,24 +864,30 @@ E193D54C2719426600900D82 /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = ""; }; E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = ""; }; - E1A2C150279A7008005EC829 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; + E19E54F828972B97003CE330 /* ColourSpace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColourSpace.swift; sourceTree = ""; }; + E19E54F928972B97003CE330 /* BlurHash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHash.swift; sourceTree = ""; }; + E19E54FA28972B97003CE330 /* StringCoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringCoding.swift; sourceTree = ""; }; + E19E54FB28972B97003CE330 /* FromString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FromString.swift; sourceTree = ""; }; + E19E54FC28972B97003CE330 /* EscapeSequences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EscapeSequences.swift; sourceTree = ""; }; + E19E54FD28972B97003CE330 /* ColourProbes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColourProbes.swift; sourceTree = ""; }; + E19E54FE28972B97003CE330 /* ToString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToString.swift; sourceTree = ""; }; + E19E54FF28972B97003CE330 /* ToUIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToUIImage.swift; sourceTree = ""; }; + E19E550028972B97003CE330 /* FromUIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FromUIImage.swift; sourceTree = ""; }; + E19E550228972B97003CE330 /* Generation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Generation.swift; sourceTree = ""; }; + E19E550328972B97003CE330 /* TupleMaths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TupleMaths.swift; sourceTree = ""; }; + E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomEdgeGradientModifier.swift; sourceTree = ""; }; + E1A16C9C2889AF1E00EA4679 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; + E1A16CA0288A7CFD00EA4679 /* AboutViewCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewCard.swift; sourceTree = ""; }; E1A2C153279A7D5A005EC829 /* UIApplicationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtensions.swift; sourceTree = ""; }; E1A2C157279A7D76005EC829 /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = ""; }; - E1A2C15B279A7D9F005EC829 /* AppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; - E1A2C15F279A7DCA005EC829 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; - E1AA331C2782541500F6439C /* PrimaryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButtonView.swift; sourceTree = ""; }; + E1A2C15F279A7DCA005EC829 /* AboutAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutAppView.swift; sourceTree = ""; }; + E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = ""; }; E1AA331E2782639D00F6439C /* OverlayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayType.swift; sourceTree = ""; }; - E1AA33212782648000F6439C /* OverlaySliderColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySliderColor.swift; sourceTree = ""; }; E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = ""; }; - E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = ""; }; - E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillHStackView.swift; sourceTree = ""; }; E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGUIDPairExtensions.swift; sourceTree = ""; }; - E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitHeaderOverlayView.swift; sourceTree = ""; }; E1B2AB9628808CDF0072B3B9 /* GoogleCast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GoogleCast.xcframework; path = Carthage/Build/GoogleCast.xcframework; sourceTree = ""; }; E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = ""; }; - E1B59FD82786AE4600A5287E /* NextUpCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpCard.swift; sourceTree = ""; }; E1BDE358278E9ED2004E4022 /* MissingItemsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissingItemsSettingsView.swift; sourceTree = ""; }; - E1BDE35A278EA3A3004E4022 /* EpisodesRowCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowCard.swift; sourceTree = ""; }; E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = ""; }; E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = ""; }; E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = ""; }; @@ -883,6 +898,23 @@ E1C812C8277AE40900918266 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; E1C812D0277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSVideoPlayerCoordinator.swift; sourceTree = ""; }; + E1C925F328875037002A7A66 /* ItemViewType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemViewType.swift; sourceTree = ""; }; + E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanDirectionGestureRecognizer.swift; sourceTree = ""; }; + E1C925F828875647002A7A66 /* LatestInLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LatestInLibraryView.swift; sourceTree = ""; }; + E1C925FB2887565C002A7A66 /* MovieItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = ""; }; + E1C925FC2887565C002A7A66 /* MovieItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemContentView.swift; sourceTree = ""; }; + E1C925FE2887565C002A7A66 /* CinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CinematicScrollView.swift; sourceTree = ""; }; + E1C926012887565C002A7A66 /* AttributeHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeHStack.swift; sourceTree = ""; }; + E1C926022887565C002A7A66 /* PlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayButton.swift; sourceTree = ""; }; + E1C926032887565C002A7A66 /* ActionButtonHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButtonHStack.swift; sourceTree = ""; }; + E1C926052887565C002A7A66 /* SeriesItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemContentView.swift; sourceTree = ""; }; + E1C926072887565C002A7A66 /* SeriesEpisodesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesEpisodesView.swift; sourceTree = ""; }; + E1C926082887565C002A7A66 /* FocusGuide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusGuide.swift; sourceTree = ""; }; + E1C926092887565C002A7A66 /* EpisodeCard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCard.swift; sourceTree = ""; }; + E1C9260A2887565C002A7A66 /* SeriesItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; + E1C92617288756BD002A7A66 /* PortraitButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortraitButton.swift; sourceTree = ""; }; + E1C92618288756BD002A7A66 /* DotHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotHStack.swift; sourceTree = ""; }; + E1C92619288756BD002A7A66 /* PortraitPosterHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortraitPosterHStack.swift; sourceTree = ""; }; E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = ""; }; @@ -894,14 +926,7 @@ E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = ""; }; E1E00A34278628A40022235B /* DoubleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleExtensions.swift; sourceTree = ""; }; E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = ""; }; - E1E5D5362783A52C00692DFE /* CinematicEpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicEpisodeItemView.swift; sourceTree = ""; }; - E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = ""; }; - E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemViewTopRow.swift; sourceTree = ""; }; - E1E5D53D2783B05200692DFE /* CinematicMovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicMovieItemView.swift; sourceTree = ""; }; - E1E5D53F2783B0C000692DFE /* CinematicItemViewTopRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemViewTopRowButton.swift; sourceTree = ""; }; - E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemsRowView.swift; sourceTree = ""; }; E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailsView.swift; sourceTree = ""; }; - E1E5D5452783C28100692DFE /* CinematicItemAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemAboutView.swift; sourceTree = ""; }; E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = ""; }; E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = ""; }; E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = ""; }; @@ -912,6 +937,8 @@ E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewView.swift; sourceTree = ""; }; E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = ""; }; E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallMenuOverlay.swift; sourceTree = ""; }; + E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemView.swift; sourceTree = ""; }; + E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemContentView.swift; sourceTree = ""; }; E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -937,7 +964,6 @@ 637FCAF5287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, 62666E1B27E501D400EC0ECD /* CoreGraphics.framework in Frameworks */, - 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, 62666E2C27E5021000EC0ECD /* QuartzCore.framework in Frameworks */, 62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */, 62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */, @@ -997,21 +1023,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 628B951D2670CABD0091AF3B /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - E1347DB4279E3C9E00BC6161 /* Puppy in Frameworks */, - E1D7E5A827892566009D0EF7 /* Nuke in Frameworks */, - 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */, - 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */, - E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */, - 536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */, - E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */, - E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -1054,7 +1065,6 @@ 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, - 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */, E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */, @@ -1124,6 +1134,7 @@ 535870662669D21700D05A09 /* Assets.xcassets */, 536D3D77267BB9650004248C /* Components */, 535870702669D21700D05A09 /* Info.plist */, + E1A16C9328875F2F00EA4679 /* Objects */, 535870682669D21700D05A09 /* Preview Content */, E12186E02718F23B0010884C /* Views */, ); @@ -1141,8 +1152,7 @@ 535870752669D60C00D05A09 /* Shared */ = { isa = PBXGroup; children = ( - 625534272821908D0087FE20 /* UIKit */, - 6286F09F271C0AA500C40ED5 /* Generated */, + E19E54F728972B97003CE330 /* BlurHashKit */, 62C29E9D26D0FE5900C1D2E7 /* Coordinators */, E1FCD08E26C466F3007C8DCF /* Errors */, 621338912660106C00A81A2A /* Extensions */, @@ -1150,6 +1160,7 @@ AE8C3157265D6F5E008AA076 /* Resources */, 091B5A852683142E00D78B61 /* ServerDiscovery */, 62EC352A26766657000E9F2D /* Singleton */, + 6286F09F271C0AA500C40ED5 /* Strings */, E13DD3C0271648EC009D4DAF /* SwiftfinStore */, 532175392671BCED005491E6 /* ViewModels */, E1AD105326D96F5A003E4A08 /* Views */, @@ -1162,15 +1173,15 @@ children = ( E1D4BF802719D22800A11E64 /* AppAppearance.swift */, E1D4BF862719D27100A11E64 /* Bitrates.swift */, + E1C925F328875037002A7A66 /* ItemViewType.swift */, 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, E19169CD272514760085832A /* HTTPScheme.swift */, - E1AA33212782648000F6439C /* OverlaySliderColor.swift */, E1AA331E2782639D00F6439C /* OverlayType.swift */, + E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */, E193D4DA27193CCA00900D82 /* PillStackable.swift */, E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, - E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */, - E10D87DD278510E300BD264C /* PosterSize.swift */, + E1937A60288F32DB00CB80AA /* Poster.swift */, 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */, E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */, 535870AC2669D8DD00D05A09 /* Typings.swift */, @@ -1182,16 +1193,13 @@ 536D3D77267BB9650004248C /* Components */ = { isa = PBXGroup; children = ( - E1BDE35C278EA3A7004E4022 /* EpisodesRowView */, + E1C92618288756BD002A7A66 /* DotHStack.swift */, E103A6A1278A7EB500820EC7 /* HomeCinematicView */, E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */, 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, - E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */, - 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */, - 53116A18268B947A003024C9 /* PlainLinkButton.swift */, + E1C92617288756BD002A7A66 /* PortraitButton.swift */, + E1C92619288756BD002A7A66 /* PortraitPosterHStack.swift */, 536D3D80267BDFC60004248C /* PortraitItemElement.swift */, - E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */, - 536D3D87267C17350004248C /* PublicUserButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, ); path = Components; @@ -1206,7 +1214,7 @@ 535870612669D21600D05A09 /* Swiftfin tvOS */, 5377CBF2263B596A003A4E83 /* Products */, 535870752669D60C00D05A09 /* Shared */, - 628B95252670CABD0091AF3B /* WidgetExtension */, + E168BD06289A414B001A6922 /* Recovered References */, ); sourceTree = ""; }; @@ -1215,7 +1223,6 @@ children = ( 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */, 535870602669D21600D05A09 /* Swiftfin tvOS.app */, - 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */, ); name = Products; sourceTree = ""; @@ -1230,6 +1237,7 @@ 53F866422687A45400DCD1D7 /* Components */, 5377CC02263B596B003A4E83 /* Info.plist */, E13D02842788B634000FCB04 /* Swiftfin.entitlements */, + E11CEB85289984F5003E74C7 /* Extensions */, 5377CBFA263B596B003A4E83 /* Preview Content */, E13DD3D027165886009D4DAF /* Views */, ); @@ -1422,15 +1430,13 @@ 53F866422687A45400DCD1D7 /* Components */ = { isa = PBXGroup; children = ( - E1A2C15B279A7D9F005EC829 /* AppIcon.swift */, E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */, - E176DE6E278E3522001EFD8D /* EpisodesRowView */, - E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */, - E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */, - 53F866432687A45F00DCD1D7 /* PortraitItemButton.swift */, - C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */, - E1AA331C2782541500F6439C /* PrimaryButtonView.swift */, - E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */, + E18E01A7288746AF0022598C /* DotHStack.swift */, + E18E01A5288746AF0022598C /* PillHStack.swift */, + E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */, + 53F866432687A45F00DCD1D7 /* PortraitPosterButton.swift */, + E1AA331C2782541500F6439C /* PrimaryButton.swift */, + E18E01A4288746AF0022598C /* RefreshableScrollView.swift */, ); path = Components; sourceTree = ""; @@ -1447,52 +1453,33 @@ 621338912660106C00A81A2A /* Extensions */ = { isa = PBXGroup; children = ( - 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */, + E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */, E1A2C157279A7D76005EC829 /* BundleExtensions.swift */, E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */, 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */, + E1399473289B1EA900401ABC /* Defaults+Workaround.swift */, E1E00A34278628A40022235B /* DoubleExtensions.swift */, - 6267B3D92671138200A7371D /* ImageExtensions.swift */, - E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */, + E11CEB8C28999B4A003E74C7 /* FontExtensions.swift */, 621338922660107500A81A2A /* StringExtensions.swift */, E1A2C153279A7D5A005EC829 /* UIApplicationExtensions.swift */, E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */, + E18E0239288749540022598C /* UIScrollViewExtensions.swift */, + E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */, E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */, 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */, - 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */, + E11895A22893409D0042947B /* ViewExtensions */, 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */, - 62666E3327E5027F00EC0ECD /* Defaults+Workaround.swift */, ); path = Extensions; sourceTree = ""; }; - 625534272821908D0087FE20 /* UIKit */ = { - isa = PBXGroup; - children = ( - 62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */, - ); - path = UIKit; - sourceTree = ""; - }; - 6286F09F271C0AA500C40ED5 /* Generated */ = { + 6286F09F271C0AA500C40ED5 /* Strings */ = { isa = PBXGroup; children = ( 6264E88B273850380081A12A /* Strings.swift */, - E14B4140279354770016CBE5 /* LocalizedLookup.swift */, ); - path = Generated; - sourceTree = ""; - }; - 628B95252670CABD0091AF3B /* WidgetExtension */ = { - isa = PBXGroup; - children = ( - 628B95362670CB800091AF3B /* JellyfinWidget.swift */, - 628B95262670CABD0091AF3B /* NextUpWidget.swift */, - 628B95282670CABE0091AF3B /* Assets.xcassets */, - 628B952A2670CABE0091AF3B /* Info.plist */, - ); - path = WidgetExtension; + path = Strings; sourceTree = ""; }; 62C29E9D26D0FE5900C1D2E7 /* Coordinators */ = { @@ -1527,7 +1514,6 @@ 62EC352A26766657000E9F2D /* Singleton */ = { isa = PBXGroup; children = ( - 536D3D73267BA8170004248C /* BackgroundManager.swift */, 53649AB0269CFB1900A2D8B7 /* LogManager.swift */, 62EC352E267666A5000E9F2D /* SessionManager.swift */, E13DD3EE27178F87009D4DAF /* SwiftfinNotificationCenter.swift */, @@ -1586,6 +1572,52 @@ path = ItemViewModel; sourceTree = ""; }; + E11895A22893409D0042947B /* ViewExtensions */ = { + isa = PBXGroup; + children = ( + E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */, + E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */, + E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */, + 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */, + ); + path = ViewExtensions; + sourceTree = ""; + }; + E11895B12893842D0042947B /* NavBarOffsetModifier */ = { + isa = PBXGroup; + children = ( + E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */, + E11895AE2893840F0042947B /* NavBarOffsetView.swift */, + ); + path = NavBarOffsetModifier; + sourceTree = ""; + }; + E11CEB85289984F5003E74C7 /* Extensions */ = { + isa = PBXGroup; + children = ( + E11CEB8828998522003E74C7 /* iOSViewExtensions */, + ); + path = Extensions; + sourceTree = ""; + }; + E11CEB8828998522003E74C7 /* iOSViewExtensions */ = { + isa = PBXGroup; + children = ( + E11895B12893842D0042947B /* NavBarOffsetModifier */, + E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */, + ); + path = iOSViewExtensions; + sourceTree = ""; + }; + E11CEB9228999D8D003E74C7 /* EpisodeItemView */ = { + isa = PBXGroup; + children = ( + E11CEB9328999D9E003E74C7 /* EpisodeItemContentView.swift */, + E11CEB8F28999D84003E74C7 /* EpisodeItemView.swift */, + ); + path = EpisodeItemView; + sourceTree = ""; + }; E12186DF2718F2030010884C /* App */ = { isa = PBXGroup; children = ( @@ -1597,14 +1629,14 @@ E12186E02718F23B0010884C /* Views */ = { isa = PBXGroup; children = ( - E1A2C15F279A7DCA005EC829 /* AboutView.swift */, + E1A2C15F279A7DCA005EC829 /* AboutAppView.swift */, E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */, 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, E1B59FD62786AE2C00A5287E /* ContinueWatchingView */, 531690E6267ABD79005D8AB9 /* HomeView.swift */, E193D54E271942C000900D82 /* ItemView */, - 536D3D7E267BDF100004248C /* LatestMediaView.swift */, E193D54C2719426600900D82 /* LibraryFilterView.swift */, + E1C925F828875647002A7A66 /* LatestInLibraryView.swift */, C4E508172703E8190045C9AB /* LibraryListView.swift */, C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */, @@ -1613,7 +1645,6 @@ C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */, C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */, C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, - E1B59FD72786AE3E00A5287E /* NextUpView */, E193D54F2719430400900D82 /* ServerDetailView.swift */, E193D54A271941D300900D82 /* ServerListView.swift */, E1E5D54D2783E66600692DFE /* SettingsView */, @@ -1656,63 +1687,99 @@ E13DD3D027165886009D4DAF /* Views */ = { isa = PBXGroup; children = ( - E1A2C150279A7008005EC829 /* AboutView.swift */, + E18E01F3288747580022598C /* AboutAppView.swift */, E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, - 5389276D263C25100035E14B /* ContinueWatchingView.swift */, 62C83B07288C6A630004ED0C /* FontPicker.swift */, - 625CB56E2678C23300530A6E /* HomeView.swift */, + E168BD07289A4162001A6922 /* HomeView */, E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */, E14F7D0A26DB3714007C3AE6 /* ItemView */, - 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */, 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, 6213388F265F83A900A81A2A /* LibraryListView.swift */, 53EE24E5265060780068F029 /* LibrarySearchView.swift */, - C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */, + 53DF641D263D9C0600A7CD1A /* LibraryView.swift */, C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */, C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */, - 53DF641D263D9C0600A7CD1A /* LibraryView.swift */, + C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */, C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */, C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */, + 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */, E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */, E13DD3E427177D15009D4DAF /* ServerListView.swift */, E1E5D54A2783E26100692DFE /* SettingsView */, E13DD3FB2717EAE8009D4DAF /* UserListView.swift */, E13DD3F4271793BB009D4DAF /* UserSignInView.swift */, - 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */, E193D5452719418B00900D82 /* VideoPlayer */, ); path = Views; sourceTree = ""; }; - E13F26AD27874ECC00DF4761 /* CompactItemView */ = { - isa = PBXGroup; - children = ( - 53272538268C20100035FBF1 /* EpisodeItemView.swift */, - 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */, - 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */, - 53116A16268B919A003024C9 /* SeriesItemView.swift */, - ); - path = CompactItemView; - sourceTree = ""; - }; E14F7D0A26DB3714007C3AE6 /* ItemView */ = { isa = PBXGroup; children = ( 535BAE9E2649E569005FA86D /* ItemView.swift */, - E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */, - E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */, - E18845FB26DEACC400B0C5B7 /* Landscape */, - E18845FA26DEACBE00B0C5B7 /* Portrait */, + E18E01D4288747230022598C /* Components */, + E18E01C0288747230022598C /* iOS */, + E18E01B4288747230022598C /* iPadOS */, ); path = ItemView; sourceTree = ""; }; + E1546778289AF47100087E35 /* CollectionItemView */ = { + isa = PBXGroup; + children = ( + E1546776289AF46E00087E35 /* CollectionItemView.swift */, + E1546779289AF48200087E35 /* CollectionItemContentView.swift */, + ); + path = CollectionItemView; + sourceTree = ""; + }; + E168BD06289A414B001A6922 /* Recovered References */ = { + isa = PBXGroup; + children = ( + E18E01F9288747580022598C /* HomeErrorView.swift */, + E18E01F8288747580022598C /* LatestInLibraryView.swift */, + E18E01F6288747580022598C /* HomeContentView.swift */, + E18E01F5288747580022598C /* HomeView.swift */, + E1937A63288F683300CB80AA /* ContinueWatchingCard.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; + E168BD07289A4162001A6922 /* HomeView */ = { + isa = PBXGroup; + children = ( + E168BD0A289A4162001A6922 /* Components */, + E168BD09289A4162001A6922 /* HomeContentView.swift */, + E168BD0F289A4162001A6922 /* HomeErrorView.swift */, + E168BD08289A4162001A6922 /* HomeView.swift */, + ); + path = HomeView; + sourceTree = ""; + }; + E168BD0A289A4162001A6922 /* Components */ = { + isa = PBXGroup; + children = ( + E168BD0B289A4162001A6922 /* ContinueWatchingView */, + E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */, + ); + path = Components; + sourceTree = ""; + }; + E168BD0B289A4162001A6922 /* ContinueWatchingView */ = { + isa = PBXGroup; + children = ( + E168BD0C289A4162001A6922 /* ContinueWatchingCard.swift */, + E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */, + ); + path = ContinueWatchingView; + sourceTree = ""; + }; E176DE6E278E3522001EFD8D /* EpisodesRowView */ = { isa = PBXGroup; children = ( - E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */, - E176DE6C278E30D2001EFD8D /* EpisodeRowCard.swift */, + E10D87DB2784EC5200BD264C /* SeriesEpisodesView.swift */, + E176DE6C278E30D2001EFD8D /* EpisodeCard.swift */, ); path = EpisodesRowView; sourceTree = ""; @@ -1737,22 +1804,122 @@ path = Overlays; sourceTree = ""; }; - E18845FA26DEACBE00B0C5B7 /* Portrait */ = { + E18E01B4288747230022598C /* iPadOS */ = { isa = PBXGroup; children = ( - E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */, - E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */, + E1FA891C289A302600176FEB /* CollectionItemView */, + E18E01B5288747230022598C /* EpisodeItemView */, + E18E01BD288747230022598C /* MovieItemView */, + E18E01B8288747230022598C /* ScrollViews */, + E18E01BA288747230022598C /* SeriesItemView */, ); - path = Portrait; + path = iPadOS; sourceTree = ""; }; - E18845FB26DEACC400B0C5B7 /* Landscape */ = { + E18E01B5288747230022598C /* EpisodeItemView */ = { isa = PBXGroup; children = ( - E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */, - E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */, + E18E01B6288747230022598C /* iPadOSEpisodeContentView.swift */, + E18E01B7288747230022598C /* iPadOSEpisodeItemView.swift */, ); - path = Landscape; + path = EpisodeItemView; + sourceTree = ""; + }; + E18E01B8288747230022598C /* ScrollViews */ = { + isa = PBXGroup; + children = ( + E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */, + ); + path = ScrollViews; + sourceTree = ""; + }; + E18E01BA288747230022598C /* SeriesItemView */ = { + isa = PBXGroup; + children = ( + E18E01BB288747230022598C /* iPadOSSeriesItemContentView.swift */, + E18E01BC288747230022598C /* iPadOSSeriesItemView.swift */, + ); + path = SeriesItemView; + sourceTree = ""; + }; + E18E01BD288747230022598C /* MovieItemView */ = { + isa = PBXGroup; + children = ( + E18E01BE288747230022598C /* iPadOSMovieItemView.swift */, + E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */, + ); + path = MovieItemView; + sourceTree = ""; + }; + E18E01C0288747230022598C /* iOS */ = { + isa = PBXGroup; + children = ( + E18E01C8288747230022598C /* CollectionItemView */, + E18E01C1288747230022598C /* EpisodeItemView */, + E18E01CE288747230022598C /* MovieItemView */, + E18E01C4288747230022598C /* ScrollViews */, + E18E01CB288747230022598C /* SeriesItemView */, + ); + path = iOS; + sourceTree = ""; + }; + E18E01C1288747230022598C /* EpisodeItemView */ = { + isa = PBXGroup; + children = ( + E18E01C2288747230022598C /* EpisodeItemContentView.swift */, + E18E01C3288747230022598C /* EpisodeItemView.swift */, + ); + path = EpisodeItemView; + sourceTree = ""; + }; + E18E01C4288747230022598C /* ScrollViews */ = { + isa = PBXGroup; + children = ( + E18E01C5288747230022598C /* CompactPortraitScrollView.swift */, + E18E01C6288747230022598C /* CompactLogoScrollView.swift */, + E18E01C7288747230022598C /* CinematicScrollView.swift */, + ); + path = ScrollViews; + sourceTree = ""; + }; + E18E01C8288747230022598C /* CollectionItemView */ = { + isa = PBXGroup; + children = ( + E18E01C9288747230022598C /* CollectionItemView.swift */, + E18E01CA288747230022598C /* CollectionItemContentView.swift */, + ); + path = CollectionItemView; + sourceTree = ""; + }; + E18E01CB288747230022598C /* SeriesItemView */ = { + isa = PBXGroup; + children = ( + E18E01CC288747230022598C /* SeriesItemContentView.swift */, + E18E01CD288747230022598C /* SeriesItemView.swift */, + ); + path = SeriesItemView; + sourceTree = ""; + }; + E18E01CE288747230022598C /* MovieItemView */ = { + isa = PBXGroup; + children = ( + E18E01D0288747230022598C /* MovieItemContentView.swift */, + E18E01CF288747230022598C /* MovieItemView.swift */, + ); + path = MovieItemView; + sourceTree = ""; + }; + E18E01D4288747230022598C /* Components */ = { + isa = PBXGroup; + children = ( + E176DE6E278E3522001EFD8D /* EpisodesRowView */, + E18E01D5288747230022598C /* AboutView.swift */, + E18E01D6288747230022598C /* ListDetailsView.swift */, + E18E01D7288747230022598C /* AttributeHStack.swift */, + E18E01D8288747230022598C /* PlayButton.swift */, + E18E01D9288747230022598C /* ActionButtonHStack.swift */, + ); + path = Components; sourceTree = ""; }; E193D5412719404B00900D82 /* MainCoordinator */ = { @@ -1784,24 +1951,65 @@ E193D54E271942C000900D82 /* ItemView */ = { isa = PBXGroup; children = ( - E1E5D53C2783A85F00692DFE /* CinematicItemView */, - E13F26AD27874ECC00DF4761 /* CompactItemView */, 53CD2A3F268A49C2002ABD4E /* ItemView.swift */, + E1546778289AF47100087E35 /* CollectionItemView */, + E1C925FF2887565C002A7A66 /* Components */, + E1C925FA2887565C002A7A66 /* MovieItemView */, + E1C925FD2887565C002A7A66 /* ScrollViews */, + E1C926042887565C002A7A66 /* SeriesItemView */, ); path = ItemView; sourceTree = ""; }; + E19E54F728972B97003CE330 /* BlurHashKit */ = { + isa = PBXGroup; + children = ( + E19E54F828972B97003CE330 /* ColourSpace.swift */, + E19E54F928972B97003CE330 /* BlurHash.swift */, + E19E54FA28972B97003CE330 /* StringCoding.swift */, + E19E54FB28972B97003CE330 /* FromString.swift */, + E19E54FC28972B97003CE330 /* EscapeSequences.swift */, + E19E54FD28972B97003CE330 /* ColourProbes.swift */, + E19E54FE28972B97003CE330 /* ToString.swift */, + E19E54FF28972B97003CE330 /* ToUIImage.swift */, + E19E550028972B97003CE330 /* FromUIImage.swift */, + E19E550228972B97003CE330 /* Generation.swift */, + E19E550328972B97003CE330 /* TupleMaths.swift */, + ); + path = BlurHashKit; + sourceTree = ""; + }; + E1A16C9328875F2F00EA4679 /* Objects */ = { + isa = PBXGroup; + children = ( + E1C926082887565C002A7A66 /* FocusGuide.swift */, + ); + path = Objects; + sourceTree = ""; + }; + E1A16CA2288A7D0000EA4679 /* AboutView */ = { + isa = PBXGroup; + children = ( + E1A16C9C2889AF1E00EA4679 /* AboutView.swift */, + E1A16CA0288A7CFD00EA4679 /* AboutViewCard.swift */, + ); + path = AboutView; + sourceTree = ""; + }; E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { isa = PBXGroup; children = ( - E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */, + E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */, + E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */, E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */, E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, + E118959C289312020042947B /* BaseItemPerson+Poster.swift */, 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, + E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */, ); path = JellyfinAPIExtensions; sourceTree = ""; @@ -1809,16 +2017,19 @@ E1AD105326D96F5A003E4A08 /* Views */ = { isa = PBXGroup; children = ( + E18E0200288749200022598C /* AppIcon.swift */, + E18E0201288749200022598C /* AttributeFillView.swift */, + E18E0202288749200022598C /* AttributeOutlineView.swift */, E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */, + E18E0203288749200022598C /* BlurView.swift */, + E18E01FF288749200022598C /* Divider.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, E1047E2227E5880000CB0D4A /* InitialFailureView.swift */, - 621338B22660A07800A81A2A /* LazyView.swift */, 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */, - 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */, 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, - E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */, 624C21742685CF60007F1390 /* SearchablePickerView.swift */, 53DE4BD1267098F300739748 /* SearchBarView.swift */, + E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */, ); path = Views; sourceTree = ""; @@ -1832,24 +2043,6 @@ path = ContinueWatchingView; sourceTree = ""; }; - E1B59FD72786AE3E00A5287E /* NextUpView */ = { - isa = PBXGroup; - children = ( - 531690EE267ABF72005D8AB9 /* NextUpView.swift */, - E1B59FD82786AE4600A5287E /* NextUpCard.swift */, - ); - path = NextUpView; - sourceTree = ""; - }; - E1BDE35C278EA3A7004E4022 /* EpisodesRowView */ = { - isa = PBXGroup; - children = ( - E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */, - E1BDE35A278EA3A3004E4022 /* EpisodesRowCard.swift */, - ); - path = EpisodesRowView; - sourceTree = ""; - }; E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */ = { isa = PBXGroup; children = ( @@ -1861,6 +2054,54 @@ path = VideoPlayerCoordinator; sourceTree = ""; }; + E1C925FA2887565C002A7A66 /* MovieItemView */ = { + isa = PBXGroup; + children = ( + E1C925FC2887565C002A7A66 /* MovieItemContentView.swift */, + E1C925FB2887565C002A7A66 /* MovieItemView.swift */, + ); + path = MovieItemView; + sourceTree = ""; + }; + E1C925FD2887565C002A7A66 /* ScrollViews */ = { + isa = PBXGroup; + children = ( + E1C925FE2887565C002A7A66 /* CinematicScrollView.swift */, + ); + path = ScrollViews; + sourceTree = ""; + }; + E1C925FF2887565C002A7A66 /* Components */ = { + isa = PBXGroup; + children = ( + E11CEB9228999D8D003E74C7 /* EpisodeItemView */, + E1A16CA2288A7D0000EA4679 /* AboutView */, + E1C926032887565C002A7A66 /* ActionButtonHStack.swift */, + E1C926012887565C002A7A66 /* AttributeHStack.swift */, + E1C926022887565C002A7A66 /* PlayButton.swift */, + ); + path = Components; + sourceTree = ""; + }; + E1C926042887565C002A7A66 /* SeriesItemView */ = { + isa = PBXGroup; + children = ( + E1C926062887565C002A7A66 /* Components */, + E1C926052887565C002A7A66 /* SeriesItemContentView.swift */, + E1C9260A2887565C002A7A66 /* SeriesItemView.swift */, + ); + path = SeriesItemView; + sourceTree = ""; + }; + E1C926062887565C002A7A66 /* Components */ = { + isa = PBXGroup; + children = ( + E1C926072887565C002A7A66 /* SeriesEpisodesView.swift */, + E1C926092887565C002A7A66 /* EpisodeCard.swift */, + ); + path = Components; + sourceTree = ""; + }; E1DD1127271E7D15005BE12F /* Objects */ = { isa = PBXGroup; children = ( @@ -1869,21 +2110,6 @@ path = Objects; sourceTree = ""; }; - E1E5D53C2783A85F00692DFE /* CinematicItemView */ = { - isa = PBXGroup; - children = ( - E107BB952788104100354E07 /* CinematicCollectionItemView.swift */, - E1E5D5362783A52C00692DFE /* CinematicEpisodeItemView.swift */, - E1E5D5452783C28100692DFE /* CinematicItemAboutView.swift */, - E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */, - E1E5D53F2783B0C000692DFE /* CinematicItemViewTopRowButton.swift */, - E1E5D53D2783B05200692DFE /* CinematicMovieItemView.swift */, - E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */, - E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */, - ); - path = CinematicItemView; - sourceTree = ""; - }; E1E5D54A2783E26100692DFE /* SettingsView */ = { isa = PBXGroup; children = ( @@ -1909,6 +2135,15 @@ path = SettingsView; sourceTree = ""; }; + E1FA891C289A302600176FEB /* CollectionItemView */ = { + isa = PBXGroup; + children = ( + E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */, + E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */, + ); + path = CollectionItemView; + sourceTree = ""; + }; E1FCD08E26C466F3007C8DCF /* Errors */ = { isa = PBXGroup; children = ( @@ -1939,7 +2174,6 @@ packageProductDependencies = ( 535870902669D7A800D05A09 /* Introspect */, 53ABFDEC26799D7700886593 /* ActivityIndicator */, - 536D3D83267BEA550004248C /* ParallaxView */, 6220D0C826D63F3700B8E046 /* Stinsen */, E13DD3CC27164CA7009D4DAF /* CoreStore */, E12186DD2718F1C50010884C /* Defaults */, @@ -1970,7 +2204,6 @@ buildRules = ( ); dependencies = ( - 6264E889273848760081A12A /* PBXTargetDependency */, ); name = "Swiftfin iOS"; packageProductDependencies = ( @@ -1995,31 +2228,6 @@ productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; productType = "com.apple.product-type.application"; }; - 628B951F2670CABD0091AF3B /* Swiftfin Widget */ = { - isa = PBXNativeTarget; - buildConfigurationList = 628B952E2670CABE0091AF3B /* Build configuration list for PBXNativeTarget "Swiftfin Widget" */; - buildPhases = ( - 628B951C2670CABD0091AF3B /* Sources */, - 628B951D2670CABD0091AF3B /* Frameworks */, - 628B951E2670CABD0091AF3B /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "Swiftfin Widget"; - packageProductDependencies = ( - 536D3D7C267BD5F90004248C /* ActivityIndicator */, - E13DD3CE27164E1F009D4DAF /* CoreStore */, - E13DD3DC27175CE3009D4DAF /* Defaults */, - E10EAA46277BB670000269ED /* JellyfinAPI */, - E1D7E5A727892566009D0EF7 /* Nuke */, - E1347DB3279E3C9E00BC6161 /* Puppy */, - ); - productName = WidgetExtensionExtension; - productReference = 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */; - productType = "com.apple.product-type.app-extension"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -2038,9 +2246,6 @@ 5377CBF0263B596A003A4E83 = { CreatedOnToolsVersion = 12.5; }; - 628B951F2670CABD0091AF3B = { - CreatedOnToolsVersion = 12.5; - }; }; }; buildConfigurationList = 5377CBEC263B596A003A4E83 /* Build configuration list for PBXProject "Swiftfin" */; @@ -2070,7 +2275,6 @@ packageReferences = ( 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, - 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */, E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */, E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */, @@ -2091,7 +2295,6 @@ projectRoot = ""; targets = ( 5377CBF0263B596A003A4E83 /* Swiftfin iOS */, - 628B951F2670CABD0091AF3B /* Swiftfin Widget */, 5358705F2669D21600D05A09 /* Swiftfin tvOS */, ); }; @@ -2151,30 +2354,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 628B951E2670CABD0091AF3B /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 53913C1526D323FE00EB3286 /* Localizable.strings in Resources */, - 53913BF126D323FE00EB3286 /* Localizable.strings in Resources */, - 53913C0626D323FE00EB3286 /* Localizable.strings in Resources */, - 53913BF426D323FE00EB3286 /* Localizable.strings in Resources */, - 53913C0C26D323FE00EB3286 /* Localizable.strings in Resources */, - 53913C0326D323FE00EB3286 /* Localizable.strings in Resources */, - 628B95292670CABE0091AF3B /* Assets.xcassets in Resources */, - 53913BF726D323FE00EB3286 /* Localizable.strings in Resources */, - 53913BFD26D323FE00EB3286 /* Localizable.strings in Resources */, - 53913C0026D323FE00EB3286 /* Localizable.strings in Resources */, - 534D4FF826A7D7CC000A7A48 /* Localizable.strings in Resources */, - 53913C0F26D323FE00EB3286 /* Localizable.strings in Resources */, - 53913C0926D323FE00EB3286 /* Localizable.strings in Resources */, - 53913BFA26D323FE00EB3286 /* Localizable.strings in Resources */, - 53913C1226D323FE00EB3286 /* Localizable.strings in Resources */, - 534D4FF526A7D7CC000A7A48 /* Localizable.strings in Resources */, - 534D4FF226A7D7CC000A7A48 /* Localizable.strings in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -2223,185 +2402,200 @@ files = ( E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */, C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */, - E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */, - E1E5D53E2783B05200692DFE /* CinematicMovieItemView.swift in Sources */, + E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, - 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */, C4B9B91427E1921B0063535C /* LiveTVNativeVideoPlayerView.swift in Sources */, + E18E021E2887492B0022598C /* Divider.swift in Sources */, E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, C4534985279A40C60045F1E2 /* LiveTVVideoPlayerView.swift in Sources */, E1A2C15A279A7D76005EC829 /* BundleExtensions.swift in Sources */, C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */, + E19E550728972B97003CE330 /* BlurHash.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, - E107BB972788104100354E07 /* CinematicCollectionItemView.swift in Sources */, - 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */, E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, + E1A16CA1288A7CFD00EA4679 /* AboutViewCard.swift in Sources */, + E1937A3F288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, 5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, - E1E5D53B2783A80900692DFE /* CinematicItemViewTopRow.swift in Sources */, E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */, + E18E02212887492B0022598C /* MultiSelectorView.swift in Sources */, C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, + E18E02242887492B0022598C /* BlurHashView.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, - E1E5D5462783C28100692DFE /* CinematicItemAboutView.swift in Sources */, E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */, E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */, + E1C926162887565C002A7A66 /* SeriesItemView.swift in Sources */, E1EBCB4A278BE443009FE6E9 /* ItemOverviewCoordinator.swift in Sources */, - 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */, + E19E550928972B97003CE330 /* StringCoding.swift in Sources */, C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */, - 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, + E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */, + E1C9260C2887565C002A7A66 /* MovieItemContentView.swift in Sources */, + E1C9260B2887565C002A7A66 /* MovieItemView.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, + E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */, E1C812CC277AE40A00918266 /* VideoPlayerView.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, - 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */, E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, - 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */, + E18E021A2887492B0022598C /* AppIcon.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */, - E1E5D5422783B33900692DFE /* PortraitItemsRowView.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */, E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, - E14B4142279354770016CBE5 /* LocalizedLookup.swift in Sources */, - E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, - E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */, + E1C9261C288756BD002A7A66 /* PortraitPosterHStack.swift in Sources */, C453497F279A2DA50045F1E2 /* LiveTVPlayerViewController.swift in Sources */, C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */, + E1C9261A288756BD002A7A66 /* PortraitButton.swift in Sources */, E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */, - 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */, + E11895AA289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */, + E1EF473A289A0F610034046B /* TruncatedTextView.swift in Sources */, + E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */, E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */, + E18E02252887492B0022598C /* PlainNavigationLinkButton.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */, + E1C925F528875037002A7A66 /* ItemViewType.swift in Sources */, 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */, 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */, + E19E550B28972B97003CE330 /* FromString.swift in Sources */, 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 5398514526B64DA100101B49 /* SettingsView.swift in Sources */, 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, E193D54B271941D300900D82 /* ServerListView.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */, + E19E551928972B97003CE330 /* Generation.swift in Sources */, 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */, + E19E550F28972B97003CE330 /* ColourProbes.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, + E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */, C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */, + E18E023C288749540022598C /* UIScrollViewExtensions.swift in Sources */, C4534983279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift in Sources */, - E10D87DF278510E400BD264C /* PosterSize.swift in Sources */, + E1C9260F2887565C002A7A66 /* AttributeHStack.swift in Sources */, E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */, - E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */, + E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */, E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */, + E19E551328972B97003CE330 /* ToUIImage.swift in Sources */, E1BDE359278E9ED2004E4022 /* MissingItemsSettingsView.swift in Sources */, E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */, C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */, E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */, - E193D4D927193CAC00900D82 /* PortraitImageStackable.swift in Sources */, - 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, + E19E551128972B97003CE330 /* ToString.swift in Sources */, C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */, E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */, - 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */, - E1047E2127E584AF00CB0D4A /* BlurHashView.swift in Sources */, - 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, + E1C926142887565C002A7A66 /* FocusGuide.swift in Sources */, E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, + E1399475289B1EA900401ABC /* Defaults+Workaround.swift in Sources */, + E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */, E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, + E18E02202887492B0022598C /* AttributeFillView.swift in Sources */, + E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, + E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */, E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */, E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */, - E1A2C15E279A7D9F005EC829 /* AppIcon.swift in Sources */, E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, - 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */, + E18E02222887492B0022598C /* SearchablePickerView.swift in Sources */, E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, + E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */, 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */, - E1047E2427E5880000CB0D4A /* InitialFailureView.swift in Sources */, - E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */, + E18E021B2887492B0022598C /* SearchBarView.swift in Sources */, E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */, 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */, - 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */, E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, + E18E021F2887492B0022598C /* InitialFailureView.swift in Sources */, E1D4BF882719D27100A11E64 /* Bitrates.swift in Sources */, E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */, + E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, - E1E5D5402783B0C000692DFE /* CinematicItemViewTopRowButton.swift in Sources */, - 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */, - 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, C4534981279A3F140045F1E2 /* tvOSLiveTVOverlay.swift in Sources */, - E13F26AF278754E300DF4761 /* CinematicSeriesItemView.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */, E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */, E13DD3D6271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, E193D549271941CC00900D82 /* UserSignInView.swift in Sources */, - 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, + E184C161288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */, E19169CF272514760085832A /* HTTPScheme.swift in Sources */, E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */, E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */, C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */, + E1C926132887565C002A7A66 /* SeriesEpisodesView.swift in Sources */, C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, E13AD7302798C60F00FDCEE8 /* NativePlayerViewController.swift in Sources */, + E18E02232887492B0022598C /* ImageView.swift in Sources */, E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */, - 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */, E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */, E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */, E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */, E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */, - 62666E3627E5027F00EC0ECD /* Defaults+Workaround.swift in Sources */, E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */, E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */, - 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */, 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, + E19E551528972B97003CE330 /* FromUIImage.swift in Sources */, + E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */, E1AA33202782639D00F6439C /* OverlayType.swift in Sources */, C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */, E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, + E1C9260D2887565C002A7A66 /* CinematicScrollView.swift in Sources */, 6264E88D273850380081A12A /* Strings.swift in Sources */, - 536D3D76267BA9BB0004248C /* MainTabViewModel.swift in Sources */, + E19E550D28972B97003CE330 /* EscapeSequences.swift in Sources */, + E1C926102887565C002A7A66 /* PlayButton.swift in Sources */, E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */, - E10C0942278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */, C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */, E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */, - E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */, - E13F26B12787589300DF4761 /* CinematicSeasonItemView.swift in Sources */, + E18E021C2887492B0022598C /* BlurView.swift in Sources */, E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */, - 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */, E10D87E327852FD000BD264C /* EpisodesRowManager.swift in Sources */, 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */, + E19E550528972B97003CE330 /* ColourSpace.swift in Sources */, E193D547271941C500900D82 /* UserListView.swift in Sources */, + E1C926152887565C002A7A66 /* EpisodeCard.swift in Sources */, E1D4BF7F2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */, - E1BDE35B278EA3A3004E4022 /* EpisodesRowCard.swift in Sources */, + E18E021D2887492B0022598C /* AttributeOutlineView.swift in Sources */, E193D53227193F7B00900D82 /* ConnectToServerCoodinator.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, - 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, + E1937A62288F32DB00CB80AA /* Poster.swift in Sources */, C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, + E19E551B28972B97003CE330 /* TupleMaths.swift in Sources */, E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */, - E1A2C160279A7DCA005EC829 /* AboutView.swift in Sources */, + E1A2C160279A7DCA005EC829 /* AboutAppView.swift in Sources */, C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, + E1A16C9D2889AF1E00EA4679 /* AboutView.swift in Sources */, + E11CEB8E28999B4A003E74C7 /* FontExtensions.swift in Sources */, C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */, E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */, 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */, + E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2410,43 +2604,55 @@ buildActionMask = 2147483647; files = ( 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, - E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */, + E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, E1D4BF7E2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */, - 62666E3427E5027F00EC0ECD /* Defaults+Workaround.swift in Sources */, 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */, 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, + E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */, E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, 62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */, - 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */, E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */, + E1C925F428875037002A7A66 /* ItemViewType.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */, 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, - 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, - 53F866442687A45F00DCD1D7 /* PortraitItemButton.swift in Sources */, + E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */, + E168BD11289A4162001A6922 /* HomeContentView.swift in Sources */, + E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */, + E18E0208288749200022598C /* BlurView.swift in Sources */, + E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */, + 53F866442687A45F00DCD1D7 /* PortraitPosterButton.swift in Sources */, + E19E551A28972B97003CE330 /* TupleMaths.swift in Sources */, + E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */, + E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */, + E19E550C28972B97003CE330 /* EscapeSequences.swift in Sources */, C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */, - E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */, 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */, - C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */, + E18E01E8288747230022598C /* SeriesItemContentView.swift in Sources */, + E19E550428972B97003CE330 /* ColourSpace.swift in Sources */, + E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */, E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */, - E176DE6D278E30D2001EFD8D /* EpisodeRowCard.swift in Sources */, + E176DE6D278E30D2001EFD8D /* EpisodeCard.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E10D87E227852FD000BD264C /* EpisodesRowManager.swift in Sources */, - 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, + E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */, + E18E01FA288747580022598C /* AboutAppView.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, + E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */, E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */, - E14B4141279354770016CBE5 /* LocalizedLookup.swift in Sources */, + E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */, E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */, 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */, + E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */, C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */, @@ -2454,7 +2660,6 @@ E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */, E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */, - E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, E19169CE272514760085832A /* HTTPScheme.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, @@ -2463,63 +2668,85 @@ 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */, E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */, + E19E551028972B97003CE330 /* ToString.swift in Sources */, + E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */, 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */, - E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */, C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */, - 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, + E18E01A9288746AF0022598C /* PortraitPosterHStack.swift in Sources */, + E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */, + E18E0204288749200022598C /* Divider.swift in Sources */, + E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */, E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */, E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */, E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */, + E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */, E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */, + E18E01EF288747230022598C /* ListDetailsView.swift in Sources */, E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */, + E1399474289B1EA900401ABC /* Defaults+Workaround.swift in Sources */, C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */, + E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */, E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, 6264E88C273850380081A12A /* Strings.swift in Sources */, C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, + E19E550A28972B97003CE330 /* FromString.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, - E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, + E11CEB8D28999B4A003E74C7 /* FontExtensions.swift in Sources */, E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */, - E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */, + E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */, E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */, + E18E01F0288747230022598C /* AttributeHStack.swift in Sources */, + E19E551228972B97003CE330 /* ToUIImage.swift in Sources */, 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */, + E18E0205288749200022598C /* AppIcon.swift in Sources */, + E168BD10289A4162001A6922 /* HomeView.swift in Sources */, + E18E01AB288746AF0022598C /* PillHStack.swift in Sources */, + E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */, + E18E0207288749200022598C /* AttributeOutlineView.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, - E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */, + E18E01AD288746AF0022598C /* DotHStack.swift in Sources */, + E1937A61288F32DB00CB80AA /* Poster.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, + E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, + E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */, 6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */, + E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, + E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, + E11895AC289383EE0042947B /* NavBarOffsetModifier.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */, + E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */, + E19E550828972B97003CE330 /* StringCoding.swift in Sources */, + E19E551428972B97003CE330 /* FromUIImage.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, + E19E550628972B97003CE330 /* BlurHash.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, 631759CF2879DB6A00A621AD /* PublicUserSignInCellView.swift in Sources */, - E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, - E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, - E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */, + E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, + E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */, 6220D0C626D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift in Sources */, E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, - E1A2C15C279A7D9F005EC829 /* AppIcon.swift in Sources */, E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */, - 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, - E1A2C151279A7008005EC829 /* AboutView.swift in Sources */, - E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */, + E10D87DC2784EC5200BD264C /* SeriesEpisodesView.swift in Sources */, E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */, - E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */, - E10C0941278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */, + E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */, + E18E01E3288747230022598C /* CompactPortraitScrollView.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */, @@ -2527,17 +2754,25 @@ E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */, C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */, E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */, + E18E01EE288747230022598C /* AboutView.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, + E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */, E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */, + E18E0206288749200022598C /* AttributeFillView.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E1A2C154279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */, - E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */, + E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */, C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */, + E184C160288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */, + E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */, + E18E01E9288747230022598C /* SeriesItemView.swift in Sources */, 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */, + E18E023A288749540022598C /* UIScrollViewExtensions.swift in Sources */, E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */, E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, + E1937A3E288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */, E176DE70278E369F001EFD8D /* MissingItemsSettingsView.swift in Sources */, C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */, E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */, @@ -2552,27 +2787,29 @@ E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, + E18E01F1288747230022598C /* PlayButton.swift in Sources */, E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, 62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */, E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */, + E19E551828972B97003CE330 /* Generation.swift in Sources */, + E18E01E6288747230022598C /* CollectionItemView.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */, - 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, + E19E550E28972B97003CE330 /* ColourProbes.swift in Sources */, E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */, 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, - 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */, E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, + E18E01EA288747230022598C /* MovieItemView.swift in Sources */, + E168BD12289A4162001A6922 /* ContinueWatchingCard.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, - 62553429282190A00087FE20 /* PanDirectionGestureRecognizer.swift in Sources */, E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, - E10D87DE278510E400BD264C /* PosterSize.swift in Sources */, E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */, E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, @@ -2580,56 +2817,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 628B951C2670CABD0091AF3B /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */, - E13DD3CB27164BA8009D4DAF /* UIDeviceExtensions.swift in Sources */, - 6264E88A27384A6F0081A12A /* NetworkError.swift in Sources */, - E19169D0272514760085832A /* HTTPScheme.swift in Sources */, - E1E00A37278628A40022235B /* DoubleExtensions.swift in Sources */, - 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */, - 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */, - E1A2C155279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */, - 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */, - E1AD105926D9A543003E4A08 /* LazyView.swift in Sources */, - E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */, - 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */, - 6264E88E273850380081A12A /* Strings.swift in Sources */, - E10D87E0278510E400BD264C /* PosterSize.swift in Sources */, - E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */, - E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */, - E14B4143279354770016CBE5 /* LocalizedLookup.swift in Sources */, - 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */, - E13DD3F72717E87D009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, - E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */, - 6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */, - E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, - E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */, - E10EAA51277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, - 62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */, - 62EC353226766849000E9F2D /* SessionManager.swift in Sources */, - E1A2C159279A7D76005EC829 /* BundleExtensions.swift in Sources */, - 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */, - 5D1603FE278A40DC00D22B99 /* SubtitleSize.swift in Sources */, - E1AA332427829B5200F6439C /* OverlayType.swift in Sources */, - E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */, - E1A2C15D279A7D9F005EC829 /* AppIcon.swift in Sources */, - 62666E3527E5027F00EC0ECD /* Defaults+Workaround.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 6264E889273848760081A12A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 628B951F2670CABD0091AF3B /* Swiftfin Widget */; - targetProxy = 6264E888273848760081A12A /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 534D4FE726A7D7CC000A7A48 /* Localizable.strings */ = { isa = PBXVariantGroup; @@ -3010,57 +3199,6 @@ }; name = Release; }; - 628B952F2670CABE0091AF3B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 70; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = WidgetExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin.widget; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 628B95302670CABE0091AF3B /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 70; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = WidgetExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin.widget; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -3091,15 +3229,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 628B952E2670CABE0091AF3B /* Build configuration list for PBXNativeTarget "Swiftfin Widget" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 628B952F2670CABE0091AF3B /* Debug */, - 628B95302670CABE0091AF3B /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -3111,14 +3240,6 @@ minimumVersion = 0.1.3; }; }; - 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/PGSSoft/ParallaxView"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.0.0; - }; - }; 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duyquang91/ActivityIndicator"; @@ -3260,16 +3381,6 @@ package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; - 536D3D7C267BD5F90004248C /* ActivityIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; - productName = ActivityIndicator; - }; - 536D3D83267BEA550004248C /* ParallaxView */ = { - isa = XCSwiftPackageProductDependency; - package = 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */; - productName = ParallaxView; - }; 53ABFDEC26799D7700886593 /* ActivityIndicator */ = { isa = XCSwiftPackageProductDependency; package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; @@ -3325,11 +3436,6 @@ package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; productName = JellyfinAPI; }; - E10EAA46277BB670000269ED /* JellyfinAPI */ = { - isa = XCSwiftPackageProductDependency; - package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; - productName = JellyfinAPI; - }; E10EAA4C277BB716000269ED /* Sliders */ = { isa = XCSwiftPackageProductDependency; package = E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */; @@ -3360,11 +3466,6 @@ package = E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */; productName = Puppy; }; - E1347DB3279E3C9E00BC6161 /* Puppy */ = { - isa = XCSwiftPackageProductDependency; - package = E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */; - productName = Puppy; - }; E1347DB5279E3CA500BC6161 /* Puppy */ = { isa = XCSwiftPackageProductDependency; package = E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */; @@ -3385,21 +3486,11 @@ package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; productName = CoreStore; }; - E13DD3CE27164E1F009D4DAF /* CoreStore */ = { - isa = XCSwiftPackageProductDependency; - package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; - productName = CoreStore; - }; E13DD3D227168E65009D4DAF /* Defaults */ = { isa = XCSwiftPackageProductDependency; package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; productName = Defaults; }; - E13DD3DC27175CE3009D4DAF /* Defaults */ = { - isa = XCSwiftPackageProductDependency; - package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; - productName = Defaults; - }; E178857C278037FD0094FBCF /* JellyfinAPI */ = { isa = XCSwiftPackageProductDependency; package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; @@ -3425,11 +3516,6 @@ package = E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; - E1D7E5A727892566009D0EF7 /* Nuke */ = { - isa = XCSwiftPackageProductDependency; - package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */; - productName = Nuke; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 5377CBE9263B596A003A4E83 /* Project object */; diff --git a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d09e4d87..82b35677 100644 --- a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -99,15 +99,6 @@ "version" : "0.8.3" } }, - { - "identity" : "parallaxview", - "kind" : "remoteSourceControl", - "location" : "https://github.com/PGSSoft/ParallaxView", - "state" : { - "revision" : "a4165b0edd9c9c923a1d6e3e4c9a807302a1a475", - "version" : "3.1.2" - } - }, { "identity" : "puppy", "kind" : "remoteSourceControl", diff --git a/Swiftfin/App/JellyfinPlayerApp.swift b/Swiftfin/App/JellyfinPlayerApp.swift index 0dfcfae6..24234776 100644 --- a/Swiftfin/App/JellyfinPlayerApp.swift +++ b/Swiftfin/App/JellyfinPlayerApp.swift @@ -24,7 +24,10 @@ struct JellyfinPlayerApp: App { EmptyView() .ignoresSafeArea() .withHostingWindow { window in - window?.rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view()) + window?.rootViewController = PreferenceUIHostingController { + MainCoordinator() + .view() + } } .onAppear { JellyfinPlayerApp.setupAppearance() @@ -63,3 +66,10 @@ extension View { background(HostingWindowFinder(callback: callback)) } } + +extension UINavigationController { + // Remove back button text + override open func viewWillLayoutSubviews() { + navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) + } +} diff --git a/Swiftfin/App/PreferenceUIHosting/PreferenceUIHostingController.swift b/Swiftfin/App/PreferenceUIHosting/PreferenceUIHostingController.swift index 5b89490b..abc2aa4a 100644 --- a/Swiftfin/App/PreferenceUIHosting/PreferenceUIHostingController.swift +++ b/Swiftfin/App/PreferenceUIHosting/PreferenceUIHostingController.swift @@ -12,10 +12,10 @@ import UIKit // MARK: PreferenceUIHostingController class PreferenceUIHostingController: UIHostingController { - init(wrappedView: V) { + init(@ViewBuilder wrappedView: @escaping () -> V) { let box = Box() super.init(rootView: AnyView( - wrappedView + wrappedView() .onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) { box.value?._prefersHomeIndicatorAutoHidden = $0 }.onPreferenceChange(SupportedOrientationsPreferenceKey.self) { diff --git a/Swiftfin/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift b/Swiftfin/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift index dacfa882..5fb925fa 100644 --- a/Swiftfin/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift +++ b/Swiftfin/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift @@ -25,7 +25,7 @@ struct PreferenceUIHostingControllerView: UIViewControllerReprese var wrappedView: () -> Wrapped func makeUIViewController(context: Context) -> PreferenceUIHostingController { - PreferenceUIHostingController(wrappedView: wrappedView()) + PreferenceUIHostingController { wrappedView() } } func updateUIViewController(_ uiViewController: PreferenceUIHostingController, context: Context) {} diff --git a/Swiftfin/Components/DotHStack.swift b/Swiftfin/Components/DotHStack.swift new file mode 100644 index 00000000..703b5802 --- /dev/null +++ b/Swiftfin/Components/DotHStack.swift @@ -0,0 +1,235 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +// TODO: Check for if statements, look at ViewBuilder's buildIf + +struct DotHStack: View { + + private let items: [AnyView] + private let restItems: [AnyView] + private let alignment: HorizontalAlignment + + var body: some View { + HStack { + items.first + + ForEach(0 ..< restItems.count, id: \.self) { i in + + Circle() + .frame(width: 2, height: 2) + + restItems[i] + } + } + } +} + +extension DotHStack { + + init( + _ data: Data, + id: KeyPath = \.self, + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: @escaping (Data.Element) -> Content + ) { + self.alignment = alignment + self.items = data.map { content($0[keyPath: id]).eraseToAnyView() } + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> A + ) { + self.alignment = alignment + self.items = [content().eraseToAnyView()] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B)> + ) { + self.alignment = alignment + let _content = content() + + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C)> + ) { + self.alignment = alignment + let _content = content() + + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C, D)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C, D, E)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F, G)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + _content.value.6.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () + -> TupleView<(A, B, C, D, E, F, G, H)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + _content.value.6.eraseToAnyView(), + _content.value.7.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () + -> TupleView<(A, B, C, D, E, F, G, H, I)> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + _content.value.6.eraseToAnyView(), + _content.value.7.eraseToAnyView(), + _content.value.8.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } + + init< + A: View, + B: View, + C: View, + D: View, + E: View, + F: View, + G: View, + H: View, + I: View, + J: View + >( + alignment: HorizontalAlignment = .leading, + @ViewBuilder content: () + -> TupleView<( + A, + B, + C, + D, + E, + F, + G, + H, + I, + J + )> + ) { + self.alignment = alignment + let _content = content() + self.items = [ + _content.value.0.eraseToAnyView(), + _content.value.1.eraseToAnyView(), + _content.value.2.eraseToAnyView(), + _content.value.3.eraseToAnyView(), + _content.value.4.eraseToAnyView(), + _content.value.5.eraseToAnyView(), + _content.value.6.eraseToAnyView(), + _content.value.7.eraseToAnyView(), + _content.value.8.eraseToAnyView(), + _content.value.9.eraseToAnyView(), + ] + self.restItems = Array(items.dropFirst()) + } +} diff --git a/Swiftfin/Components/EpisodesRowView/EpisodeRowCard.swift b/Swiftfin/Components/EpisodesRowView/EpisodeRowCard.swift deleted file mode 100644 index 97a3ef51..00000000 --- a/Swiftfin/Components/EpisodesRowView/EpisodeRowCard.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct EpisodeRowCard: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - let viewModel: EpisodesRowManager - let episode: BaseItemDto - - var body: some View { - Button { - itemRouter.route(to: \.item, episode) - } label: { - HStack(alignment: .top) { - VStack(alignment: .leading) { - - ImageView( - episode.getBackdropImage(maxWidth: 200), - blurHash: episode.getBackdropImageBlurHash() - ) - .mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10)) - .frame(width: 200, height: 112) - .overlay { - if episode.id == viewModel.item.id { - RoundedRectangle(cornerRadius: 6) - .stroke(Color.jellyfinPurple, lineWidth: 4) - } - } - .padding(.top) - .accessibilityIgnoresInvertColors() - - VStack(alignment: .leading) { - Text(episode.getEpisodeLocator() ?? "S-:E-") - .font(.footnote) - .foregroundColor(.secondary) - Text(episode.name ?? L10n.noTitle) - .font(.body) - .padding(.bottom, 1) - .lineLimit(2) - - if episode.unaired { - Text(episode.airDateLabel ?? L10n.noOverviewAvailable) - .font(.caption) - .foregroundColor(.secondary) - .fontWeight(.light) - .lineLimit(3) - } else { - Text(episode.overview ?? "") - .font(.caption) - .foregroundColor(.secondary) - .fontWeight(.light) - .lineLimit(3) - } - } - - Spacer() - } - .frame(width: 200) - .shadow(radius: 4, y: 2) - } - } - .buttonStyle(PlainButtonStyle()) - } -} diff --git a/Swiftfin/Components/EpisodesRowView/EpisodesRowView.swift b/Swiftfin/Components/EpisodesRowView/EpisodesRowView.swift deleted file mode 100644 index 13dcb0fe..00000000 --- a/Swiftfin/Components/EpisodesRowView/EpisodesRowView.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct EpisodesRowView: View where RowManager: EpisodesRowManager { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @ObservedObject - var viewModel: RowManager - let onlyCurrentSeason: Bool - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - - HStack { - - if onlyCurrentSeason { - if let currentSeason = Array(viewModel.seasonsEpisodes.keys).first(where: { $0.id == viewModel.item.id }) { - Text(currentSeason.name ?? L10n.noTitle) - .accessibility(addTraits: [.isHeader]) - } - } else { - Menu { - ForEach( - viewModel.sortedSeasons, - id: \.self - ) { season in - Button { - viewModel.select(season: season) - } label: { - if season.id == viewModel.selectedSeason?.id { - Label(season.name ?? L10n.season, systemImage: "checkmark") - } else { - Text(season.name ?? L10n.season) - } - } - } - } label: { - HStack(spacing: 5) { - Text(viewModel.selectedSeason?.name ?? L10n.unknown) - .fontWeight(.semibold) - .fixedSize() - Image(systemName: "chevron.down") - } - } - } - - Spacer() - } - .padding() - - ScrollView(.horizontal, showsIndicators: false) { - ScrollViewReader { reader in - HStack(alignment: .top, spacing: 15) { - if viewModel.isLoading { - VStack(alignment: .leading) { - - ZStack { - Color.gray.ignoresSafeArea() - - ProgressView() - } - .mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10)) - .frame(width: 200, height: 112) - - VStack(alignment: .leading) { - Text("S-:E-") - .font(.footnote) - .foregroundColor(.secondary) - Text("--") - .font(.body) - .padding(.bottom, 1) - .lineLimit(2) - } - - Spacer() - } - .frame(width: 200) - .shadow(radius: 4, y: 2) - } else if let selectedSeason = viewModel.selectedSeason { - if let seasonEpisodes = viewModel.seasonsEpisodes[selectedSeason] { - if seasonEpisodes.isEmpty { - VStack(alignment: .leading) { - - Color.gray.ignoresSafeArea() - .mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10)) - .frame(width: 200, height: 112) - - VStack(alignment: .leading) { - Text("--") - .font(.footnote) - .foregroundColor(.secondary) - - L10n.noEpisodesAvailable.text - .font(.body) - .padding(.bottom, 1) - .lineLimit(2) - } - - Spacer() - } - .frame(width: 200) - .shadow(radius: 4, y: 2) - } else { - ForEach(seasonEpisodes, id: \.self) { episode in - EpisodeRowCard(viewModel: viewModel, episode: episode) - .id(episode.id) - } - } - } - } - } - .padding(.horizontal) - .onChange(of: viewModel.selectedSeason) { _ in - if viewModel.selectedSeason?.id == viewModel.item.seasonId { - reader.scrollTo(viewModel.item.id) - } - } - .onChange(of: viewModel.seasonsEpisodes) { _ in - if viewModel.selectedSeason?.id == viewModel.item.seasonId { - reader.scrollTo(viewModel.item.id) - } - } - } - .edgesIgnoringSafeArea(.horizontal) - } - } - } -} diff --git a/Swiftfin/Components/PillHStackView.swift b/Swiftfin/Components/PillHStack.swift similarity index 70% rename from Swiftfin/Components/PillHStackView.swift rename to Swiftfin/Components/PillHStack.swift index 4ae9f8b4..e9bef9d1 100644 --- a/Swiftfin/Components/PillHStackView.swift +++ b/Swiftfin/Components/PillHStack.swift @@ -8,20 +8,22 @@ import SwiftUI -struct PillHStackView: View { +struct PillHStack: View { let title: String - let items: [ItemType] - let selectedAction: (ItemType) -> Void + let items: [Item] + let selectedAction: (Item) -> Void var body: some View { VStack(alignment: .leading) { Text(title) - .font(.callout) + .font(.title2) .fontWeight(.semibold) - .padding(.top, 3) - .padding(.leading, 16) .accessibility(addTraits: [.isHeader]) + .padding(.leading) + .if(UIDevice.isIPad) { view in + view.padding(.leading) + } ScrollView(.horizontal, showsIndicators: false) { HStack { @@ -39,17 +41,16 @@ struct PillHStackView: View { .fontWeight(.semibold) .foregroundColor(.primary) .fixedSize() - .padding(.leading, 10) - .padding(.trailing, 10) - .padding(.top, 10) - .padding(.bottom, 10) + .padding(10) } .fixedSize() } } } - .padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + .padding(.horizontal) + .if(UIDevice.isIPad) { view in + view.padding(.horizontal) + } } } } diff --git a/Swiftfin/Components/PortraitHStackView.swift b/Swiftfin/Components/PortraitHStackView.swift deleted file mode 100644 index 41b64c8c..00000000 --- a/Swiftfin/Components/PortraitHStackView.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct PortraitImageHStackView: View { - - let items: [ItemType] - let maxWidth: CGFloat - let horizontalAlignment: HorizontalAlignment - let textAlignment: TextAlignment - let topBarView: () -> TopBarView - let selectedAction: (ItemType) -> Void - - init( - items: [ItemType], - maxWidth: CGFloat = 110, - horizontalAlignment: HorizontalAlignment = .leading, - textAlignment: TextAlignment = .leading, - topBarView: @escaping () -> TopBarView, - selectedAction: @escaping (ItemType) -> Void - ) { - self.items = items - self.maxWidth = maxWidth - self.horizontalAlignment = horizontalAlignment - self.textAlignment = textAlignment - self.topBarView = topBarView - self.selectedAction = selectedAction - } - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - topBarView() - - ScrollView(.horizontal, showsIndicators: false) { - HStack(alignment: .top, spacing: 15) { - ForEach(items, id: \.self.portraitImageID) { item in - Button { - selectedAction(item) - } label: { - VStack(alignment: horizontalAlignment) { - ImageView( - item.imageURLConstructor(maxWidth: Int(maxWidth)), - blurHash: item.blurHash, - failureView: { - InitialFailureView(item.failureInitials) - } - ) - .portraitPoster(width: maxWidth) - .shadow(radius: 4, y: 2) - .accessibilityIgnoresInvertColors() - - if item.showTitle { - Text(item.title) - .font(.footnote) - .fontWeight(.regular) - .foregroundColor(.primary) - .multilineTextAlignment(textAlignment) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(2) - } - - if let description = item.subtitle { - Text(description) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(.secondary) - .multilineTextAlignment(textAlignment) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(2) - } - } - .frame(width: maxWidth) - } - .padding(.bottom) - } - } - .padding(.horizontal) - } - } - } -} diff --git a/Swiftfin/Components/PortraitItemButton.swift b/Swiftfin/Components/PortraitPosterButton.swift similarity index 74% rename from Swiftfin/Components/PortraitItemButton.swift rename to Swiftfin/Components/PortraitPosterButton.swift index 2e4aeda4..e56215d2 100644 --- a/Swiftfin/Components/PortraitItemButton.swift +++ b/Swiftfin/Components/PortraitPosterButton.swift @@ -6,23 +6,25 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // -import JellyfinAPI import SwiftUI -struct PortraitItemButton: View { +struct PortraitPosterButton: View { - let item: ItemType + @Environment(\.colorScheme) + private var colorScheme + + let item: Item let maxWidth: CGFloat let horizontalAlignment: HorizontalAlignment let textAlignment: TextAlignment - let selectedAction: (ItemType) -> Void + let selectedAction: (Item) -> Void init( - item: ItemType, + item: Item, maxWidth: CGFloat = 110, horizontalAlignment: HorizontalAlignment = .leading, textAlignment: TextAlignment = .leading, - selectedAction: @escaping (ItemType) -> Void + selectedAction: @escaping (Item) -> Void ) { self.item = item self.maxWidth = maxWidth @@ -37,14 +39,12 @@ struct PortraitItemButton: View { } label: { VStack(alignment: horizontalAlignment) { ImageView( - item.imageURLConstructor(maxWidth: Int(maxWidth)), - blurHash: item.blurHash, + item.portraitPosterImageSource(maxWidth: maxWidth), failureView: { - InitialFailureView(item.failureInitials) + InitialFailureView(item.title.initials) } ) .portraitPoster(width: maxWidth) - .shadow(radius: 4, y: 2) .accessibilityIgnoresInvertColors() if item.showTitle { @@ -53,7 +53,6 @@ struct PortraitItemButton: View { .fontWeight(.regular) .foregroundColor(.primary) .multilineTextAlignment(textAlignment) - .fixedSize(horizontal: false, vertical: true) .lineLimit(2) } @@ -63,13 +62,13 @@ struct PortraitItemButton: View { .fontWeight(.medium) .foregroundColor(.secondary) .multilineTextAlignment(textAlignment) - .fixedSize(horizontal: false, vertical: true) .lineLimit(2) } } .frame(width: maxWidth) } - .frame(alignment: .top) - .padding(.bottom) + .if(colorScheme == .light) { view in + view.shadow(radius: 4, y: 2) + } } } diff --git a/Swiftfin/Components/PortraitPosterHStack.swift b/Swiftfin/Components/PortraitPosterHStack.swift new file mode 100644 index 00000000..e7a31763 --- /dev/null +++ b/Swiftfin/Components/PortraitPosterHStack.swift @@ -0,0 +1,84 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct PortraitPosterHStack: View { + + private let title: String + private let items: [Item] + private let itemWidth: CGFloat + private let trailingContent: () -> TrailingContent + private let selectedAction: (Item) -> Void + + init( + title: String, + items: [Item], + itemWidth: CGFloat = 110, + @ViewBuilder trailingContent: @escaping () -> TrailingContent, + selectedAction: @escaping (Item) -> Void + ) { + self.title = title + self.items = items + self.itemWidth = itemWidth + self.trailingContent = trailingContent + self.selectedAction = selectedAction + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(title) + .font(.title2) + .fontWeight(.semibold) + .accessibility(addTraits: [.isHeader]) + .padding(.leading) + .if(UIDevice.isIPad) { view in + view.padding(.leading) + } + + Spacer() + + trailingContent() + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 15) { + ForEach(items, id: \.hashValue) { item in + PortraitPosterButton( + item: item, + maxWidth: itemWidth, + horizontalAlignment: .leading + ) { item in + selectedAction(item) + } + } + } + .padding(.horizontal) + .if(UIDevice.isIPad) { view in + view.padding(.horizontal) + } + } + } + } +} + +extension PortraitPosterHStack where TrailingContent == EmptyView { + init( + title: String, + items: [Item], + itemWidth: CGFloat = 110, + selectedAction: @escaping (Item) -> Void + ) { + self.title = title + self.items = items + self.itemWidth = itemWidth + self.trailingContent = { EmptyView() } + self.selectedAction = selectedAction + } +} diff --git a/Swiftfin/Components/PrimaryButtonView.swift b/Swiftfin/Components/PrimaryButton.swift similarity index 76% rename from Swiftfin/Components/PrimaryButtonView.swift rename to Swiftfin/Components/PrimaryButton.swift index 6fd9b752..fba33235 100644 --- a/Swiftfin/Components/PrimaryButtonView.swift +++ b/Swiftfin/Components/PrimaryButton.swift @@ -8,7 +8,7 @@ import SwiftUI -struct PrimaryButtonView: View { +struct PrimaryButton: View { private let title: String private let action: () -> Void @@ -24,12 +24,10 @@ struct PrimaryButtonView: View { } label: { ZStack { Rectangle() - .foregroundColor(Color(UIColor.systemPurple)) - .frame(maxWidth: 400, maxHeight: 50) + .foregroundColor(Color.jellyfinPurple) + .frame(maxWidth: 400) .frame(height: 50) .cornerRadius(10) - .padding(.horizontal, 30) - .padding([.top, .bottom], 20) Text(title) .foregroundColor(Color.white) diff --git a/Swiftfin/Components/RefreshableScrollView.swift b/Swiftfin/Components/RefreshableScrollView.swift new file mode 100644 index 00000000..12857675 --- /dev/null +++ b/Swiftfin/Components/RefreshableScrollView.swift @@ -0,0 +1,33 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Introspect +import SwiftUI + +struct RefreshableScrollView: View { + + let content: () -> Content + let onRefresh: () -> Void + + private let refreshHelper = RefreshHelper() + + var body: some View { + ScrollView(showsIndicators: false) { + content() + } + .introspectScrollView { scrollView in + let control = UIRefreshControl() + + refreshHelper.refreshControl = control + refreshHelper.refreshAction = onRefresh + + control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged) + scrollView.refreshControl = control + } + } +} diff --git a/Swiftfin/Components/TruncatedTextView.swift b/Swiftfin/Components/TruncatedTextView.swift deleted file mode 100644 index 54195865..00000000 --- a/Swiftfin/Components/TruncatedTextView.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct TruncatedTextView: View { - - @State - private var truncated: Bool = false - @State - private var shrinkText: String - private var text: String - let font: UIFont - let lineLimit: Int - let seeMoreAction: () -> Void - - private var moreLessText: String { - if !truncated { - return "" - } else { - return L10n.seeMore - } - } - - init( - _ text: String, - lineLimit: Int, - font: UIFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body), - seeMoreAction: @escaping () -> Void - ) { - self.text = text - self.lineLimit = lineLimit - _shrinkText = State(wrappedValue: text) - self.font = font - self.seeMoreAction = seeMoreAction - } - - var body: some View { - VStack(alignment: .center) { - Group { - Text(shrinkText) - .overlay { - if truncated { - LinearGradient( - stops: [ - .init(color: .systemBackground.opacity(0), location: 0.5), - .init(color: .systemBackground.opacity(0.8), location: 0.7), - .init(color: .systemBackground, location: 1), - ], - startPoint: .top, - endPoint: .bottom - ) - } - } - } - .lineLimit(lineLimit) - .background { - // Render the limited text and measure its size - Text(text) - .lineLimit(lineLimit + 2) - .background { - GeometryReader { visibleTextGeometry in - Color.clear - .onAppear { - let size = CGSize(width: visibleTextGeometry.size.width, height: .greatestFiniteMagnitude) - let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font] - var low = 0 - var heigh = shrinkText.count - var mid = heigh - while (heigh - low) > 1 { - let attributedText = NSAttributedString(string: shrinkText, attributes: attributes) - let boundingRect = attributedText.boundingRect( - with: size, - options: NSStringDrawingOptions - .usesLineFragmentOrigin, - context: nil - ) - if boundingRect.size.height > visibleTextGeometry.size.height { - truncated = true - heigh = mid - mid = (heigh + low) / 2 - - } else { - if mid == text.count { - break - } else { - low = mid - mid = (low + heigh) / 2 - } - } - shrinkText = String(text.prefix(mid)) - } - - if truncated { - shrinkText = String(shrinkText.prefix(shrinkText.count - 2)) - } - } - } - } - .hidden() - } - .font(Font(font)) - - if truncated { - Button { - seeMoreAction() - } label: { - Text(moreLessText) - } - } - } - } -} diff --git a/Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetModifier.swift b/Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetModifier.swift new file mode 100644 index 00000000..65534c9f --- /dev/null +++ b/Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetModifier.swift @@ -0,0 +1,25 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct NavBarOffsetModifier: ViewModifier { + + @Binding + var scrollViewOffset: CGFloat + + let start: CGFloat + let end: CGFloat + + func body(content: Content) -> some View { + NavBarOffsetView(scrollViewOffset: $scrollViewOffset, start: start, end: end) { + content + } + .ignoresSafeArea() + } +} diff --git a/Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetView.swift b/Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetView.swift new file mode 100644 index 00000000..0f015eda --- /dev/null +++ b/Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetView.swift @@ -0,0 +1,94 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct NavBarOffsetView: UIViewControllerRepresentable { + + @Binding + private var scrollViewOffset: CGFloat + + private let start: CGFloat + private let end: CGFloat + private let content: () -> Content + + init(scrollViewOffset: Binding, start: CGFloat, end: CGFloat, @ViewBuilder content: @escaping () -> Content) { + self._scrollViewOffset = scrollViewOffset + self.start = start + self.end = end + self.content = content + } + + init(start: CGFloat, end: CGFloat, @ViewBuilder body: @escaping () -> Content) { + self._scrollViewOffset = Binding(get: { 0 }, set: { _ in }) + self.start = start + self.end = end + self.content = body + } + + func makeUIViewController(context: Context) -> UINavBarOffsetHostingController { + UINavBarOffsetHostingController(rootView: content()) + } + + func updateUIViewController(_ uiViewController: UINavBarOffsetHostingController, context: Context) { + uiViewController.scrollViewDidScroll(scrollViewOffset, start: start, end: end) + } +} + +class UINavBarOffsetHostingController: UIHostingController { + + private var lastScrollViewOffset: CGFloat = 0 + + private lazy var navBarBlurView: UIVisualEffectView = { + let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial)) + blurView.translatesAutoresizingMaskIntoConstraints = false + return blurView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = nil + + view.addSubview(navBarBlurView) + navBarBlurView.alpha = 0 + + NSLayoutConstraint.activate([ + navBarBlurView.topAnchor.constraint(equalTo: view.topAnchor), + navBarBlurView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navBarBlurView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + navBarBlurView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + } + + func scrollViewDidScroll(_ offset: CGFloat, start: CGFloat, end: CGFloat) { + let diff = end - start + let currentProgress = (offset - start) / diff + let offset = min(max(currentProgress, 0), 1) + + self.navigationController?.navigationBar + .titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(offset)] + navBarBlurView.alpha = offset + lastScrollViewOffset = offset + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.navigationBar + .titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(lastScrollViewOffset)] + self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + self.navigationController?.navigationBar.shadowImage = UIImage() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label] + self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default) + self.navigationController?.navigationBar.shadowImage = nil + } +} diff --git a/Swiftfin/Extensions/iOSViewExtensions/iOSViewExtensions.swift b/Swiftfin/Extensions/iOSViewExtensions/iOSViewExtensions.swift new file mode 100644 index 00000000..3ebdf829 --- /dev/null +++ b/Swiftfin/Extensions/iOSViewExtensions/iOSViewExtensions.swift @@ -0,0 +1,15 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension View { + func navBarOffset(_ scrollViewOffset: Binding, start: CGFloat, end: CGFloat) -> some View { + self.modifier(NavBarOffsetModifier(scrollViewOffset: scrollViewOffset, start: start, end: end)) + } +} diff --git a/Swiftfin/Views/AboutView.swift b/Swiftfin/Views/AboutAppView.swift similarity index 99% rename from Swiftfin/Views/AboutView.swift rename to Swiftfin/Views/AboutAppView.swift index 681a1882..1ed8d379 100644 --- a/Swiftfin/Views/AboutView.swift +++ b/Swiftfin/Views/AboutAppView.swift @@ -8,7 +8,7 @@ import SwiftUI -struct AboutView: View { +struct AboutAppView: View { var body: some View { List { diff --git a/Swiftfin/Views/BasicAppSettingsView.swift b/Swiftfin/Views/BasicAppSettingsView.swift index 124f1ed0..6b9615fb 100644 --- a/Swiftfin/Views/BasicAppSettingsView.swift +++ b/Swiftfin/Views/BasicAppSettingsView.swift @@ -13,7 +13,7 @@ import SwiftUI struct BasicAppSettingsView: View { @EnvironmentObject - var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router + private var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router @ObservedObject var viewModel: BasicAppSettingsViewModel @State diff --git a/Swiftfin/Views/ContinueWatchingView.swift b/Swiftfin/Views/ContinueWatchingView.swift deleted file mode 100644 index 2fcac8e7..00000000 --- a/Swiftfin/Views/ContinueWatchingView.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct ContinueWatchingView: View { - - @EnvironmentObject - var homeRouter: HomeCoordinator.Router - @ObservedObject - var viewModel: HomeViewModel - - var body: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(alignment: .top, spacing: 20) { - ForEach(viewModel.resumeItems, id: \.id) { item in - - Button { - homeRouter.route(to: \.item, item) - } label: { - VStack(alignment: .leading) { - - ZStack { - Group { - if item.itemType == .episode { - ImageView(sources: [ - item.getSeriesThumbImage(maxWidth: 320), - item.getSeriesBackdropImage(maxWidth: 320), - ]) - .frame(width: 320, height: 180) - } else { - ImageView(sources: [ - item.getThumbImage(maxWidth: 320), - item.getBackdropImage(maxWidth: 320), - ]) - .frame(width: 320, height: 180) - } - } - .accessibilityIgnoresInvertColors() - - HStack { - VStack { - - Spacer() - - ZStack(alignment: .bottom) { - - LinearGradient( - colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)], - startPoint: .top, - endPoint: .bottom - ) - .frame(height: 35) - - VStack(alignment: .leading, spacing: 0) { - Text(item.getItemProgressString() ?? L10n.continue) - .font(.subheadline) - .padding(.bottom, 5) - .padding(.leading, 10) - .foregroundColor(.white) - - HStack { - Color.jellyfinPurple - .frame(width: 320 * (item.userData?.playedPercentage ?? 0) / 100, height: 7) - - Spacer(minLength: 0) - } - } - } - } - } - } - .frame(width: 320, height: 180) - .mask(Rectangle().cornerRadius(10)) - .shadow(radius: 4, y: 2) - - VStack(alignment: .leading) { - Text("\(item.seriesName ?? item.name ?? "")") - .font(.callout) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - - if item.itemType == .episode { - Text(item.getEpisodeLocator() ?? "") - .font(.callout) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - } - } - } - .contextMenu { - Button(role: .destructive) { - viewModel.removeItemFromResume(item) - } label: { - Label(L10n.removeFromResume, systemImage: "minus.circle") - } - } - } - } - .padding(.horizontal) - } - } -} diff --git a/Swiftfin/Views/HomeView.swift b/Swiftfin/Views/HomeView.swift deleted file mode 100644 index b1f8f02b..00000000 --- a/Swiftfin/Views/HomeView.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation -import Introspect -import SwiftUI - -struct HomeView: View { - - @EnvironmentObject - var homeRouter: HomeCoordinator.Router - @StateObject - var viewModel = HomeViewModel() - - private let refreshHelper = RefreshHelper() - - @ViewBuilder - var innerBody: some View { - if let errorMessage = viewModel.errorMessage { - VStack(spacing: 5) { - if viewModel.isLoading { - ProgressView() - .frame(width: 100, height: 100) - .scaleEffect(2) - } else { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 72)) - .foregroundColor(Color.red) - .frame(width: 100, height: 100) - } - - Text("\(errorMessage.code)") - Text(errorMessage.message) - .frame(minWidth: 50, maxWidth: 240) - .multilineTextAlignment(.center) - - PrimaryButtonView(title: L10n.retry) { - viewModel.refresh() - } - } - .offset(y: -50) - } else if viewModel.isLoading { - ProgressView() - .frame(width: 100, height: 100) - .scaleEffect(2) - } else { - ScrollView { - VStack(alignment: .leading) { - if !viewModel.resumeItems.isEmpty { - ContinueWatchingView(viewModel: viewModel) - } - - if !viewModel.nextUpItems.isEmpty { - PortraitImageHStackView( - items: viewModel.nextUpItems, - horizontalAlignment: .leading - ) { - L10n.nextUp.text - .font(.title2) - .fontWeight(.bold) - .padding() - .accessibility(addTraits: [.isHeader]) - } selectedAction: { item in - homeRouter.route(to: \.item, item) - } - } - - if !viewModel.latestAddedItems.isEmpty { - PortraitImageHStackView(items: viewModel.latestAddedItems) { - L10n.recentlyAdded.text - .font(.title2) - .fontWeight(.bold) - .padding() - .accessibility(addTraits: [.isHeader]) - } selectedAction: { item in - homeRouter.route(to: \.item, item) - } - } - - ForEach(viewModel.libraries, id: \.self) { library in - - LatestMediaView(viewModel: LatestMediaViewModel(library: library)) { - HStack { - Text(L10n.latestWithString(library.name ?? "")) - .font(.title2) - .fontWeight(.bold) - .accessibility(addTraits: [.isHeader]) - - Spacer() - - Button { - homeRouter - .route(to: \.library, ( - viewModel: .init( - parentID: library.id!, - filters: viewModel.recentFilterSet - ), - title: library.name ?? "" - )) - } label: { - HStack { - L10n.seeAll.text.font(.subheadline).fontWeight(.bold) - Image(systemName: "chevron.right").font(Font.subheadline.bold()) - } - } - } - .padding() - } - } - } - .padding(.bottom, 50) - } - .introspectScrollView { scrollView in - let control = UIRefreshControl() - - refreshHelper.refreshControl = control - refreshHelper.refreshAction = viewModel.refresh - - control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged) - scrollView.refreshControl = control - } - } - } - - var body: some View { - innerBody - .navigationTitle(L10n.home) - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - homeRouter.route(to: \.settings) - } label: { - Image(systemName: "gearshape.fill") - .accessibilityLabel(L10n.settings) - } - } - } - .onAppear { - refreshHelper.refreshStaleData() - } - } -} diff --git a/Swiftfin/Views/HomeView/Components/ContinueWatchingView/ContinueWatchingCard.swift b/Swiftfin/Views/HomeView/Components/ContinueWatchingView/ContinueWatchingCard.swift new file mode 100644 index 00000000..f07d9c85 --- /dev/null +++ b/Swiftfin/Views/HomeView/Components/ContinueWatchingView/ContinueWatchingCard.swift @@ -0,0 +1,97 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct ContinueWatchingLandscapeButton: View { + + @EnvironmentObject + private var homeRouter: HomeCoordinator.Router + + let item: BaseItemDto + + var body: some View { + Button { + homeRouter.route(to: \.item, item) + } label: { + VStack(alignment: .leading) { + + ZStack { + Group { + if item.type == .episode { + ImageView([ + item.seriesImageSource(.thumb, maxWidth: 320), + item.seriesImageSource(.backdrop, maxWidth: 320), + ]) + .frame(width: 320, height: 180) + } else { + ImageView([ + item.imageSource(.thumb, maxWidth: 320), + item.imageSource(.backdrop, maxWidth: 320), + ]) + .frame(width: 320, height: 180) + } + } + .accessibilityIgnoresInvertColors() + + HStack { + VStack { + + Spacer() + + ZStack(alignment: .bottom) { + + LinearGradient( + colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: 35) + + VStack(alignment: .leading, spacing: 0) { + Text(item.getItemProgressString() ?? L10n.continue) + .font(.subheadline) + .padding(.bottom, 5) + .padding(.leading, 10) + .foregroundColor(.white) + + HStack { + Color.jellyfinPurple + .frame(width: 320 * (item.userData?.playedPercentage ?? 0) / 100, height: 7) + + Spacer(minLength: 0) + } + } + } + } + } + } + .frame(width: 320, height: 180) + .mask(Rectangle().cornerRadius(10)) + .shadow(radius: 4, y: 2) + + VStack(alignment: .leading) { + Text("\(item.seriesName ?? item.name ?? "")") + .font(.callout) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + + if item.type == .episode { + Text(item.seasonEpisodeLocator ?? "") + .font(.callout) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + } + } + } +} diff --git a/Swiftfin/Views/HomeView/Components/ContinueWatchingView/ContinueWatchingView.swift b/Swiftfin/Views/HomeView/Components/ContinueWatchingView/ContinueWatchingView.swift new file mode 100644 index 00000000..210a044e --- /dev/null +++ b/Swiftfin/Views/HomeView/Components/ContinueWatchingView/ContinueWatchingView.swift @@ -0,0 +1,37 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import NukeUI +import SwiftUI + +struct ContinueWatchingView: View { + + @EnvironmentObject + private var homeRouter: HomeCoordinator.Router + @ObservedObject + var viewModel: HomeViewModel + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 20) { + ForEach(viewModel.resumeItems, id: \.id) { item in + ContinueWatchingLandscapeButton(item: item) + .contextMenu { + Button(role: .destructive) { + viewModel.removeItemFromResume(item) + } label: { + Label(L10n.removeFromResume, systemImage: "minus.circle") + } + } + } + } + .padding(.horizontal) + } + } +} diff --git a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift new file mode 100644 index 00000000..5556ee10 --- /dev/null +++ b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift @@ -0,0 +1,39 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct LatestInLibraryView: View { + + @EnvironmentObject + private var homeRouter: HomeCoordinator.Router + @ObservedObject + var viewModel: LatestMediaViewModel + + var body: some View { + PortraitPosterHStack( + title: L10n.latestWithString(viewModel.library.displayName), + items: viewModel.items, + itemWidth: UIDevice.isIPad ? 130 : 110 + ) { + Button { + let libraryViewModel = LibraryViewModel(parentID: viewModel.library.id, filters: HomeViewModel.recentFilterSet) + homeRouter.route(to: \.library, (viewModel: libraryViewModel, title: viewModel.library.displayName)) + } label: { + HStack { + L10n.seeAll.text + Image(systemName: "chevron.right") + } + .font(.subheadline.bold()) + } + } selectedAction: { item in + homeRouter.route(to: \.item, item) + } + } +} diff --git a/Swiftfin/Views/HomeView/HomeContentView.swift b/Swiftfin/Views/HomeView/HomeContentView.swift new file mode 100644 index 00000000..d9ed697e --- /dev/null +++ b/Swiftfin/Views/HomeView/HomeContentView.swift @@ -0,0 +1,57 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension HomeView { + + struct ContentView: View { + + @EnvironmentObject + private var homeRouter: HomeCoordinator.Router + @ObservedObject + var viewModel: HomeViewModel + + var body: some View { + RefreshableScrollView { + VStack(alignment: .leading, spacing: 20) { + if !viewModel.resumeItems.isEmpty { + ContinueWatchingView(viewModel: viewModel) + } + + if !viewModel.nextUpItems.isEmpty { + PortraitPosterHStack( + title: L10n.nextUp, + items: viewModel.nextUpItems, + itemWidth: UIDevice.isIPad ? 130 : 110 + ) { item in + homeRouter.route(to: \.item, item) + } + } + + if !viewModel.latestAddedItems.isEmpty { + PortraitPosterHStack( + title: L10n.recentlyAdded, + items: viewModel.latestAddedItems, + itemWidth: UIDevice.isIPad ? 130 : 110 + ) { item in + homeRouter.route(to: \.item, item) + } + } + + ForEach(viewModel.libraries, id: \.self) { library in + LatestInLibraryView(viewModel: .init(library: library)) + } + } + .padding(.bottom, 50) + } onRefresh: { + viewModel.refresh() + } + } + } +} diff --git a/Swiftfin/Views/HomeView/HomeErrorView.swift b/Swiftfin/Views/HomeView/HomeErrorView.swift new file mode 100644 index 00000000..e24f0f35 --- /dev/null +++ b/Swiftfin/Views/HomeView/HomeErrorView.swift @@ -0,0 +1,48 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension HomeView { + + struct ErrorView: View { + + @ObservedObject + var viewModel: HomeViewModel + + let errorMessage: ErrorMessage + + var body: some View { + VStack(spacing: 5) { + if viewModel.isLoading { + ProgressView() + .frame(width: 100, height: 100) + .scaleEffect(2) + } else { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 72)) + .foregroundColor(Color.red) + .frame(width: 100, height: 100) + } + + Text("\(errorMessage.code)") + + Text(errorMessage.message) + .frame(minWidth: 50, maxWidth: 240) + .multilineTextAlignment(.center) + + PrimaryButton(title: L10n.retry) { + viewModel.refresh() + } + .frame(maxWidth: 300) + .frame(height: 50) + } + .offset(y: -50) + } + } +} diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift new file mode 100644 index 00000000..42c6ea94 --- /dev/null +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -0,0 +1,41 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import SwiftUI + +struct HomeView: View { + + @EnvironmentObject + private var homeRouter: HomeCoordinator.Router + @ObservedObject + var viewModel: HomeViewModel + + var body: some View { + Group { + if let errorMessage = viewModel.errorMessage { + ErrorView(viewModel: viewModel, errorMessage: errorMessage) + } else if viewModel.isLoading { + ProgressView() + } else { + ContentView(viewModel: viewModel) + } + } + .navigationTitle(L10n.home) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + homeRouter.route(to: \.settings) + } label: { + Image(systemName: "gearshape.fill") + .accessibilityLabel(L10n.settings) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemOverviewView.swift b/Swiftfin/Views/ItemOverviewView.swift index b4a3d700..29f0f200 100644 --- a/Swiftfin/Views/ItemOverviewView.swift +++ b/Swiftfin/Views/ItemOverviewView.swift @@ -12,13 +12,13 @@ import SwiftUI struct ItemOverviewView: View { @EnvironmentObject - var itemOverviewRouter: ItemOverviewCoordinator.Router + private var itemOverviewRouter: ItemOverviewCoordinator.Router let item: BaseItemDto var body: some View { ScrollView(showsIndicators: false) { Text(item.overview ?? "") - .font(.footnote) + .font(.body) .padding() } .navigationBarTitle(L10n.overview, displayMode: .inline) diff --git a/Swiftfin/Views/ItemView/Components/AboutView.swift b/Swiftfin/Views/ItemView/Components/AboutView.swift new file mode 100644 index 00000000..7fa2192c --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/AboutView.swift @@ -0,0 +1,81 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemView { + + struct AboutView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: ItemViewModel + + var body: some View { + VStack(alignment: .leading) { + L10n.about.text + .font(.title2) + .fontWeight(.bold) + .accessibility(addTraits: [.isHeader]) + .padding(.horizontal) + .if(UIDevice.isIPad) { view in + view.padding(.horizontal) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ImageView( + viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel + .item.imageSource(.primary, maxWidth: 300) + ) + .portraitPoster(width: 130) + .accessibilityIgnoresInvertColors() + + Button { + itemRouter.route(to: \.itemOverview, viewModel.item) + } label: { + ZStack { + + Color.secondarySystemFill + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 10) { + Text(viewModel.item.displayName) + .font(.title2) + .fontWeight(.semibold) + + Spacer() + + if let overview = viewModel.item.overview { + Text(overview) + .lineLimit(4) + .font(.footnote) + .foregroundColor(.secondary) + } else { + L10n.noOverviewAvailable.text + .font(.footnote) + .foregroundColor(.secondary) + } + } + .padding() + } + .frame(width: 330, height: 195) + } + .buttonStyle(PlainButtonStyle()) + } + .padding(.horizontal) + .if(UIDevice.isIPad) { view in + view.padding(.horizontal) + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift new file mode 100644 index 00000000..fbbd3854 --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift @@ -0,0 +1,90 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct ActionButtonHStack: View { + + @ObservedObject + private var viewModel: ItemViewModel + private let equalSpacing: Bool + + init(viewModel: ItemViewModel, equalSpacing: Bool = true) { + self.viewModel = viewModel + self.equalSpacing = equalSpacing + } + + var body: some View { + HStack(alignment: .center, spacing: 15) { + Button { + UIDevice.impact(.light) + viewModel.toggleWatchState() + } label: { + if viewModel.isWatched { + Image(systemName: "checkmark.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle( + .primary, + Color.jellyfinPurple + ) + } else { + Image(systemName: "checkmark.circle") + .foregroundStyle(.white) + } + } + .buttonStyle(PlainButtonStyle()) + .if(equalSpacing) { view in + view.frame(maxWidth: .infinity) + } + + Button { + UIDevice.impact(.light) + viewModel.toggleFavoriteState() + } label: { + if viewModel.isFavorited { + Image(systemName: "heart.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(Color.red) + } else { + Image(systemName: "heart") + .foregroundStyle(.white) + } + } + .buttonStyle(PlainButtonStyle()) + .if(equalSpacing) { view in + view.frame(maxWidth: .infinity) + } + + if viewModel.videoPlayerViewModels.count > 1 { + Menu { + ForEach(viewModel.videoPlayerViewModels, id: \.versionName) { viewModelOption in + Button { + viewModel.selectedVideoPlayerViewModel = viewModelOption + } label: { + if viewModelOption.versionName == viewModel.selectedVideoPlayerViewModel?.versionName { + Label(viewModelOption.versionName ?? L10n.noTitle, systemImage: "checkmark") + } else { + Text(viewModelOption.versionName ?? L10n.noTitle) + } + } + } + } label: { + HStack(spacing: 5) { + Image(systemName: "list.dash") + } + } + .if(equalSpacing) { view in + view.frame(maxWidth: .infinity) + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/AttributeHStack.swift b/Swiftfin/Views/ItemView/Components/AttributeHStack.swift new file mode 100644 index 00000000..a2c76cb0 --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/AttributeHStack.swift @@ -0,0 +1,49 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct AttributesHStack: View { + + @ObservedObject + var viewModel: ItemViewModel + + var body: some View { + HStack { + if let officialRating = viewModel.item.officialRating { + AttributeOutlineView(text: officialRating) + } + + if let selectedPlayerViewModel = viewModel.selectedVideoPlayerViewModel { + if selectedPlayerViewModel.item.isHD ?? false { + AttributeFillView(text: "HD") + } + + if (selectedPlayerViewModel.videoStream.width ?? 0) > 3800 { + AttributeFillView(text: "4K") + } + + if selectedPlayerViewModel.audioStreams.contains(where: { $0.channelLayout == "5.1" }) { + AttributeFillView(text: "5.1") + } + + if selectedPlayerViewModel.audioStreams.contains(where: { $0.channelLayout == "7.1" }) { + AttributeFillView(text: "7.1") + } + + if !selectedPlayerViewModel.subtitleStreams.isEmpty { + AttributeOutlineView(text: "CC") + } + } + } + .foregroundColor(Color(UIColor.darkGray)) + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/EpisodesRowView/EpisodeCard.swift b/Swiftfin/Views/ItemView/Components/EpisodesRowView/EpisodeCard.swift new file mode 100644 index 00000000..b6775c41 --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/EpisodesRowView/EpisodeCard.swift @@ -0,0 +1,68 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct EpisodeCard: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ScaledMetric + private var staticOverviewHeight: CGFloat = 50 + @Environment(\.colorScheme) + private var colorScheme + + let episode: BaseItemDto + + var body: some View { + Button { + if episode != .placeHolder && episode != .noResults { + itemRouter.route(to: \.item, episode) + } + } label: { + VStack(alignment: .leading) { + ImageView(episode.imageSource(.primary, maxWidth: 200)) + .frame(width: 200, height: 112) + .cornerRadius(10) + .accessibilityIgnoresInvertColors() + + VStack(alignment: .leading) { + Text(episode.episodeLocator ?? L10n.unknown) + .font(.footnote) + .foregroundColor(.secondary) + + Text(episode.displayName) + .font(.body) + .padding(.bottom, 1) + .lineLimit(2) + + ZStack(alignment: .topLeading) { + Color.clear + .frame(height: staticOverviewHeight) + + if episode.unaired { + Text(episode.airDateLabel ?? L10n.noOverviewAvailable) + } else { + Text(episode.overview ?? L10n.noOverviewAvailable) + } + } + .font(.caption.weight(.light)) + .foregroundColor(.secondary) + .lineLimit(4) + .multilineTextAlignment(.leading) + } + } + .frame(width: 200) + } + .buttonStyle(PlainButtonStyle()) + .if(colorScheme == .light) { view in + view.shadow(radius: 4, y: 2) + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/EpisodesRowView/SeriesEpisodesView.swift b/Swiftfin/Views/ItemView/Components/EpisodesRowView/SeriesEpisodesView.swift new file mode 100644 index 00000000..aa3ef5fb --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/EpisodesRowView/SeriesEpisodesView.swift @@ -0,0 +1,85 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct SeriesEpisodesView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: RowManager + + @ViewBuilder + private var headerView: some View { + HStack { + Menu { + ForEach(viewModel.sortedSeasons) { season in + Button { + viewModel.select(season: season) + } label: { + if season.id == viewModel.selectedSeason?.id { + Label(season.name ?? L10n.unknown, systemImage: "checkmark") + } else { + Text(season.name ?? L10n.unknown) + } + } + } + } label: { + HStack(spacing: 5) { + Group { + Text(viewModel.selectedSeason?.name ?? L10n.unknown) + .fixedSize() + Image(systemName: "chevron.down") + } + .font(.title3.weight(.semibold)) + } + } + + Spacer() + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + + headerView + .padding(.horizontal) + .if(UIDevice.isIPad) { view in + view.padding(.horizontal) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: 15) { + if viewModel.isLoading { + ForEach(0 ..< 5) { _ in + EpisodeCard(episode: .placeHolder) + .redacted(reason: .placeholder) + } + } else if let selectedSeason = viewModel.selectedSeason { + if let seasonEpisodes = viewModel.seasonsEpisodes[selectedSeason] { + if seasonEpisodes.isEmpty { + EpisodeCard(episode: .noResults) + } else { + ForEach(seasonEpisodes) { episode in + EpisodeCard(episode: episode) + .id(episode.id) + } + } + } + } + } + .padding(.horizontal) + .if(UIDevice.isIPad) { view in + view.padding(.horizontal) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/ListDetailsView.swift b/Swiftfin/Views/ItemView/Components/ListDetailsView.swift new file mode 100644 index 00000000..a0905082 --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/ListDetailsView.swift @@ -0,0 +1,42 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct ListDetailsView: View { + + let title: String + let items: [BaseItemDto.ItemDetail] + + var body: some View { + VStack(alignment: .leading) { + + VStack(alignment: .leading, spacing: 20) { + Text(title) + .font(.title3) + .fontWeight(.bold) + .accessibility(addTraits: [.isHeader]) + + ForEach(items, id: \.self.title) { item in + VStack(alignment: .leading, spacing: 2) { + Text(item.title) + .font(.subheadline) + Text(item.content) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + .font(.subheadline) + .foregroundColor(Color.secondary) + } + .accessibilityElement(children: .combine) + } + } + .padding(.bottom, 20) + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/PlayButton.swift b/Swiftfin/Views/ItemView/Components/PlayButton.swift new file mode 100644 index 00000000..90f801d5 --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/PlayButton.swift @@ -0,0 +1,59 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct PlayButton: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: ItemViewModel + + var body: some View { + Button { + if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { + itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) + } else { + LogManager.log.error("Attempted to play item but no playback information available") + } + } label: { + ZStack { + Rectangle() + .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple) + .cornerRadius(10) + + HStack { + Image(systemName: "play.fill") + .font(.system(size: 20)) + Text(viewModel.playButtonText()) + .font(.callout) + .fontWeight(.semibold) + } + .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) + } + } + .contextMenu { + if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 { + Button { + if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { + selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true) + itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) + } else { + LogManager.log.error("Attempted to play item but no playback information available") + } + } label: { + Label(L10n.playFromBeginning, systemImage: "gobackward") + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 35ab3db0..9eb30e57 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -9,64 +9,44 @@ import Introspect import JellyfinAPI import SwiftUI +import WidgetKit -// Intermediary view for ItemView to set navigation bar settings -struct ItemNavigationView: View { - private let item: BaseItemDto +struct ItemView: View { - init(item: BaseItemDto) { - self.item = item - } - - var body: some View { - ItemView(item: item) - .navigationBarTitle(item.name ?? "", displayMode: .inline) - .introspectNavigationController { navigationController in - let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.clear] - navigationController.navigationBar.titleTextAttributes = textAttributes - } - } -} - -private struct ItemView: View { - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - - @State - private var orientation: UIDeviceOrientation = .unknown - @Environment(\.horizontalSizeClass) - private var hSizeClass - @Environment(\.verticalSizeClass) - private var vSizeClass - - private let viewModel: ItemViewModel - - init(item: BaseItemDto) { - switch item.itemType { - case .movie: - self.viewModel = MovieItemViewModel(item: item) - case .season: - self.viewModel = SeasonItemViewModel(item: item) - case .episode: - self.viewModel = EpisodeItemViewModel(item: item) - case .series: - self.viewModel = SeriesItemViewModel(item: item) - case .boxset, .folder: - self.viewModel = CollectionItemViewModel(item: item) - default: - self.viewModel = ItemViewModel(item: item) - } - } + let item: BaseItemDto var body: some View { Group { - if hSizeClass == .compact && vSizeClass == .regular { - ItemPortraitMainView() - .environmentObject(viewModel) - } else { - ItemLandscapeMainView() - .environmentObject(viewModel) + switch item.type { + case .movie: + if UIDevice.isIPad { + iPadOSMovieItemView(viewModel: .init(item: item)) + } else { + MovieItemView(viewModel: .init(item: item)) + } + case .series: + if UIDevice.isIPad { + iPadOSSeriesItemView(viewModel: .init(item: item)) + } else { + SeriesItemView(viewModel: .init(item: item)) + } + case .episode: + if UIDevice.isIPad { + iPadOSEpisodeItemView(viewModel: .init(item: item)) + } else { + EpisodeItemView(viewModel: .init(item: item)) + } + case .boxSet: + if UIDevice.isIPad { + iPadOSCollectionItemView(viewModel: .init(item: item)) + } else { + CollectionItemView(viewModel: .init(item: item)) + } + default: + Text(L10n.notImplementedYetWithType(item.type ?? "--")) } } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(item.displayName) } } diff --git a/Swiftfin/Views/ItemView/ItemViewBody.swift b/Swiftfin/Views/ItemView/ItemViewBody.swift deleted file mode 100644 index 1fd32492..00000000 --- a/Swiftfin/Views/ItemView/ItemViewBody.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import JellyfinAPI -import SwiftUI - -struct ItemViewBody: View { - - @Environment(\.horizontalSizeClass) - private var hSizeClass - @Environment(\.verticalSizeClass) - private var vSizeClass - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @EnvironmentObject - private var viewModel: ItemViewModel - @Default(.showCastAndCrew) - var showCastAndCrew - - var body: some View { - VStack(alignment: .leading) { - // MARK: Overview - - if let itemOverview = viewModel.item.overview { - if hSizeClass == .compact && vSizeClass == .regular { - TruncatedTextView( - itemOverview, - lineLimit: 5, - font: UIFont.preferredFont(forTextStyle: .footnote) - ) { - itemRouter.route(to: \.itemOverview, viewModel.item) - } - .padding(.horizontal) - .padding(.top) - } else { - Text(itemOverview) - .font(.footnote) - .padding() - } - } else { - L10n.noOverviewAvailable.text - .font(.footnote) - .padding() - } - - // MARK: Seasons - - if let seriesViewModel = viewModel as? SeriesItemViewModel { - PortraitImageHStackView( - items: seriesViewModel.seasons, - topBarView: { - L10n.seasons.text - .fontWeight(.semibold) - .padding() - .accessibility(addTraits: [.isHeader]) - }, - selectedAction: { season in - itemRouter.route(to: \.item, season) - } - ) - } - - // MARK: Genres - - if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStackView( - title: L10n.genres, - items: genres, - selectedAction: { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } - ) - .padding(.bottom) - } - - // MARK: Studios - - if let studios = viewModel.item.studios { - PillHStackView( - title: L10n.studios, - items: studios - ) { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } - .padding(.bottom) - } - - // MARK: Episodes - - if let episodeViewModel = viewModel as? EpisodeItemViewModel { - EpisodesRowView(viewModel: episodeViewModel, onlyCurrentSeason: false) - } else if let seasonViewModel = viewModel as? SeasonItemViewModel { - EpisodesRowView(viewModel: seasonViewModel, onlyCurrentSeason: true) - } - - // MARK: Series - - if let episodeViewModel = viewModel as? EpisodeItemViewModel { - if let seriesItem = episodeViewModel.series { - let a = [seriesItem] - PortraitImageHStackView(items: a) { - L10n.series.text - .fontWeight(.semibold) - .padding(.bottom) - .padding(.horizontal) - } selectedAction: { seriesItem in - itemRouter.route(to: \.item, seriesItem) - } - } - } - - // MARK: Collection Items - - if let collectionViewModel = viewModel as? CollectionItemViewModel { - PortraitImageHStackView(items: collectionViewModel.collectionItems) { - L10n.items.text - .fontWeight(.semibold) - .padding(.bottom) - .padding(.horizontal) - .accessibility(addTraits: [.isHeader]) - } selectedAction: { collectionItem in - itemRouter.route(to: \.item, collectionItem) - } - } - - // MARK: Cast & Crew - - if showCastAndCrew { - if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty { - PortraitImageHStackView( - items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") }, - topBarView: { - L10n.castAndCrew.text - .fontWeight(.semibold) - .padding(.bottom) - .padding(.horizontal) - .accessibility(addTraits: [.isHeader]) - }, - selectedAction: { person in - itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) - } - ) - } - } - - // MARK: Recommended - - if !viewModel.similarItems.isEmpty { - PortraitImageHStackView( - items: viewModel.similarItems, - topBarView: { - L10n.recommended.text - .fontWeight(.semibold) - .padding(.bottom) - .padding(.horizontal) - .accessibility(addTraits: [.isHeader]) - }, - selectedAction: { item in - itemRouter.route(to: \.item, item) - } - ) - } - - // MARK: Details - - switch viewModel.item.itemType { - case .movie, .episode: - ItemViewDetailsView(viewModel: viewModel) - .padding() - default: - EmptyView() - .frame(height: 50) - } - } - } -} diff --git a/Swiftfin/Views/ItemView/ItemViewDetailsView.swift b/Swiftfin/Views/ItemView/ItemViewDetailsView.swift deleted file mode 100644 index 73ee4975..00000000 --- a/Swiftfin/Views/ItemView/ItemViewDetailsView.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct ItemViewDetailsView: View { - - @ObservedObject - var viewModel: ItemViewModel - - var body: some View { - VStack(alignment: .leading) { - - if !viewModel.informationItems.isEmpty { - VStack(alignment: .leading, spacing: 20) { - L10n.information.text - .font(.title3) - .fontWeight(.bold) - .accessibility(addTraits: [.isHeader]) - - ForEach(viewModel.informationItems, id: \.self.title) { informationItem in - VStack(alignment: .leading, spacing: 2) { - Text(informationItem.title) - .font(.subheadline) - Text(informationItem.content) - .font(.subheadline) - .foregroundColor(Color.secondary) - } - .accessibilityElement(children: .combine) - } - } - .padding(.bottom, 20) - } - - VStack(alignment: .leading, spacing: 20) { - L10n.media.text - .font(.title3) - .fontWeight(.bold) - .accessibility(addTraits: [.isHeader]) - - VStack(alignment: .leading, spacing: 2) { - L10n.file.text - .font(.subheadline) - Text(viewModel.selectedVideoPlayerViewModel?.filename ?? "--") - .lineLimit(nil) - .fixedSize(horizontal: false, vertical: true) - .font(.subheadline) - .foregroundColor(Color.secondary) - } - .accessibilityElement(children: .combine) - - VStack(alignment: .leading, spacing: 2) { - L10n.containers.text - .font(.subheadline) - Text(viewModel.selectedVideoPlayerViewModel?.container ?? "--") - .font(.subheadline) - .foregroundColor(Color.secondary) - } - .accessibilityElement(children: .combine) - - ForEach(viewModel.selectedVideoPlayerViewModel?.mediaItems ?? [], id: \.self.title) { mediaItem in - VStack(alignment: .leading, spacing: 2) { - Text(mediaItem.title) - .font(.subheadline) - Text(mediaItem.content) - .lineLimit(nil) - .fixedSize(horizontal: false, vertical: true) - .font(.subheadline) - .foregroundColor(Color.secondary) - } - .accessibilityElement(children: .combine) - } - } - } - } -} diff --git a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift b/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift deleted file mode 100644 index 5865135a..00000000 --- a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Stinsen -import SwiftUI - -struct ItemLandscapeMainView: View { - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @EnvironmentObject - private var viewModel: ItemViewModel - @State - private var playButtonText: String = "" - - // MARK: innerBody - - private var innerBody: some View { - HStack { - // MARK: Sidebar Image - - VStack { - ImageView( - viewModel.item.portraitHeaderViewURL(maxWidth: 130), - blurHash: viewModel.item.getPrimaryImageBlurHash() - ) - .frame(width: 130, height: 195) - .cornerRadius(10) - .accessibilityIgnoresInvertColors() - - Spacer().frame(height: 15) - - // MARK: Play - - Button { - self.itemRouter.route(to: \.videoPlayer, viewModel.selectedVideoPlayerViewModel!) - } label: { - HStack { - Image(systemName: "play.fill") - .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) - .font(.system(size: 20)) - Text(viewModel.playButtonText()) - .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) - .font(.callout) - .fontWeight(.semibold) - } - .frame(width: 130, height: 40) - .background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple) - .cornerRadius(10) - } - .disabled(viewModel.playButtonItem == nil || viewModel.selectedVideoPlayerViewModel == nil) - .contextMenu { - if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 { - Button { - if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { - selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true) - itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) - } else { - LogManager.log.error("Attempted to play item but no playback information available") - } - } label: { - Label(L10n.playFromBeginning, systemImage: "gobackward") - } - } - } - - Spacer() - } - - ScrollView { - VStack(alignment: .leading) { - // MARK: ItemLandscapeTopBarView - - ItemLandscapeTopBarView() - .environmentObject(viewModel) - - // MARK: ItemViewBody - - ItemViewBody() - .environmentObject(viewModel) - } - } - } - .onAppear { - playButtonText = viewModel.playButtonText() - } - } - - // MARK: body - - var body: some View { - VStack { - ZStack { - // MARK: Backdrop - - ImageView( - viewModel.item.getBackdropImage(maxWidth: 200), - blurHash: viewModel.item.getBackdropImageBlurHash() - ) - .opacity(0.3) - .edgesIgnoringSafeArea(.all) - .blur(radius: 8) - .layoutPriority(-1) - .accessibilityIgnoresInvertColors() - - // iPadOS is making the view go all the way to the edge. - // We have to accomodate this here - if UIDevice.current.userInterfaceIdiom == .pad { - innerBody.padding(.horizontal, 25) - } else { - innerBody - } - } - } - } -} diff --git a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift b/Swiftfin/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift deleted file mode 100644 index 79b9e827..00000000 --- a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct ItemLandscapeTopBarView: View { - - @EnvironmentObject - private var viewModel: ItemViewModel - - var body: some View { - HStack { - VStack(alignment: .leading) { - - // MARK: Name - - Text(viewModel.getItemDisplayName()) - .font(.title) - .fontWeight(.semibold) - .foregroundColor(.primary) - .padding(.leading, 16) - .padding(.bottom, 10) - .accessibility(addTraits: [.isHeader]) - - // MARK: Details - - HStack { - - if viewModel.item.unaired { - if let premiereDateLabel = viewModel.item.airDateLabel { - Text(premiereDateLabel) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - } - - if let runtime = viewModel.item.getItemRuntime() { - Text(runtime) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .padding(.leading, 16) - } - - if viewModel.item.productionYear != nil { - Text(String(viewModel.item.productionYear ?? 0)) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - } - - if viewModel.item.officialRating != nil { - Text(viewModel.item.officialRating!) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1) - ) - } - - Spacer() - - if viewModel.item.itemType.showDetails { - // MARK: Favorite - - Button { - viewModel.updateFavoriteState() - } label: { - if viewModel.isFavorited { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) - .font(.system(size: 20)) - } else { - Image(systemName: "heart").foregroundColor(Color.primary) - .font(.system(size: 20)) - } - } - .disabled(viewModel.isLoading) - - // MARK: Watched - - Button { - viewModel.updateWatchState() - } label: { - if viewModel.isWatched { - Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) - .font(.system(size: 20)) - } else { - Image(systemName: "checkmark.circle").foregroundColor(Color.primary) - .font(.system(size: 20)) - } - } - .disabled(viewModel.isLoading) - } - } - .padding(.leading) - - if viewModel.videoPlayerViewModels.count > 1 { - Menu { - ForEach(viewModel.videoPlayerViewModels, id: \.versionName) { viewModelOption in - Button { - viewModel.selectedVideoPlayerViewModel = viewModelOption - } label: { - if viewModelOption.versionName == viewModel.selectedVideoPlayerViewModel?.versionName { - Label(viewModelOption.versionName ?? L10n.noTitle, systemImage: "checkmark") - } else { - Text(viewModelOption.versionName ?? L10n.noTitle) - } - } - } - } label: { - HStack(spacing: 5) { - Text(viewModel.selectedVideoPlayerViewModel?.versionName ?? L10n.noTitle) - .fontWeight(.semibold) - .fixedSize() - Image(systemName: "chevron.down") - } - } - .padding(.leading) - } - } - } - } -} diff --git a/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift b/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift deleted file mode 100644 index 865e6bff..00000000 --- a/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct PortraitHeaderOverlayView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @EnvironmentObject - private var viewModel: ItemViewModel - @State - private var playButtonText: String = "" - - var body: some View { - VStack(alignment: .leading) { - HStack(alignment: .bottom, spacing: 12) { - - // MARK: Portrait Image - - ImageView( - viewModel.item.portraitHeaderViewURL(maxWidth: 130), - blurHash: viewModel.item.getPrimaryImageBlurHash() - ) - .portraitPoster(width: 130) - .accessibilityIgnoresInvertColors() - - VStack(alignment: .leading, spacing: 1) { - Spacer() - - // MARK: Name - - Text(viewModel.getItemDisplayName()) - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .padding(.bottom, 10) - - // MARK: Details - - HStack { - if viewModel.item.unaired { - if let premiereDateLabel = viewModel.item.airDateLabel { - Text(premiereDateLabel) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - } - - if viewModel.shouldDisplayRuntime() { - if let runtime = viewModel.item.getItemRuntime() { - Text(runtime) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - } - - if let productionYear = viewModel.item.productionYear { - Text(String(productionYear)) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - - if let officialRating = viewModel.item.officialRating { - Text(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.videoPlayerViewModels.count > 1 { - Menu { - ForEach(viewModel.videoPlayerViewModels, id: \.versionName) { viewModelOption in - Button { - viewModel.selectedVideoPlayerViewModel = viewModelOption - } label: { - if viewModelOption.versionName == viewModel.selectedVideoPlayerViewModel?.versionName { - Label(viewModelOption.versionName ?? L10n.noTitle, systemImage: "checkmark") - } else { - Text(viewModelOption.versionName ?? L10n.noTitle) - } - } - } - } label: { - HStack(spacing: 5) { - Text(viewModel.selectedVideoPlayerViewModel?.versionName ?? L10n.noTitle) - .fontWeight(.semibold) - .fixedSize() - Image(systemName: "chevron.down") - } - } - } - } - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) - } - - HStack { - - // MARK: Play - - Button { - if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { - itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) - } else { - LogManager.log.error("Attempted to play item but no playback information available") - } - } label: { - HStack { - Image(systemName: "play.fill") - .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) - .font(.system(size: 20)) - Text(playButtonText) - .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) - .font(.callout) - .fontWeight(.semibold) - } - .frame(width: 130, height: 40) - .background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple) - .cornerRadius(10) - } - .disabled(viewModel.playButtonItem == nil || viewModel.selectedVideoPlayerViewModel == nil) - .contextMenu { - if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 { - Button { - if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { - selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true) - itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) - } else { - LogManager.log.error("Attempted to play item but no playback information available") - } - } label: { - Label(L10n.playFromBeginning, systemImage: "gobackward") - } - } - } - - Spacer() - - if viewModel.item.itemType.showDetails { - // MARK: Favorite - - Button { - viewModel.updateFavoriteState() - } label: { - if viewModel.isFavorited { - Image(systemName: "heart.fill") - .foregroundColor(Color(UIColor.systemRed)) - .font(.system(size: 20)) - } else { - Image(systemName: "heart") - .foregroundColor(Color.primary) - .font(.system(size: 20)) - } - } - .disabled(viewModel.isLoading) - - // MARK: Watched - - Button { - viewModel.updateWatchState() - } label: { - if viewModel.isWatched { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(Color.jellyfinPurple) - .font(.system(size: 20)) - } else { - Image(systemName: "checkmark.circle") - .foregroundColor(Color.primary) - .font(.system(size: 20)) - } - } - .disabled(viewModel.isLoading) - } - }.padding(.top, 8) - } - .onAppear { - playButtonText = viewModel.playButtonText() - } - .padding(.horizontal) - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -189 : -64) - } -} diff --git a/Swiftfin/Views/ItemView/Portrait/ItemPortraitMainView.swift b/Swiftfin/Views/ItemView/Portrait/ItemPortraitMainView.swift deleted file mode 100644 index dee7b781..00000000 --- a/Swiftfin/Views/ItemView/Portrait/ItemPortraitMainView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct ItemPortraitMainView: View { - - @EnvironmentObject - var itemRouter: ItemCoordinator.Router - @EnvironmentObject - private var viewModel: ItemViewModel - - // MARK: portraitHeaderView - - var portraitHeaderView: some View { - ImageView( - viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)), - blurHash: viewModel.item.getBackdropImageBlurHash() - ) - .opacity(0.4) - .blur(radius: 2.0) - .accessibilityIgnoresInvertColors() - } - - // MARK: portraitStaticOverlayView - - var portraitStaticOverlayView: some View { - PortraitHeaderOverlayView() - .environmentObject(viewModel) - } - - // MARK: body - - var body: some View { - VStack(alignment: .leading) { - // MARK: ParallaxScrollView - - ParallaxHeaderScrollView( - header: portraitHeaderView, - staticOverlayView: portraitStaticOverlayView, - overlayAlignment: .bottomLeading, - headerHeight: UIScreen.main.bounds.width * 0.5625 - ) { - VStack { - Spacer() - .frame(height: 70) - - ItemViewBody() - .environmentObject(viewModel) - } - } - } - } -} diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift new file mode 100644 index 00000000..4dcbe4f4 --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift @@ -0,0 +1,62 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension CollectionItemView { + + struct ContentView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: CollectionItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + + // MARK: Genres + + if let genres = viewModel.item.genreItems, !genres.isEmpty { + PillHStack( + title: L10n.genres, + items: genres + ) { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + } + + Divider() + } + + // MARK: Studios + + if let studios = viewModel.item.studios { + PillHStack( + title: L10n.studios, + items: studios + ) { studio in + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) + } + + Divider() + } + + // MARK: Items + + if !viewModel.collectionItems.isEmpty { + PortraitPosterHStack( + title: L10n.items, + items: viewModel.collectionItems + ) { item in + itemRouter.route(to: \.item, item) + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift new file mode 100644 index 00000000..add70278 --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift @@ -0,0 +1,35 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +struct CollectionItemView: View { + + @ObservedObject + var viewModel: CollectionItemViewModel + @Default(.itemViewType) + private var itemViewType + + var body: some View { + switch itemViewType { + case .compactPoster: + ItemView.CompactPosterScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + case .compactLogo: + ItemView.CompactLogoScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + case .cinematic: + ItemView.CinematicScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift new file mode 100644 index 00000000..427c42e3 --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift @@ -0,0 +1,149 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension EpisodeItemView { + + struct ContentView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: EpisodeItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + + VStack(alignment: .center) { + ImageView(viewModel.item.imageSource(.primary, maxWidth: 600)) + .frame(maxHeight: 300) + .aspectRatio(1.77, contentMode: .fill) + .cornerRadius(10) + .padding(.horizontal) + + ShelfView(viewModel: viewModel) + } + + // MARK: Overview + + if let itemOverview = viewModel.item.overview { + TruncatedTextView(text: itemOverview) { + itemRouter.route(to: \.itemOverview, viewModel.item) + } + .font(.footnote) + .lineLimit(5) + .padding(.horizontal) + } + + // MARK: Genres + + if let genres = viewModel.item.genreItems, !genres.isEmpty { + PillHStack( + title: L10n.genres, + items: genres, + selectedAction: { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + } + ) + + Divider() + } + + if let studios = viewModel.item.studios, !studios.isEmpty { + PillHStack( + title: L10n.studios, + items: studios + ) { studio in + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) + } + + Divider() + } + + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + !castAndCrew.isEmpty + { + PortraitPosterHStack( + title: L10n.castAndCrew, + items: castAndCrew + ) { person in + itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) + } + + Divider() + } + + // MARK: Details + + if let informationItems = viewModel.item.createInformationItems(), !informationItems.isEmpty { + ListDetailsView(title: L10n.information, items: informationItems) + .padding(.horizontal) + } + } + } + } +} + +extension EpisodeItemView.ContentView { + + struct ShelfView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: EpisodeItemViewModel + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Text(viewModel.item.seriesName ?? "--") + .font(.headline) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + .lineLimit(2) + .padding(.horizontal) + .foregroundColor(.secondary) + + Text(viewModel.item.displayName) + .font(.title2) + .fontWeight(.bold) + .multilineTextAlignment(.center) + .lineLimit(2) + .padding(.horizontal) + + DotHStack { + if let episodeLocation = viewModel.item.episodeLocator { + Text(episodeLocation) + } + + if let productionYear = viewModel.item.premiereDateYear { + Text(productionYear) + } + + if let runtime = viewModel.item.getItemRuntime() { + Text(runtime) + } + } + .font(.caption) + .foregroundColor(.secondary) + .padding(.horizontal) + + ItemView.AttributesHStack(viewModel: viewModel) + + ItemView.PlayButton(viewModel: viewModel) + .frame(maxWidth: 300) + .frame(height: 50) + + ItemView.ActionButtonHStack(viewModel: viewModel) + .font(.title) + .frame(maxWidth: 300) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift new file mode 100644 index 00000000..f6101c9b --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift @@ -0,0 +1,32 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct EpisodeItemView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @State + private var scrollViewOffset: CGFloat = 0 + @ObservedObject + var viewModel: EpisodeItemViewModel + + var body: some View { + ScrollView(showsIndicators: false) { + ContentView(viewModel: viewModel) + } + .scrollViewOffset($scrollViewOffset) + .navBarOffset( + $scrollViewOffset, + start: 0, + end: 10 + ) + } +} diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift new file mode 100644 index 00000000..d7dfb745 --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift @@ -0,0 +1,88 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +extension MovieItemView { + + struct ContentView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: MovieItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + + // MARK: Genres + + if let genres = viewModel.item.genreItems, !genres.isEmpty { + PillHStack( + title: L10n.genres, + items: genres + ) { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + } + + Divider() + } + + // MARK: Studios + + if let studios = viewModel.item.studios, !studios.isEmpty { + PillHStack( + title: L10n.studios, + items: studios + ) { studio in + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) + } + + Divider() + } + + // MARK: Cast and Crew + + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + !castAndCrew.isEmpty + { + PortraitPosterHStack( + title: L10n.castAndCrew, + items: castAndCrew + ) { person in + itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) + } + + Divider() + } + + // MARK: Similar + + if !viewModel.similarItems.isEmpty { + PortraitPosterHStack( + title: L10n.recommended, + items: viewModel.similarItems + ) { item in + itemRouter.route(to: \.item, item) + } + + Divider() + } + + ItemView.AboutView(viewModel: viewModel) + + if let informationItems = viewModel.item.createInformationItems(), !informationItems.isEmpty { + ListDetailsView(title: L10n.information, items: informationItems) + .padding(.horizontal) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift new file mode 100644 index 00000000..3cd60a6b --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift @@ -0,0 +1,36 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +struct MovieItemView: View { + + @ObservedObject + var viewModel: MovieItemViewModel + @Default(.itemViewType) + private var itemViewType + + var body: some View { + switch itemViewType { + case .compactPoster: + ItemView.CompactPosterScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + case .compactLogo: + ItemView.CompactLogoScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + case .cinematic: + ItemView.CinematicScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift new file mode 100644 index 00000000..85dd9336 --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift @@ -0,0 +1,182 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct CinematicScrollView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @State + private var scrollViewOffset: CGFloat = 0 + @State + private var blurHashBottomEdgeColor: Color = .secondarySystemFill + @ObservedObject + var viewModel: ItemViewModel + + let content: () -> Content + + private var topOpacity: CGFloat { + let start = UIScreen.main.bounds.height * 0.5 + let end = UIScreen.main.bounds.height * 0.65 + let diff = end - start + let opacity = min(max((scrollViewOffset - start) / diff, 0), 1) + return opacity + } + + @ViewBuilder + private var headerView: some View { + ImageView(viewModel.item.imageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)) + .frame(height: UIScreen.main.bounds.height * 0.6) + .bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor) + .onAppear { + if let headerBlurHash = viewModel.item.blurHash(.backdrop) { + let bottomRGB = BlurHash(string: headerBlurHash)!.averageLinearRGB + blurHashBottomEdgeColor = Color( + red: Double(bottomRGB.0), + green: Double(bottomRGB.1), + blue: Double(bottomRGB.2) + ) + } + } + } + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + + VStack(spacing: 0) { + Spacer() + + OverlayView(viewModel: viewModel) + .padding(.horizontal) + .padding(.bottom) + .background { + BlurView(style: .systemThinMaterialDark) + .mask { + LinearGradient( + stops: [ + .init(color: .white.opacity(0), location: 0), + .init(color: .white, location: 0.3), + .init(color: .white, location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + } + } + .overlay { + Color.systemBackground + .opacity(topOpacity) + } + } + .frame(height: UIScreen.main.bounds.height * 0.8) + + content() + .padding(.vertical) + .background(Color.systemBackground) + } + } + .edgesIgnoringSafeArea(.top) + .scrollViewOffset($scrollViewOffset) + .navBarOffset( + $scrollViewOffset, + start: UIScreen.main.bounds.height * 0.66, + end: UIScreen.main.bounds.height * 0.66 + 50 + ) + .backgroundParallaxHeader( + $scrollViewOffset, + height: UIScreen.main.bounds.height * 0.6, + multiplier: 0.3 + ) { + headerView + } + } + } +} + +extension ItemView.CinematicScrollView { + + struct OverlayView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: ItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + + VStack(alignment: .center, spacing: 10) { + ImageView( + viewModel.item.imageURL(.logo, maxWidth: UIScreen.main.bounds.width), + resizingMode: .aspectFit + ) { + Text(viewModel.item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + .foregroundColor(.white) + } + .frame(height: 100) + .frame(maxWidth: .infinity) + + DotHStack { + if let firstGenre = viewModel.item.genres?.first { + Text(firstGenre) + } + + if let premiereYear = viewModel.item.premiereDateYear { + Text(premiereYear) + } + + if let playButtonitem = viewModel.playButtonItem, let runtime = playButtonitem.getItemRuntime() { + Text(runtime) + } + } + .font(.caption) + .foregroundColor(Color(UIColor.lightGray)) + .padding(.horizontal) + + ItemView.PlayButton(viewModel: viewModel) + .frame(maxWidth: 300) + .frame(height: 50) + + ItemView.ActionButtonHStack(viewModel: viewModel) + .font(.title) + .frame(maxWidth: 300) + } + .frame(maxWidth: .infinity) + + if let firstTagline = viewModel.item.taglines?.first { + Text(firstTagline) + .font(.body) + .fontWeight(.semibold) + .multilineTextAlignment(.leading) + .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.white) + } + + if let itemOverview = viewModel.item.overview { + TruncatedTextView(text: itemOverview) { + itemRouter.route(to: \.itemOverview, viewModel.item) + } + .font(.footnote) + .lineLimit(4) + .foregroundColor(.white) + .fixedSize(horizontal: false, vertical: true) + } + + ItemView.AttributesHStack(viewModel: viewModel) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift new file mode 100644 index 00000000..e62a003e --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift @@ -0,0 +1,189 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct CompactLogoScrollView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @State + private var scrollViewOffset: CGFloat = 0 + @State + private var blurHashBottomEdgeColor: Color = .secondarySystemFill + @ObservedObject + var viewModel: ItemViewModel + + let content: () -> Content + + private var topOpacity: CGFloat { + let start = UIScreen.main.bounds.height * 0.25 + let end = UIScreen.main.bounds.height * 0.44 + let diff = end - start + let opacity = min(max((scrollViewOffset - start) / diff, 0), 1) + return opacity + } + + @ViewBuilder + private var headerView: some View { + ImageView(viewModel.item.imageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)) + .frame(height: UIScreen.main.bounds.height * 0.35) + .bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor) + .onAppear { + if let backdropBlurHash = viewModel.item.blurHash(.backdrop) { + let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB + blurHashBottomEdgeColor = Color( + red: Double(bottomRGB.0), + green: Double(bottomRGB.1), + blue: Double(bottomRGB.2) + ) + } + } + } + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + + Color.clear + .frame(height: UIScreen.main.bounds.height * 0.25) + + OverlayView(scrollViewOffset: $scrollViewOffset, viewModel: viewModel) + .padding(.horizontal) + .padding(.bottom) + .background { + BlurView(style: .systemThinMaterialDark) + .mask { + LinearGradient( + stops: [ + .init(color: .clear, location: 0), + .init(color: .white, location: 0.15), + ], + startPoint: .top, + endPoint: .bottom + ) + } + } + .overlay { + Color.systemBackground + .opacity(topOpacity) + } + + VStack(alignment: .leading, spacing: 10) { + if let firstTagline = viewModel.item.taglines?.first { + Text(firstTagline) + .font(.body) + .fontWeight(.semibold) + .multilineTextAlignment(.leading) + .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) + } + + if let itemOverview = viewModel.item.overview { + TruncatedTextView(text: itemOverview) { + itemRouter.route(to: \.itemOverview, viewModel.item) + } + .font(.footnote) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) + } + } + .padding(.horizontal) + .padding(.top) + .frame(maxWidth: .infinity) + .background(Color.systemBackground) + + content() + .padding(.vertical) + .background(Color.systemBackground) + } + } + .edgesIgnoringSafeArea(.top) + .scrollViewOffset($scrollViewOffset) + .navBarOffset( + $scrollViewOffset, + start: UIScreen.main.bounds.height * 0.43, + end: UIScreen.main.bounds.height * 0.43 + 50 + ) + .backgroundParallaxHeader( + $scrollViewOffset, + height: UIScreen.main.bounds.height * 0.35, + multiplier: 0.3 + ) { + headerView + } + } + } +} + +extension ItemView.CompactLogoScrollView { + + struct OverlayView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @Binding + var scrollViewOffset: CGFloat + @ObservedObject + var viewModel: ItemViewModel + + private var topOpacity: CGFloat { + let start = UIScreen.main.bounds.height * 0.25 + let end = UIScreen.main.bounds.height * 0.44 + let diff = end - start + let opacity = min(max((scrollViewOffset - start) / diff, 0), 1) + return 1 - opacity + } + + var body: some View { + VStack(alignment: .center, spacing: 10) { + ImageView( + viewModel.item.imageURL(.logo, maxWidth: UIScreen.main.bounds.width), + resizingMode: .aspectFit + ) { + Text(viewModel.item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + .foregroundColor(.white) + } + .frame(height: 100) + .frame(maxWidth: .infinity) + + DotHStack { + if let firstGenre = viewModel.item.genres?.first { + Text(firstGenre) + } + + if let premiereYear = viewModel.item.premiereDateYear { + Text(premiereYear) + } + + if let playButtonitem = viewModel.playButtonItem, let runtime = playButtonitem.getItemRuntime() { + Text(runtime) + } + } + .font(.caption) + .foregroundColor(Color(UIColor.lightGray)) + .padding(.horizontal) + + ItemView.AttributesHStack(viewModel: viewModel) + + ItemView.PlayButton(viewModel: viewModel) + .frame(maxWidth: 300) + .frame(height: 50) + + ItemView.ActionButtonHStack(viewModel: viewModel) + .font(.title) + .frame(maxWidth: 300) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift new file mode 100644 index 00000000..40f55ff9 --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift @@ -0,0 +1,203 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct CompactPosterScrollView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @State + private var scrollViewOffset: CGFloat = 0 + @State + private var blurHashBottomEdgeColor: Color = .secondarySystemFill + @ObservedObject + var viewModel: ItemViewModel + + let content: () -> Content + + private var topOpacity: CGFloat { + let start = UIScreen.main.bounds.height * 0.20 + let end = UIScreen.main.bounds.height * 0.4 + let diff = end - start + let opacity = min(max((scrollViewOffset - start) / diff, 0), 1) + return opacity + } + + @ViewBuilder + private var headerView: some View { + ImageView(viewModel.item.imageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)) + .frame(height: UIScreen.main.bounds.height * 0.35) + .bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor) + } + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + Color.clear + .frame(height: UIScreen.main.bounds.height * 0.15) + + OverlayView(scrollViewOffset: $scrollViewOffset, viewModel: viewModel) + .padding(.horizontal) + .padding(.bottom) + .background { + BlurView(style: .systemThinMaterialDark) + .mask { + LinearGradient( + stops: [ + .init(color: .white.opacity(0), location: 0.2), + .init(color: .white.opacity(0.5), location: 0.3), + .init(color: .white, location: 0.55), + ], + startPoint: .top, + endPoint: .bottom + ) + } + } + .overlay { + Color.systemBackground + .opacity(topOpacity) + } + + VStack(alignment: .leading, spacing: 10) { + if let firstTagline = viewModel.item.taglines?.first { + Text(firstTagline) + .font(.body) + .fontWeight(.semibold) + .multilineTextAlignment(.leading) + .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) + } + + if let itemOverview = viewModel.item.overview { + TruncatedTextView(text: itemOverview) { + itemRouter.route(to: \.itemOverview, viewModel.item) + } + .font(.footnote) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) + } + } + .padding(.horizontal) + .padding(.top) + .frame(maxWidth: .infinity) + .background(Color.systemBackground) + .foregroundColor(.white) + + content() + .padding(.vertical) + .background(Color.systemBackground) + } + } + .edgesIgnoringSafeArea(.top) + .scrollViewOffset($scrollViewOffset) + .navBarOffset( + $scrollViewOffset, + start: UIScreen.main.bounds.height * 0.28, + end: UIScreen.main.bounds.height * 0.28 + 50 + ) + .backgroundParallaxHeader( + $scrollViewOffset, + height: UIScreen.main.bounds.height * 0.35, + multiplier: 0.8 + ) { + headerView + } + .onAppear { + if let backdropBlurHash = viewModel.item.blurHash(.backdrop) { + let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB + blurHashBottomEdgeColor = Color( + red: Double(bottomRGB.0), + green: Double(bottomRGB.1), + blue: Double(bottomRGB.2) + ) + } + } + } + } +} + +extension ItemView.CompactPosterScrollView { + + struct OverlayView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @Binding + var scrollViewOffset: CGFloat + @ObservedObject + var viewModel: ItemViewModel + + @ViewBuilder + private var rightShelfView: some View { + VStack(alignment: .leading) { + + // MARK: Name + + Text(viewModel.item.displayName) + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.white) + + // MARK: Details + + DotHStack { + if viewModel.item.unaired { + if let premiereDateLabel = viewModel.item.airDateLabel { + Text(premiereDateLabel) + } + } else { + if let productionYear = viewModel.item.productionYear { + Text(String(productionYear)) + } + } + + if let playButtonitem = viewModel.playButtonItem, let runtime = playButtonitem.getItemRuntime() { + Text(runtime) + } + } + .lineLimit(1) + .font(.subheadline.weight(.medium)) + .foregroundColor(Color(UIColor.lightGray)) + + ItemView.AttributesHStack(viewModel: viewModel) + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .bottom, spacing: 12) { + + // MARK: Portrait Image + + ImageView(viewModel.item.imageSource(.primary, maxWidth: 130)) + .portraitPoster(width: 130) + .accessibilityIgnoresInvertColors() + + rightShelfView + .padding(.bottom) + } + + // MARK: Play + + HStack(alignment: .center) { + + ItemView.PlayButton(viewModel: viewModel) + .frame(width: 130, height: 40) + + Spacer() + + ItemView.ActionButtonHStack(viewModel: viewModel, equalSpacing: false) + .font(.title2) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift new file mode 100644 index 00000000..042bef0b --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift @@ -0,0 +1,85 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +extension SeriesItemView { + + struct ContentView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: SeriesItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + + // MARK: Episodes + + SeriesEpisodesView(viewModel: viewModel) + + // MARK: Genres + + if let genres = viewModel.item.genreItems, !genres.isEmpty { + PillHStack( + title: L10n.genres, + items: genres + ) { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + } + + Divider() + } + + // MARK: Studios + + if let studios = viewModel.item.studios, !studios.isEmpty { + PillHStack( + title: L10n.studios, + items: studios + ) { studio in + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) + } + + Divider() + } + + // MARK: Cast and Crew + + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty { + PortraitPosterHStack( + title: L10n.castAndCrew, + items: castAndCrew + ) { person in + itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) + } + + Divider() + } + + // MARK: Similar + + if !viewModel.similarItems.isEmpty { + PortraitPosterHStack( + title: L10n.recommended, + items: viewModel.similarItems + ) { item in + itemRouter.route(to: \.item, item) + } + + Divider() + } + + ItemView.AboutView(viewModel: viewModel) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift new file mode 100644 index 00000000..243dbf8c --- /dev/null +++ b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift @@ -0,0 +1,36 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +struct SeriesItemView: View { + + @ObservedObject + var viewModel: SeriesItemViewModel + @Default(.itemViewType) + private var itemViewType + + var body: some View { + switch itemViewType { + case .compactPoster: + ItemView.CompactPosterScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + case .compactLogo: + ItemView.CompactLogoScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + case .cinematic: + ItemView.CinematicScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift new file mode 100644 index 00000000..6d6ccf45 --- /dev/null +++ b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift @@ -0,0 +1,65 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension iPadOSCollectionItemView { + + struct ContentView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: CollectionItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + + // MARK: Genres + + if let genres = viewModel.item.genreItems, !genres.isEmpty { + PillHStack( + title: L10n.genres, + items: genres + ) { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + } + + Divider() + } + + // MARK: Studios + + if let studios = viewModel.item.studios { + PillHStack( + title: L10n.studios, + items: studios + ) { studio in + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) + } + + Divider() + } + + // MARK: Items + + if !viewModel.collectionItems.isEmpty { + PortraitPosterHStack( + title: L10n.items, + items: viewModel.collectionItems, + itemWidth: 130 + ) { item in + itemRouter.route(to: \.item, item) + } + } + + ItemView.AboutView(viewModel: viewModel) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift new file mode 100644 index 00000000..e8cc46fb --- /dev/null +++ b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift @@ -0,0 +1,21 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct iPadOSCollectionItemView: View { + + @ObservedObject + var viewModel: CollectionItemViewModel + + var body: some View { + ItemView.iPadOSCinematicScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + } +} diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift new file mode 100644 index 00000000..67b45904 --- /dev/null +++ b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift @@ -0,0 +1,67 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension iPadOSEpisodeItemView { + + struct ContentView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: EpisodeItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + + // MARK: Genres + + if let genres = viewModel.item.genreItems, !genres.isEmpty { + PillHStack( + title: L10n.genres, + items: genres, + selectedAction: { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + } + ) + + Divider() + } + + if let studios = viewModel.item.studios, !studios.isEmpty { + PillHStack( + title: L10n.studios, + items: studios + ) { studio in + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) + } + + Divider() + } + + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + !castAndCrew.isEmpty + { + PortraitPosterHStack( + title: L10n.castAndCrew, + items: castAndCrew, + itemWidth: 130 + ) { person in + itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) + } + + Divider() + } + + ItemView.AboutView(viewModel: viewModel) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift new file mode 100644 index 00000000..89827837 --- /dev/null +++ b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift @@ -0,0 +1,24 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct iPadOSEpisodeItemView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: EpisodeItemViewModel + + var body: some View { + ItemView.iPadOSCinematicScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } + } +} diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift new file mode 100644 index 00000000..dbcf1ca6 --- /dev/null +++ b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift @@ -0,0 +1,84 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension iPadOSMovieItemView { + + struct ContentView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: MovieItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + + // MARK: Genres + + if let genres = viewModel.item.genreItems, !genres.isEmpty { + PillHStack( + title: L10n.genres, + items: genres + ) { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + } + + Divider() + } + + // MARK: Studios + + if let studios = viewModel.item.studios, !studios.isEmpty { + PillHStack( + title: L10n.studios, + items: studios + ) { studio in + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) + } + + Divider() + } + + // MARK: Cast and Crew + + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + !castAndCrew.isEmpty + { + PortraitPosterHStack( + title: L10n.castAndCrew, + items: castAndCrew, + itemWidth: UIDevice.isIPad ? 130 : 110 + ) { person in + itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) + } + + Divider() + } + + // MARK: Similar + + if !viewModel.similarItems.isEmpty { + PortraitPosterHStack( + title: L10n.recommended, + items: viewModel.similarItems, + itemWidth: UIDevice.isIPad ? 130 : 110 + ) { item in + itemRouter.route(to: \.item, item) + } + + Divider() + } + + ItemView.AboutView(viewModel: viewModel) + } + } + } +} diff --git a/Swiftfin/Components/PortraitItemElement.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift similarity index 59% rename from Swiftfin/Components/PortraitItemElement.swift rename to Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift index c04c5dc0..179eabdf 100644 --- a/Swiftfin/Components/PortraitItemElement.swift +++ b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift @@ -9,11 +9,14 @@ import JellyfinAPI import SwiftUI -// Not implemented on iOS, but used by a shared Coordinator. -struct PortraitItemElement: View { - var item: BaseItemDto +struct iPadOSMovieItemView: View { + + @ObservedObject + var viewModel: MovieItemViewModel var body: some View { - EmptyView() + ItemView.iPadOSCinematicScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } } } diff --git a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift new file mode 100644 index 00000000..56411729 --- /dev/null +++ b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift @@ -0,0 +1,165 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension ItemView { + + struct iPadOSCinematicScrollView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @State + private var scrollViewOffset: CGFloat = 0 + @ObservedObject + var viewModel: ItemViewModel + + let content: () -> Content + + private var topOpacity: CGFloat { + let start = UIScreen.main.bounds.height * 0.45 + let end = UIScreen.main.bounds.height * 0.65 + let diff = end - start + let opacity = min(max((scrollViewOffset - start) / diff, 0), 1) + return opacity + } + + @ViewBuilder + private var headerView: some View { + Group { + if viewModel.item.type == .episode { + ImageView(viewModel.item.imageSource(.primary, maxWidth: 1920)) + } else { + ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 1920)) + } + } + .frame(height: UIScreen.main.bounds.height * 0.8) + } + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + VStack(spacing: 0) { + Spacer() + + OverlayView(viewModel: viewModel) + .padding2(.horizontal) + .padding2(.bottom) + } + .frame(height: UIScreen.main.bounds.height * 0.8) + .background { + BlurView(style: .systemThinMaterialDark) + .mask { + LinearGradient( + stops: [ + .init(color: .clear, location: 0.4), + .init(color: .white, location: 0.8), + ], + startPoint: .top, + endPoint: .bottom + ) + } + } + .overlay { + Color.systemBackground + .opacity(topOpacity) + } + + content() + .padding(.vertical) + .background(Color.systemBackground) + } + } + .edgesIgnoringSafeArea(.top) + .edgesIgnoringSafeArea(.horizontal) + .scrollViewOffset($scrollViewOffset) + .navBarOffset( + $scrollViewOffset, + start: UIScreen.main.bounds.height * 0.65, + end: UIScreen.main.bounds.height * 0.65 + 50 + ) + .backgroundParallaxHeader( + $scrollViewOffset, + height: UIScreen.main.bounds.height * 0.8, + multiplier: 0.3 + ) { + headerView + } + } + } +} + +extension ItemView.iPadOSCinematicScrollView { + + struct OverlayView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: ItemViewModel + + var body: some View { + VStack(alignment: .leading) { + ImageView( + viewModel.item.imageURL(.logo, maxWidth: 500), + resizingMode: .aspectFit + ) { + Text(viewModel.item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + .lineLimit(2) + .multilineTextAlignment(.leading) + .foregroundColor(.white) + } + .frame(maxWidth: UIScreen.main.bounds.width * 0.4, maxHeight: 100) + + HStack(alignment: .bottom) { + + VStack(alignment: .leading) { + + DotHStack { + if let firstGenre = viewModel.item.genres?.first { + Text(firstGenre) + } + + if let premiereYear = viewModel.item.premiereDateYear { + Text(premiereYear) + } + + if let playButtonitem = viewModel.playButtonItem, let runtime = playButtonitem.getItemRuntime() { + Text(runtime) + } + } + .font(.caption) + .foregroundColor(Color(UIColor.lightGray)) + + TruncatedTextView(text: viewModel.item.overview ?? L10n.noOverviewAvailable) { + itemRouter.route(to: \.itemOverview, viewModel.item) + } + .lineLimit(3) + .foregroundColor(.white) + + ItemView.AttributesHStack(viewModel: viewModel) + } + .padding(.trailing, 200) + + Spacer() + + VStack(spacing: 10) { + ItemView.PlayButton(viewModel: viewModel) + .frame(height: 50) + + ItemView.ActionButtonHStack(viewModel: viewModel) + .font(.title) + } + .frame(width: 250) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift new file mode 100644 index 00000000..29071e05 --- /dev/null +++ b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift @@ -0,0 +1,88 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension iPadOSSeriesItemView { + + struct ContentView: View { + + @EnvironmentObject + private var itemRouter: ItemCoordinator.Router + @ObservedObject + var viewModel: SeriesItemViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + + // MARK: Episodes + + SeriesEpisodesView(viewModel: viewModel) + + // MARK: Genres + + if let genres = viewModel.item.genreItems, !genres.isEmpty { + PillHStack( + title: L10n.genres, + items: genres + ) { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + } + + Divider() + } + + // MARK: Studios + + if let studios = viewModel.item.studios, !studios.isEmpty { + PillHStack( + title: L10n.studios, + items: studios + ) { studio in + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) + } + + Divider() + } + + // MARK: Cast and Crew + + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + !castAndCrew.isEmpty + { + PortraitPosterHStack( + title: L10n.castAndCrew, + items: castAndCrew, + itemWidth: 130 + ) { person in + itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) + } + + Divider() + } + + // MARK: Similar + + if !viewModel.similarItems.isEmpty { + PortraitPosterHStack( + title: L10n.recommended, + items: viewModel.similarItems, + itemWidth: 130 + ) { item in + itemRouter.route(to: \.item, item) + } + + Divider() + } + + ItemView.AboutView(viewModel: viewModel) + } + } + } +} diff --git a/Shared/Views/PortraitItemSize.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift similarity index 51% rename from Shared/Views/PortraitItemSize.swift rename to Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift index 4c06f4c7..ebd36872 100644 --- a/Shared/Views/PortraitItemSize.swift +++ b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift @@ -6,13 +6,17 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // +import JellyfinAPI import SwiftUI -extension View { +struct iPadOSSeriesItemView: View { - /// Applies Portrait Poster frame with proper corner radius ratio against the width - func portraitPoster(width: CGFloat) -> some View { - self.frame(width: width, height: width * 1.5) - .cornerRadius((width * 1.5) / 40) + @ObservedObject + var viewModel: SeriesItemViewModel + + var body: some View { + ItemView.iPadOSCinematicScrollView(viewModel: viewModel) { + ContentView(viewModel: viewModel) + } } } diff --git a/Swiftfin/Views/LatestMediaView.swift b/Swiftfin/Views/LatestMediaView.swift deleted file mode 100644 index d76658b4..00000000 --- a/Swiftfin/Views/LatestMediaView.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Stinsen -import SwiftUI - -struct LatestMediaView: View { - - @EnvironmentObject - var homeRouter: HomeCoordinator.Router - @StateObject - var viewModel: LatestMediaViewModel - var topBarView: () -> TopBarView - - var body: some View { - PortraitImageHStackView( - items: viewModel.items, - horizontalAlignment: .leading - ) { - topBarView() - } selectedAction: { item in - homeRouter.route(to: \.item, item) - } - } -} diff --git a/Swiftfin/Views/LibraryFilterView.swift b/Swiftfin/Views/LibraryFilterView.swift index 9ac7c584..06bcb4ec 100644 --- a/Swiftfin/Views/LibraryFilterView.swift +++ b/Swiftfin/Views/LibraryFilterView.swift @@ -13,7 +13,7 @@ import SwiftUI struct LibraryFilterView: View { @EnvironmentObject - var filterRouter: FilterCoordinator.Router + private var filterRouter: FilterCoordinator.Router @Binding var filters: LibraryFilters var parentId: String = "" diff --git a/Swiftfin/Views/LibraryListView.swift b/Swiftfin/Views/LibraryListView.swift index 26f22e89..31b9e12b 100644 --- a/Swiftfin/Views/LibraryListView.swift +++ b/Swiftfin/Views/LibraryListView.swift @@ -6,7 +6,6 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // -import Defaults import Foundation import JellyfinAPI import Stinsen @@ -14,21 +13,10 @@ import SwiftUI struct LibraryListView: View { @EnvironmentObject - var libraryListRouter: LibraryListCoordinator.Router + private var libraryListRouter: LibraryListCoordinator.Router @StateObject var viewModel = LibraryListViewModel() - @Default(.Experimental.liveTVAlphaEnabled) - var liveTVAlphaEnabled - - var supportedCollectionTypes: [BaseItemDto.ItemType] { - if liveTVAlphaEnabled { - return [.movie, .season, .series, .liveTV, .boxset, .unknown] - } else { - return [.movie, .season, .series, .boxset, .unknown] - } - } - var body: some View { ScrollView { LazyVStack { @@ -57,14 +45,9 @@ struct LibraryListView: View { .padding(.bottom, 5) if !viewModel.isLoading { - ForEach(viewModel.libraries.filter { [self] library in - let collectionType = library.collectionType ?? "other" - let itemType = BaseItemDto.ItemType(rawValue: collectionType) ?? .unknown - return self.supportedCollectionTypes.contains(itemType) - }, id: \.id) { library in + ForEach(viewModel.filteredLibraries, id: \.id) { library in Button { - let itemType = BaseItemDto.ItemType(rawValue: library.collectionType ?? "other") ?? .unknown - if itemType == .liveTV { + if library.collectionType == "livetv" { libraryListRouter.route(to: \.liveTV) } else { libraryListRouter.route( @@ -77,7 +60,7 @@ struct LibraryListView: View { } } label: { ZStack { - ImageView(library.getPrimaryImage(maxWidth: 500), blurHash: library.getPrimaryImageBlurHash()) + ImageView(library.imageSource(.primary, maxWidth: 500)) .opacity(0.4) .accessibilityIgnoresInvertColors() HStack { diff --git a/Swiftfin/Views/LibrarySearchView.swift b/Swiftfin/Views/LibrarySearchView.swift index a82f7214..12a94302 100644 --- a/Swiftfin/Views/LibrarySearchView.swift +++ b/Swiftfin/Views/LibrarySearchView.swift @@ -13,7 +13,7 @@ import SwiftUI struct LibrarySearchView: View { @EnvironmentObject - var searchRouter: SearchCoordinator.Router + private var searchRouter: SearchCoordinator.Router @StateObject var viewModel: LibrarySearchViewModel @State @@ -84,7 +84,7 @@ struct LibrarySearchView: View { if !items.isEmpty { LazyVGrid(columns: tracks) { ForEach(items, id: \.id) { item in - PortraitItemButton(item: item) { item in + PortraitPosterButton(item: item) { item in searchRouter.route(to: \.item, item) } } diff --git a/Swiftfin/Views/LibraryView.swift b/Swiftfin/Views/LibraryView.swift index 89337214..0ef898b8 100644 --- a/Swiftfin/Views/LibraryView.swift +++ b/Swiftfin/Views/LibraryView.swift @@ -12,7 +12,7 @@ import SwiftUI struct LibraryView: View { @EnvironmentObject - var libraryRouter: LibraryCoordinator.Router + private var libraryRouter: LibraryCoordinator.Router @StateObject var viewModel: LibraryViewModel var title: String @@ -47,7 +47,7 @@ struct LibraryView: View { VStack { LazyVGrid(columns: tracks) { ForEach(viewModel.items, id: \.id) { item in - PortraitItemButton(item: item) { item in + PortraitPosterButton(item: item) { item in libraryRouter.route(to: \.item, item) } } diff --git a/Swiftfin/Views/LiveTVChannelItemElement.swift b/Swiftfin/Views/LiveTVChannelItemElement.swift index 941d63be..7409486b 100644 --- a/Swiftfin/Views/LiveTVChannelItemElement.swift +++ b/Swiftfin/Views/LiveTVChannelItemElement.swift @@ -61,7 +61,7 @@ struct LiveTVChannelItemElement: View { Spacer() } VStack { - ImageView(channel.getPrimaryImage(maxWidth: 128)) + ImageView(channel.imageURL(.primary, maxWidth: 128)) .aspectRatio(contentMode: .fit) .frame(width: 128, alignment: .center) .padding(.init(top: 8, leading: 0, bottom: 0, trailing: 0)) diff --git a/Swiftfin/Views/LiveTVChannelItemWideElement.swift b/Swiftfin/Views/LiveTVChannelItemWideElement.swift index 4481b361..9b1421f4 100644 --- a/Swiftfin/Views/LiveTVChannelItemWideElement.swift +++ b/Swiftfin/Views/LiveTVChannelItemWideElement.swift @@ -64,7 +64,7 @@ struct LiveTVChannelItemWideElement: View { ZStack { HStack { ZStack(alignment: .center) { - ImageView(channel.getPrimaryImage(maxWidth: 128)) + ImageView(channel.imageURL(.primary, maxWidth: 128)) .aspectRatio(contentMode: .fit) .padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0)) VStack(alignment: .center) { diff --git a/Swiftfin/Views/LiveTVChannelsView.swift b/Swiftfin/Views/LiveTVChannelsView.swift index 96164f65..1a064f07 100644 --- a/Swiftfin/Views/LiveTVChannelsView.swift +++ b/Swiftfin/Views/LiveTVChannelsView.swift @@ -16,7 +16,7 @@ typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String) struct LiveTVChannelsView: View { @EnvironmentObject - var router: LiveTVCoordinator.Router + private var liveTVRouter: LiveTVCoordinator.Router @StateObject var viewModel = LiveTVChannelsViewModel() @State @@ -97,7 +97,7 @@ struct LiveTVChannelsView: View { onSelect: { loadingAction in loadingAction(true) self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in - self.router.route(to: \.videoPlayer, playerViewModel) + self.liveTVRouter.route(to: \.videoPlayer, playerViewModel) DispatchQueue.main.asyncAfter(deadline: .now() + 2) { loadingAction(false) } diff --git a/Swiftfin/Views/LiveTVProgramsView.swift b/Swiftfin/Views/LiveTVProgramsView.swift index 3cdea18b..27cd65b6 100644 --- a/Swiftfin/Views/LiveTVProgramsView.swift +++ b/Swiftfin/Views/LiveTVProgramsView.swift @@ -11,7 +11,7 @@ import SwiftUI struct LiveTVProgramsView: View { @EnvironmentObject - var programsRouter: LiveTVProgramsCoordinator.Router + private var programsRouter: LiveTVProgramsCoordinator.Router @StateObject var viewModel = LiveTVProgramsViewModel() @@ -21,15 +21,11 @@ struct LiveTVProgramsView: View { if !viewModel.recommendedItems.isEmpty, let items = viewModel.recommendedItems { - PortraitImageHStackView( - items: items, - horizontalAlignment: .leading - ) { - Text("On Now") - .font(.headline) - .fontWeight(.semibold) - .padding(.leading, 90) - } selectedAction: { item in + + PortraitPosterHStack( + title: "On Now", + items: items + ) { item in if let chanId = item.channelId, let chan = viewModel.findChannel(id: chanId) { @@ -42,15 +38,10 @@ struct LiveTVProgramsView: View { if !viewModel.seriesItems.isEmpty, let items = viewModel.seriesItems { - PortraitImageHStackView( - items: items, - horizontalAlignment: .leading - ) { - Text("Shows") - .font(.headline) - .fontWeight(.semibold) - .padding(.leading, 90) - } selectedAction: { item in + PortraitPosterHStack( + title: "Shows", + items: items + ) { item in if let chanId = item.channelId, let chan = viewModel.findChannel(id: chanId) { @@ -63,15 +54,10 @@ struct LiveTVProgramsView: View { if !viewModel.movieItems.isEmpty, let items = viewModel.movieItems { - PortraitImageHStackView( - items: items, - horizontalAlignment: .leading - ) { - Text("Movies") - .font(.headline) - .fontWeight(.semibold) - .padding(.leading, 90) - } selectedAction: { item in + PortraitPosterHStack( + title: "Movies", + items: items + ) { item in if let chanId = item.channelId, let chan = viewModel.findChannel(id: chanId) { @@ -84,15 +70,10 @@ struct LiveTVProgramsView: View { if !viewModel.sportsItems.isEmpty, let items = viewModel.sportsItems { - PortraitImageHStackView( - items: items, - horizontalAlignment: .leading - ) { - Text("Sports") - .font(.headline) - .fontWeight(.semibold) - .padding(.leading, 90) - } selectedAction: { item in + PortraitPosterHStack( + title: "Sports", + items: items + ) { item in if let chanId = item.channelId, let chan = viewModel.findChannel(id: chanId) { @@ -105,15 +86,10 @@ struct LiveTVProgramsView: View { if !viewModel.kidsItems.isEmpty, let items = viewModel.kidsItems { - PortraitImageHStackView( - items: items, - horizontalAlignment: .leading - ) { - Text("Kids") - .font(.headline) - .fontWeight(.semibold) - .padding(.leading, 90) - } selectedAction: { item in + PortraitPosterHStack( + title: "Kids", + items: items + ) { item in if let chanId = item.channelId, let chan = viewModel.findChannel(id: chanId) { @@ -126,15 +102,10 @@ struct LiveTVProgramsView: View { if !viewModel.newsItems.isEmpty, let items = viewModel.newsItems { - PortraitImageHStackView( - items: items, - horizontalAlignment: .leading - ) { - Text("News") - .font(.headline) - .fontWeight(.semibold) - .padding(.leading, 90) - } selectedAction: { item in + PortraitPosterHStack( + title: "News", + items: items + ) { item in if let chanId = item.channelId, let chan = viewModel.findChannel(id: chanId) { diff --git a/Swiftfin/Views/ServerListView.swift b/Swiftfin/Views/ServerListView.swift index 8f110cd5..0ba8526f 100644 --- a/Swiftfin/Views/ServerListView.swift +++ b/Swiftfin/Views/ServerListView.swift @@ -12,7 +12,7 @@ import SwiftUI struct ServerListView: View { @EnvironmentObject - var serverListRouter: ServerListCoordinator.Router + private var serverListRouter: ServerListCoordinator.Router @ObservedObject var viewModel: ServerListViewModel @@ -69,9 +69,11 @@ struct ServerListView: View { .frame(minWidth: 50, maxWidth: 240) .multilineTextAlignment(.center) - PrimaryButtonView(title: L10n.connect.stringValue) { + PrimaryButton(title: L10n.connect.stringValue) { serverListRouter.route(to: \.connectToServer) } + .frame(maxWidth: 300) + .frame(height: 50) } } diff --git a/Swiftfin/Views/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView.swift index 5f9696d2..e07da362 100644 --- a/Swiftfin/Views/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView.swift @@ -14,20 +14,10 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject - var settingsRouter: SettingsCoordinator.Router + private var settingsRouter: SettingsCoordinator.Router @ObservedObject var viewModel: SettingsViewModel - @Default(.inNetworkBandwidth) - var inNetworkStreamBitrate - @Default(.outOfNetworkBandwidth) - var outOfNetworkStreamBitrate - @Default(.isAutoSelectSubtitles) - var isAutoSelectSubtitles - @Default(.autoSelectSubtitlesLangCode) - var autoSelectSubtitlesLangcode - @Default(.autoSelectAudioLangCode) - var autoSelectAudioLangcode @Default(.appAppearance) var appAppearance @Default(.overlayType) @@ -48,6 +38,8 @@ struct SettingsView: View { var resumeOffset @Default(.subtitleSize) var subtitleSize + @Default(.itemViewType) + var itemViewType @Default(.subtitleFontName) var subtitleFontName @@ -96,21 +88,6 @@ struct SettingsView: View { } } - // TODO: Implement these for playback - // Section(header: Text("Networking")) { - // Picker("Default local quality", selection: $inNetworkStreamBitrate) { - // ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - // Text(bitrate.name).tag(bitrate.value) - // } - // } -// - // Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { - // ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - // Text(bitrate.name).tag(bitrate.value) - // } - // } - // } - Section(header: L10n.videoPlayer.text) { Picker(L10n.jumpForwardLength, selection: $jumpForwardLength) { ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in @@ -171,6 +148,13 @@ struct SettingsView: View { } } + // Not localized yet. Will be in a settings re-organization + Picker("Item View", selection: $itemViewType) { + ForEach(ItemViewType.allCases, id: \.self) { itemViewType in + Text(itemViewType.label).tag(itemViewType.rawValue) + } + } + Button { settingsRouter.route(to: \.missingSettings) } label: { diff --git a/Swiftfin/Views/UserListView.swift b/Swiftfin/Views/UserListView.swift index 1f585c73..b598f4a1 100644 --- a/Swiftfin/Views/UserListView.swift +++ b/Swiftfin/Views/UserListView.swift @@ -11,7 +11,7 @@ import SwiftUI struct UserListView: View { @EnvironmentObject - var userListRouter: UserListCoordinator.Router + private var userListRouter: UserListCoordinator.Router @ObservedObject var viewModel: UserListViewModel @@ -59,23 +59,11 @@ struct UserListView: View { .frame(minWidth: 50, maxWidth: 240) .multilineTextAlignment(.center) - Button { + PrimaryButton(title: L10n.signIn) { userListRouter.route(to: \.userSignIn, viewModel.server) - } label: { - ZStack { - Rectangle() - .foregroundColor(Color.jellyfinPurple) - .frame(maxWidth: 400, maxHeight: 50) - .frame(height: 50) - .cornerRadius(10) - .padding(.horizontal, 30) - .padding([.top, .bottom], 20) - - L10n.signIn.text - .foregroundColor(Color.white) - .bold() - } } + .frame(maxWidth: 300) + .frame(height: 50) } } diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index ffcc0be4..ac7cc6f6 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -487,6 +487,7 @@ struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { hlsStreamURL: URL(string: "www.apple.com")!, streamType: .direct, response: PlaybackInfoResponse(), + videoStream: MediaStream(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], subtitleStreams: [MediaStream(displayTitle: "None", index: -1)], chapters: [], diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 75d7b202701e7c5d3a85d89da9de261ba9a9a9b8..c475e43bd1ffbaf472c4ea551fd4d9d3621fb4ce 100644 GIT binary patch delta 182 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
FDEE+s0w@VD={T__pF|9B3xmK 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 z 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

X8v$yd;i=Wik{BVevnpNFdCL_)Ld-$VbsD_} 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

<-w`Z4Zhl)C5`1R6=EURgEB2pV@%DerfWM#T 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 diff --git a/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/Contents.json b/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/Contents.json deleted file mode 100644 index d7aaf31c..00000000 --- a/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "1024.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WidgetExtension/Info.plist b/WidgetExtension/Info.plist deleted file mode 100644 index a98e4afd..00000000 --- a/WidgetExtension/Info.plist +++ /dev/null @@ -1,29 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - $(PRODUCT_NAME) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSExtension - - NSExtensionPointIdentifier - com.apple.widgetkit-extension - - - diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift deleted file mode 100644 index dee25aeb..00000000 --- a/WidgetExtension/NextUpWidget.swift +++ /dev/null @@ -1,542 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Combine -import JellyfinAPI -import Nuke -import SwiftUI -import WidgetKit - -enum WidgetError: String, Error { - case unknown - case emptyServer - case emptyUser - case emptyHeader -} - -struct NextUpWidgetProvider: TimelineProvider { - func placeholder(in context: Context) -> NextUpEntry { - NextUpEntry(date: Date(), items: [], error: nil) - } - - func getSnapshot(in context: Context, completion: @escaping (NextUpEntry) -> Void) { - guard let currentLogin = SessionManager.main.currentLogin else { return } - - let currentDate = Date() - let server = currentLogin.server - let savedUser = currentLogin.user - var tempCancellables = Set() - - JellyfinAPIAPI.basePath = server.currentURI - TvShowsAPI.getNextUp( - userId: savedUser.id, - limit: 3, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - imageTypeLimit: 1, - enableImageTypes: [.primary, .backdrop, .thumb] - ) - .subscribe(on: DispatchQueue.global(qos: .background)) - .sink(receiveCompletion: { result in - switch result { - case .finished: - break - case let .failure(error): - completion(NextUpEntry(date: currentDate, items: [], error: error)) - } - }, receiveValue: { response in - let dispatchGroup = DispatchGroup() - let items = response.items ?? [] - var downloadedItems = [(BaseItemDto, UIImage?)]() - items.enumerated().forEach { _, item in - dispatchGroup.enter() - ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in - guard case let .success(image) = result else { - dispatchGroup.leave() - return - } - downloadedItems.append((item, image.image)) - dispatchGroup.leave() - } - } - - dispatchGroup.notify(queue: .main) { - completion(NextUpEntry(date: currentDate, items: downloadedItems, error: nil)) - } - }) - .store(in: &tempCancellables) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - guard let currentLogin = SessionManager.main.currentLogin else { return } - - let currentDate = Date() - let entryDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)! - let server = currentLogin.server - let savedUser = currentLogin.user - - var tempCancellables = Set() - - JellyfinAPIAPI.basePath = server.currentURI - TvShowsAPI.getNextUp( - userId: savedUser.id, - limit: 3, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - imageTypeLimit: 1, - enableImageTypes: [.primary, .backdrop, .thumb] - ) - .subscribe(on: DispatchQueue.global(qos: .background)) - .sink(receiveCompletion: { result in - switch result { - case .finished: - break - case let .failure(error): - completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: error)], policy: .after(entryDate))) - } - }, receiveValue: { response in - let dispatchGroup = DispatchGroup() - let items = response.items ?? [] - var downloadedItems = [(BaseItemDto, UIImage?)]() - items.enumerated().forEach { _, item in - dispatchGroup.enter() - ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in - guard case let .success(image) = result else { - dispatchGroup.leave() - return - } - downloadedItems.append((item, image.image)) - dispatchGroup.leave() - } - } - - dispatchGroup.notify(queue: .main) { - completion(Timeline( - entries: [NextUpEntry(date: currentDate, items: downloadedItems, error: nil)], - policy: .after(entryDate) - )) - } - }) - .store(in: &tempCancellables) - } -} - -struct NextUpEntry: TimelineEntry { - let date: Date - let items: [(BaseItemDto, UIImage?)] - let error: Error? -} - -struct NextUpEntryView: View { - var entry: NextUpWidgetProvider.Entry - - @Environment(\.widgetFamily) - var family - - @ViewBuilder - var body: some View { - Group { - if let error = entry.error { - HStack { - Image(systemName: "exclamationmark.octagon") - Text((error as? WidgetError)?.rawValue ?? "") - } - .background(Color.blue) - } else if entry.items.isEmpty { - L10n.emptyNextUp.text - .font(.body) - .bold() - .foregroundColor(.primary) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - } else { - switch family { - case .systemSmall: - small(item: entry.items.first) - case .systemMedium: - medium(items: entry.items) - case .systemLarge: - large(items: entry.items) - default: - EmptyView() - } - } - } - .background(Color(.secondarySystemBackground)) - } -} - -extension NextUpEntryView { - var smallVideoPlaceholderView: some View { - VStack(alignment: .leading) { - Color(.systemGray) - .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) - .cornerRadius(8) - .shadow(radius: 8) - Color(.systemGray2) - .frame(width: 100, height: 10) - Color(.systemGray3) - .frame(width: 80, height: 10) - } - } - - var largeVideoPlaceholderView: some View { - HStack(spacing: 20) { - Color(.systemGray) - .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) - .cornerRadius(8) - .shadow(radius: 8) - VStack(alignment: .leading, spacing: 8) { - Color(.systemGray2) - .frame(width: 100, height: 10) - Color(.systemGray3) - .frame(width: 80, height: 10) - } - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - } - } -} - -extension NextUpEntryView { - var headerSymbol: some View { - Image("WidgetHeaderSymbol") - .resizable() - .frame(width: 12, height: 12) - .cornerRadius(4) - .shadow(radius: 8) - } - - func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View { - let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(item.0.id!)")! - return Link(destination: url, label: { - VStack(alignment: .leading) { - if let image = item.1 { - Image(uiImage: image) - .resizable() - .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) - .clipped() - .cornerRadius(8) - .shadow(radius: 8) - } - Text(item.0.seriesName ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Text( - "\(item.0.name ?? "") · \(L10n.seasonAndEpisode(String(item.0.parentIndexNumber ?? 0), String(item.0.indexNumber ?? 0)))" - ) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - } - }) - } - - func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View { - let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(item.0.id!)")! - return Link(destination: url, label: { - HStack(spacing: 20) { - if let image = item.1 { - Image(uiImage: image) - .resizable() - .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) - .clipped() - .cornerRadius(8) - .shadow(radius: 8) - } - VStack(alignment: .leading, spacing: 8) { - Text(item.0.seriesName ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - - Text( - "\(item.0.name ?? "") · \(L10n.seasonAndEpisode(String(item.0.parentIndexNumber ?? 0), String(item.0.indexNumber ?? 0)))" - ) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - } - } - }) - } -} - -extension NextUpEntryView { - func small(item: (BaseItemDto, UIImage?)?) -> some View { - VStack(alignment: .trailing) { - headerSymbol - if let item = item { - smallVideoView(item: item) - } else { - smallVideoPlaceholderView - } - } - .padding(12) - } - - func medium(items: [(BaseItemDto, UIImage?)]) -> some View { - VStack(alignment: .trailing) { - headerSymbol - HStack(spacing: 16) { - if let firstItem = items[safe: 0] { - smallVideoView(item: firstItem) - } else { - smallVideoPlaceholderView - } - if let secondItem = items[safe: 1] { - smallVideoView(item: secondItem) - } else { - smallVideoPlaceholderView - } - } - } - .padding(12) - } - - func large(items: [(BaseItemDto, UIImage?)]) -> some View { - VStack(spacing: 0) { - if let firstItem = items[safe: 0] { - let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(firstItem.0.id!)")! - Link( - destination: url, - label: { - ZStack(alignment: .topTrailing) { - ZStack(alignment: .bottomLeading) { - if let image = firstItem.1 { - Image(uiImage: image) - .centerCropped() - .innerShadow(color: Color.black.opacity(0.5), radius: 0.5) - } - VStack(alignment: .leading, spacing: 8) { - Text(firstItem.0.seriesName ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.white) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - Text( - "\(firstItem.0.name ?? "") · \(L10n.seasonAndEpisode(String(firstItem.0.parentIndexNumber ?? 0), String(firstItem.0.indexNumber ?? 0)))" - ) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.gray) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - } - .shadow(radius: 8) - .padding(12) - } - headerSymbol - .padding(12) - } - .clipped() - .shadow(radius: 8) - } - ) - } - VStack(spacing: 8) { - if let secondItem = items[safe: 1] { - largeVideoView(item: secondItem) - } else { - largeVideoPlaceholderView - } - Divider() - if let thirdItem = items[safe: 2] { - largeVideoView(item: thirdItem) - } else { - largeVideoPlaceholderView - } - } - .padding(12) - } - } -} - -struct NextUpWidget: Widget { - let kind: String = "NextUpWidget" - - var body: some WidgetConfiguration { - StaticConfiguration( - kind: kind, - provider: NextUpWidgetProvider() - ) { entry in - NextUpEntryView(entry: entry) - } - .configurationDisplayName(L10n.nextUp) - .description("Keep watching where you left off or see what's up next.") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} - -struct NextUpWidget_Previews: PreviewProvider { - static var previews: some View { - Group { - NextUpEntryView(entry: .init( - date: Date(), - items: [( - .init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol") - )], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - NextUpEntryView(entry: .init( - date: Date(), - items: [ - ( - .init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol") - ), - ( - .init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol") - ), - ], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - NextUpEntryView(entry: .init( - date: Date(), - items: [ - ( - .init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol") - ), - ( - .init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol") - ), - ( - .init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"), - UIImage(named: "WidgetHeaderSymbol") - ), - ], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemLarge)) - NextUpEntryView(entry: .init( - date: Date(), - items: [( - .init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol") - )], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - .preferredColorScheme(.dark) - NextUpEntryView(entry: .init( - date: Date(), - items: [ - ( - .init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol") - ), - ( - .init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol") - ), - ], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - .preferredColorScheme(.dark) - NextUpEntryView(entry: .init( - date: Date(), - items: [ - ( - .init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol") - ), - ( - .init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol") - ), - ( - .init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"), - UIImage(named: "WidgetHeaderSymbol") - ), - ], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemLarge)) - .preferredColorScheme(.dark) - NextUpEntryView(entry: .init( - date: Date(), - items: [], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - .preferredColorScheme(.dark) - NextUpEntryView(entry: .init( - date: Date(), - items: [ - ( - .init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol") - ), - ], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - .preferredColorScheme(.dark) - NextUpEntryView(entry: .init( - date: Date(), - items: [ - ( - .init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol") - ), - ( - .init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol") - ), - ], - error: nil - )) - .previewContext(WidgetPreviewContext(family: .systemLarge)) - .preferredColorScheme(.dark) - } - } -} - -import SwiftUI - -private extension View { - func innerShadow(color: Color, radius: CGFloat = 0.1) -> some View { - modifier(InnerShadow(color: color, radius: min(max(0, radius), 1))) - } -} - -private struct InnerShadow: ViewModifier { - var color: Color = .gray - var radius: CGFloat = 0.1 - - private var colors: [Color] { - [color.opacity(0.75), color.opacity(0.0), .clear] - } - - func body(content: Content) -> some View { - GeometryReader { geo in - content - .overlay( - LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .top, endPoint: .bottom) - .frame(height: self.radius * self.minSide(geo)), - alignment: .top - ) - .overlay( - LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .bottom, endPoint: .top) - .frame(height: self.radius * self.minSide(geo)), - alignment: .bottom - ) - } - } - - func minSide(_ geo: GeometryProxy) -> CGFloat { - CGFloat(3) * min(geo.size.width, geo.size.height) / 2 - } -} diff --git a/swiftgen.yml b/swiftgen.yml index 9da8a089..6a5d425d 100644 --- a/swiftgen.yml +++ b/swiftgen.yml @@ -2,6 +2,4 @@ strings: inputs: Translations/en.lproj outputs: - templateName: structured-swift5 - output: Shared/Generated/Strings.swift - params: - lookupFunction: TranslationService.shared.lookupTranslation(forKey:inTable:) + output: Shared/Strings/Strings.swift