diff --git a/Shared/Components/TruncatedText.swift b/Shared/Components/TruncatedText.swift index 1ed51519..41dc86e0 100644 --- a/Shared/Components/TruncatedText.swift +++ b/Shared/Components/TruncatedText.swift @@ -11,6 +11,8 @@ import SwiftUI // TODO: only allow `view` selection when truncated? // TODO: fix when also using `lineLimit(reserveSpace > 1)` +// TODO: some false positives for showing see more? +// TODO: allow removing empty lines struct TruncatedText: View { diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 8176293f..e1059db3 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -288,7 +288,7 @@ E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; }; E122A9132788EAAD0060FA63 /* MediaStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStream.swift */; }; E122A9142788EAAD0060FA63 /* MediaStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStream.swift */; }; - E12376AE2A33D680001F5B44 /* AboutViewCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12376AD2A33D680001F5B44 /* AboutViewCard.swift */; }; + E12376AE2A33D680001F5B44 /* AboutView+Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12376AD2A33D680001F5B44 /* AboutView+Card.swift */; }; E12376B02A33D6AE001F5B44 /* AboutViewCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12376AF2A33D6AE001F5B44 /* AboutViewCard.swift */; }; E12376B12A33DB33001F5B44 /* MediaSourceInfoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E170D106294D23BA0017224C /* MediaSourceInfoCoordinator.swift */; }; E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12376B22A33DFAC001F5B44 /* ItemOverviewView.swift */; }; @@ -1115,7 +1115,7 @@ E11CEB8F28999D84003E74C7 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; E11CEB9328999D9E003E74C7 /* EpisodeItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemContentView.swift; sourceTree = ""; }; E122A9122788EAAD0060FA63 /* MediaStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStream.swift; sourceTree = ""; }; - E12376AD2A33D680001F5B44 /* AboutViewCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewCard.swift; sourceTree = ""; }; + E12376AD2A33D680001F5B44 /* AboutView+Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AboutView+Card.swift"; sourceTree = ""; }; E12376AF2A33D6AE001F5B44 /* AboutViewCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewCard.swift; sourceTree = ""; }; E12376B22A33DFAC001F5B44 /* ItemOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewView.swift; sourceTree = ""; }; E129428428F080B500796AC6 /* OnReceiveNotificationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnReceiveNotificationModifier.swift; sourceTree = ""; }; @@ -2879,7 +2879,7 @@ E18ACA932A15A3D800BB4F35 /* Components */ = { isa = PBXGroup; children = ( - E12376AD2A33D680001F5B44 /* AboutViewCard.swift */, + E12376AD2A33D680001F5B44 /* AboutView+Card.swift */, E1E750662A33E9B400B2C1EE /* MediaSourcesCard.swift */, E1E750652A33E9B400B2C1EE /* OverviewCard.swift */, E1E750672A33E9B400B2C1EE /* RatingsCard.swift */, @@ -4393,7 +4393,7 @@ E1A1529028FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */, E11042752B8013DF00821020 /* Stateful.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, - E12376AE2A33D680001F5B44 /* AboutViewCard.swift in Sources */, + E12376AE2A33D680001F5B44 /* AboutView+Card.swift in Sources */, E1A2C154279A7D5A005EC829 /* UIApplication.swift in Sources */, E1D8428F2933F2D900D1041A /* MediaSourceInfo.swift in Sources */, E1BDF2EC2952290200CC0294 /* AspectFillActionButton.swift in Sources */, diff --git a/Swiftfin/App/SwiftfinApp.swift b/Swiftfin/App/SwiftfinApp.swift index f237814a..dd01fb68 100644 --- a/Swiftfin/App/SwiftfinApp.swift +++ b/Swiftfin/App/SwiftfinApp.swift @@ -45,6 +45,11 @@ struct SwiftfinApp: App { // Sometimes the tab bar won't appear properly on push, always have material background UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance(idiom: .unspecified) + + // don't keep last user id + if Defaults[.signOutOnClose] { + Defaults[.lastSignedInUserID] = nil + } } var body: some Scene { diff --git a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift index 250b0134..22365aae 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift @@ -6,22 +6,112 @@ // Copyright (c) 2024 Jellyfin & Jellyfin Contributors // +import CollectionHStack import Defaults import JellyfinAPI +import OrderedCollections import SwiftUI // TODO: rename `AboutItemView` +// TODO: see what to do about bottom padding +// - don't like it adds more than the edge +// - just have this determine bottom padding +// instead of scrollviews? extension ItemView { struct AboutView: View { + private enum AboutViewItem: Hashable { + + case image + case overview + case mediaSource(MediaSourceInfo) + case ratings + } + @Default(.accentColor) private var accentColor @ObservedObject var viewModel: ItemViewModel + @State + private var contentSize: CGSize = .zero + @State + private var items: OrderedSet + + init(viewModel: ItemViewModel) { + self.viewModel = viewModel + + var items: OrderedSet = [ + .image, + .overview, + ] + + if let mediaSources = viewModel.item.mediaSources { + items.append(contentsOf: mediaSources.map { AboutViewItem.mediaSource($0) }) + } + + if viewModel.item.hasRatings { + items.append(.ratings) + } + + self._items = State(initialValue: items) + } + + // TODO: break out into a general solution for general use? + // use similar math from CollectionHStack + private var padImageWidth: CGFloat { + let portraitMinWidth: CGFloat = 140 + let contentWidth = contentSize.width + let usableWidth = contentWidth - EdgeInsets.edgePadding * 2 + var columns = CGFloat(Int(usableWidth / portraitMinWidth)) + let preItemSpacing = (columns - 1) * (EdgeInsets.edgePadding / 2) + let preTotalNegative = EdgeInsets.edgePadding * 2 + preItemSpacing + + if columns * portraitMinWidth + preTotalNegative > contentWidth { + columns -= 1 + } + + let itemSpacing = (columns - 1) * (EdgeInsets.edgePadding / 2) + let totalNegative = EdgeInsets.edgePadding * 2 + itemSpacing + let itemWidth = (contentWidth - totalNegative) / columns + + return max(0, itemWidth) + } + + private var phoneImageWidth: CGFloat { + let contentWidth = contentSize.width + let usableWidth = contentWidth - EdgeInsets.edgePadding * 2 + let itemSpacing = (EdgeInsets.edgePadding / 2) * 2 + let itemWidth = (usableWidth - itemSpacing) / 3 + + return max(0, itemWidth) + } + + private var cardSize: CGSize { + let height = UIDevice.isPad ? padImageWidth * 3 / 2 : phoneImageWidth * 3 / 2 + let width = height * 1.65 + + return CGSize(width: width, height: height) + } + + private var imageView: some View { + ZStack { + Color.clear + + ImageView( + viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel + .item.imageSource(.primary, maxWidth: 300) + ) + .accessibilityIgnoresInvertColors() + } + .posterStyle(.portrait) + .posterShadow() + .frame(width: UIDevice.isPad ? padImageWidth : phoneImageWidth) + } + var body: some View { VStack(alignment: .leading) { L10n.about.text @@ -30,30 +120,30 @@ extension ItemView { .accessibility(addTraits: [.isHeader]) .edgePadding(.horizontal) - ScrollView(.horizontal, showsIndicators: false) { - HStack { - ImageView( - viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel - .item.imageSource(.primary, maxWidth: 300) - ) - .posterStyle(.portrait) - .posterShadow() - .frame(width: 130) - .accessibilityIgnoresInvertColors() - + CollectionHStack($items, variadicWidths: true) { item in + switch item { + case .image: + imageView + case .overview: OverviewCard(item: viewModel.item) - - if let mediaSources = viewModel.item.mediaSources { - ForEach(mediaSources) { source in - MediaSourcesCard(subtitle: mediaSources.count > 1 ? source.displayTitle : nil, source: source) - } - } - + .frame(width: cardSize.width, height: cardSize.height) + case let .mediaSource(source): + MediaSourcesCard( + subtitle: (viewModel.item.mediaSources ?? []).count > 1 ? source.displayTitle : nil, + source: source + ) + .frame(width: cardSize.width, height: cardSize.height) + case .ratings: RatingsCard(item: viewModel.item) + .frame(width: cardSize.width, height: cardSize.height) } - .edgePadding(.horizontal) } + .clipsToBounds(false) + .insets(horizontal: EdgeInsets.edgePadding) + .itemSpacing(EdgeInsets.edgePadding / 2) + .scrollBehavior(.continuousLeadingEdge) } + .trackingSize($contentSize) } } } diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift similarity index 93% rename from Swiftfin/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift rename to Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift index ba783219..6829e37b 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift @@ -23,8 +23,8 @@ extension ItemView.AboutView { } label: { ZStack(alignment: .leading) { - Color.secondarySystemFill - .cornerRadius(10) + Color.systemFill + .cornerRadius(ratio: 1 / 45, of: \.height) VStack(alignment: .leading, spacing: 5) { Text(title) @@ -48,7 +48,6 @@ extension ItemView.AboutView { } .padding() } - .frame(width: 330, height: 195) } .buttonStyle(.plain) } diff --git a/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift b/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift index 1110e16f..8ed6be6a 100644 --- a/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift +++ b/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift @@ -50,7 +50,7 @@ extension ItemView { } var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 0) { overlay() .frame(height: (size.height + safeAreaInsets.vertical) * heightRatio) diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift index 142a2970..f0a005dc 100644 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift @@ -16,7 +16,7 @@ struct CollectionItemView: View { var viewModel: CollectionItemViewModel var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { ContentView(viewModel: viewModel) .edgePadding(.bottom) } diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift index 784e89fe..1df326d4 100644 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift @@ -15,7 +15,7 @@ struct EpisodeItemView: View { var viewModel: EpisodeItemViewModel var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { ContentView(viewModel: viewModel) .edgePadding(.bottom) } diff --git a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift index bceb066f..668ea432 100644 --- a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift @@ -36,7 +36,7 @@ extension ItemView { ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 1920)) } } - .aspectRatio(contentMode: .fill) + .aspectRatio(1.77, contentMode: .fill) } var body: some View {