Complete Collections' `ItemView` (#1500)
* WIP * Remove Divider * self deprecation message * `OrderedDictionary<BaseItemKind, [BaseItemDto]>` * Localization fun * cleanup * Remove play button items & order by BaseItemKind. * Fix AttributesHStack on iPad and make sure they align to the correct side. Looks jarring on Collections since Collections are often more limited on AttributesHStack items. * Localization fix * cleanup * cleanup --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
346dfde4fc
commit
6700ce969c
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AttributeBadge<Content: View>: View {
|
struct AttributeBadge: View {
|
||||||
|
|
||||||
@Environment(\.font)
|
@Environment(\.font)
|
||||||
private var font
|
private var font
|
||||||
|
@ -19,7 +19,7 @@ struct AttributeBadge<Content: View>: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private let style: AttributeStyle
|
private let style: AttributeStyle
|
||||||
private let content: () -> Content
|
private let content: () -> any View
|
||||||
|
|
||||||
private var usedFont: Font {
|
private var usedFont: Font {
|
||||||
font ?? .caption.weight(.semibold)
|
font ?? .caption.weight(.semibold)
|
||||||
|
@ -29,6 +29,7 @@ struct AttributeBadge<Content: View>: View {
|
||||||
private var innerBody: some View {
|
private var innerBody: some View {
|
||||||
if style == .fill {
|
if style == .fill {
|
||||||
content()
|
content()
|
||||||
|
.eraseToAnyView()
|
||||||
.padding(.init(vertical: 1, horizontal: 4))
|
.padding(.init(vertical: 1, horizontal: 4))
|
||||||
.hidden()
|
.hidden()
|
||||||
.background {
|
.background {
|
||||||
|
@ -36,11 +37,13 @@ struct AttributeBadge<Content: View>: View {
|
||||||
.cornerRadius(2)
|
.cornerRadius(2)
|
||||||
.inverseMask {
|
.inverseMask {
|
||||||
content()
|
content()
|
||||||
|
.eraseToAnyView()
|
||||||
.padding(.init(vertical: 1, horizontal: 4))
|
.padding(.init(vertical: 1, horizontal: 4))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content()
|
content()
|
||||||
|
.eraseToAnyView()
|
||||||
.foregroundStyle(Color(UIColor.lightGray))
|
.foregroundStyle(Color(UIColor.lightGray))
|
||||||
.padding(.init(vertical: 1, horizontal: 4))
|
.padding(.init(vertical: 1, horizontal: 4))
|
||||||
.overlay(
|
.overlay(
|
||||||
|
@ -57,7 +60,7 @@ struct AttributeBadge<Content: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttributeBadge where Content == Text {
|
extension AttributeBadge {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
style: AttributeStyle,
|
style: AttributeStyle,
|
||||||
|
@ -76,9 +79,6 @@ extension AttributeBadge where Content == Text {
|
||||||
Text(title)
|
Text(title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension AttributeBadge where Content == Label<Text, Image> {
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
style: AttributeStyle,
|
style: AttributeStyle,
|
||||||
|
|
|
@ -24,14 +24,171 @@ extension BaseItemKind: SupportedCaseIterable {
|
||||||
|
|
||||||
extension BaseItemKind: ItemFilter {
|
extension BaseItemKind: ItemFilter {
|
||||||
|
|
||||||
// TODO: localize
|
|
||||||
var displayTitle: String {
|
var displayTitle: String {
|
||||||
rawValue
|
switch self {
|
||||||
|
case .aggregateFolder:
|
||||||
|
return L10n.aggregateFolder
|
||||||
|
case .audio:
|
||||||
|
return L10n.audio
|
||||||
|
case .audioBook:
|
||||||
|
return L10n.audioBook
|
||||||
|
case .basePluginFolder:
|
||||||
|
return L10n.basePluginFolder
|
||||||
|
case .book:
|
||||||
|
return L10n.book
|
||||||
|
case .boxSet:
|
||||||
|
return L10n.collection
|
||||||
|
case .channel:
|
||||||
|
return L10n.channel
|
||||||
|
case .channelFolderItem:
|
||||||
|
return L10n.channelFolderItem
|
||||||
|
case .collectionFolder:
|
||||||
|
return L10n.collectionFolder
|
||||||
|
case .episode:
|
||||||
|
return L10n.episode
|
||||||
|
case .folder:
|
||||||
|
return L10n.folder
|
||||||
|
case .genre:
|
||||||
|
return L10n.genre
|
||||||
|
case .manualPlaylistsFolder:
|
||||||
|
return L10n.manualPlaylistsFolder
|
||||||
|
case .movie:
|
||||||
|
return L10n.movie
|
||||||
|
case .liveTvChannel:
|
||||||
|
return L10n.liveTVChannel
|
||||||
|
case .liveTvProgram:
|
||||||
|
return L10n.liveTVProgram
|
||||||
|
case .musicAlbum:
|
||||||
|
return L10n.album
|
||||||
|
case .musicArtist:
|
||||||
|
return L10n.artist
|
||||||
|
case .musicGenre:
|
||||||
|
return L10n.genre
|
||||||
|
case .musicVideo:
|
||||||
|
return L10n.musicVideo
|
||||||
|
case .person:
|
||||||
|
return L10n.person
|
||||||
|
case .photo:
|
||||||
|
return L10n.photo
|
||||||
|
case .photoAlbum:
|
||||||
|
return L10n.photoAlbum
|
||||||
|
case .playlist:
|
||||||
|
return L10n.playlist
|
||||||
|
case .playlistsFolder:
|
||||||
|
return L10n.playlistsFolder
|
||||||
|
case .program:
|
||||||
|
return L10n.program
|
||||||
|
case .recording:
|
||||||
|
return L10n.recording
|
||||||
|
case .season:
|
||||||
|
return L10n.season
|
||||||
|
case .series:
|
||||||
|
return L10n.series
|
||||||
|
case .studio:
|
||||||
|
return L10n.studio
|
||||||
|
case .trailer:
|
||||||
|
return L10n.trailer
|
||||||
|
case .tvChannel:
|
||||||
|
return L10n.tvChannel
|
||||||
|
case .tvProgram:
|
||||||
|
return L10n.tvProgram
|
||||||
|
case .userRootFolder:
|
||||||
|
return L10n.userRootFolder
|
||||||
|
case .userView:
|
||||||
|
return L10n.userView
|
||||||
|
case .video:
|
||||||
|
return L10n.video
|
||||||
|
case .year:
|
||||||
|
return L10n.year
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BaseItemKind {
|
extension BaseItemKind {
|
||||||
|
|
||||||
|
var pluralDisplayTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .aggregateFolder:
|
||||||
|
return L10n.aggregateFolders
|
||||||
|
case .audio:
|
||||||
|
return L10n.audio
|
||||||
|
case .audioBook:
|
||||||
|
return L10n.audioBooks
|
||||||
|
case .basePluginFolder:
|
||||||
|
return L10n.basePluginFolders
|
||||||
|
case .book:
|
||||||
|
return L10n.books
|
||||||
|
case .boxSet:
|
||||||
|
return L10n.collections
|
||||||
|
case .channel:
|
||||||
|
return L10n.channels
|
||||||
|
case .channelFolderItem:
|
||||||
|
return L10n.channelFolderItems
|
||||||
|
case .collectionFolder:
|
||||||
|
return L10n.collectionFolders
|
||||||
|
case .episode:
|
||||||
|
return L10n.episodes
|
||||||
|
case .folder:
|
||||||
|
return L10n.folders
|
||||||
|
case .genre:
|
||||||
|
return L10n.genres
|
||||||
|
case .manualPlaylistsFolder:
|
||||||
|
return L10n.manualPlaylistsFolders
|
||||||
|
case .movie:
|
||||||
|
return L10n.movies
|
||||||
|
case .liveTvChannel:
|
||||||
|
return L10n.liveTVChannels
|
||||||
|
case .liveTvProgram:
|
||||||
|
return L10n.liveTVPrograms
|
||||||
|
case .musicAlbum:
|
||||||
|
return L10n.albums
|
||||||
|
case .musicArtist:
|
||||||
|
return L10n.artists
|
||||||
|
case .musicGenre:
|
||||||
|
return L10n.genres
|
||||||
|
case .musicVideo:
|
||||||
|
return L10n.musicVideos
|
||||||
|
case .person:
|
||||||
|
return L10n.people
|
||||||
|
case .photo:
|
||||||
|
return L10n.photos
|
||||||
|
case .photoAlbum:
|
||||||
|
return L10n.photoAlbums
|
||||||
|
case .playlist:
|
||||||
|
return L10n.playlists
|
||||||
|
case .playlistsFolder:
|
||||||
|
return L10n.playlistsFolders
|
||||||
|
case .program:
|
||||||
|
return L10n.programs
|
||||||
|
case .recording:
|
||||||
|
return L10n.recordings
|
||||||
|
case .season:
|
||||||
|
return L10n.seasons
|
||||||
|
case .series:
|
||||||
|
return L10n.series
|
||||||
|
case .studio:
|
||||||
|
return L10n.studios
|
||||||
|
case .trailer:
|
||||||
|
return L10n.trailers
|
||||||
|
case .tvChannel:
|
||||||
|
return L10n.tvChannels
|
||||||
|
case .tvProgram:
|
||||||
|
return L10n.tvPrograms
|
||||||
|
case .userRootFolder:
|
||||||
|
return L10n.userRootFolders
|
||||||
|
case .userView:
|
||||||
|
return L10n.userViews
|
||||||
|
case .video:
|
||||||
|
return L10n.videos
|
||||||
|
case .year:
|
||||||
|
return L10n.years
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BaseItemKind {
|
||||||
|
|
||||||
|
/// Item types that can be identified on the server.
|
||||||
static var itemIdentifiableCases: [BaseItemKind] {
|
static var itemIdentifiableCases: [BaseItemKind] {
|
||||||
[.boxSet, .movie, .person, .series]
|
[.boxSet, .movie, .person, .series]
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,4 +13,21 @@ extension OrderedDictionary {
|
||||||
var isNotEmpty: Bool {
|
var isNotEmpty: Bool {
|
||||||
!isEmpty
|
!isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compactKeys<WrappedKey: Hashable>() -> OrderedDictionary<WrappedKey, Value> where Key == WrappedKey? {
|
||||||
|
reduce(into: OrderedDictionary<WrappedKey, Value>()) { result, pair in
|
||||||
|
if let unwrappedKey = pair.key {
|
||||||
|
result[unwrappedKey] = pair.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedKeys(by areInIncreasingOrder: (Key, Key) -> Bool) -> OrderedDictionary<Key, Value> {
|
||||||
|
let sortedKeys = keys.sorted(by: areInIncreasingOrder)
|
||||||
|
|
||||||
|
return OrderedDictionary(uniqueKeysWithValues: sortedKeys.compactMap { key in
|
||||||
|
guard let value = self[key] else { return nil }
|
||||||
|
return (key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,10 @@ internal enum L10n {
|
||||||
internal static func agesGroup(_ p1: Any) -> String {
|
internal static func agesGroup(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "agesGroup", String(describing: p1), fallback: "Age %@")
|
return L10n.tr("Localizable", "agesGroup", String(describing: p1), fallback: "Age %@")
|
||||||
}
|
}
|
||||||
|
/// Aggregate folder
|
||||||
|
internal static let aggregateFolder = L10n.tr("Localizable", "aggregateFolder", fallback: "Aggregate folder")
|
||||||
|
/// Aggregate folders
|
||||||
|
internal static let aggregateFolders = L10n.tr("Localizable", "aggregateFolders", fallback: "Aggregate folders")
|
||||||
/// Aired
|
/// Aired
|
||||||
internal static let aired = L10n.tr("Localizable", "aired", fallback: "Aired")
|
internal static let aired = L10n.tr("Localizable", "aired", fallback: "Aired")
|
||||||
/// Aired episode order
|
/// Aired episode order
|
||||||
|
@ -84,6 +88,10 @@ internal enum L10n {
|
||||||
internal static let album = L10n.tr("Localizable", "album", fallback: "Album")
|
internal static let album = L10n.tr("Localizable", "album", fallback: "Album")
|
||||||
/// Album Artist
|
/// Album Artist
|
||||||
internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist")
|
internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist")
|
||||||
|
/// Album Artists
|
||||||
|
internal static let albumArtists = L10n.tr("Localizable", "albumArtists", fallback: "Album Artists")
|
||||||
|
/// Albums
|
||||||
|
internal static let albums = L10n.tr("Localizable", "albums", fallback: "Albums")
|
||||||
/// All
|
/// All
|
||||||
internal static let all = L10n.tr("Localizable", "all", fallback: "All")
|
internal static let all = L10n.tr("Localizable", "all", fallback: "All")
|
||||||
/// All Audiences
|
/// All Audiences
|
||||||
|
@ -128,6 +136,8 @@ internal enum L10n {
|
||||||
internal static let art = L10n.tr("Localizable", "art", fallback: "Art")
|
internal static let art = L10n.tr("Localizable", "art", fallback: "Art")
|
||||||
/// Artist
|
/// Artist
|
||||||
internal static let artist = L10n.tr("Localizable", "artist", fallback: "Artist")
|
internal static let artist = L10n.tr("Localizable", "artist", fallback: "Artist")
|
||||||
|
/// Artists
|
||||||
|
internal static let artists = L10n.tr("Localizable", "artists", fallback: "Artists")
|
||||||
/// Ascending
|
/// Ascending
|
||||||
internal static let ascending = L10n.tr("Localizable", "ascending", fallback: "Ascending")
|
internal static let ascending = L10n.tr("Localizable", "ascending", fallback: "Ascending")
|
||||||
/// Aspect Fill
|
/// Aspect Fill
|
||||||
|
@ -138,6 +148,10 @@ internal enum L10n {
|
||||||
internal static let audioBitDepthNotSupported = L10n.tr("Localizable", "audioBitDepthNotSupported", fallback: "The audio bit depth is not supported")
|
internal static let audioBitDepthNotSupported = L10n.tr("Localizable", "audioBitDepthNotSupported", fallback: "The audio bit depth is not supported")
|
||||||
/// The audio bitrate is not supported
|
/// The audio bitrate is not supported
|
||||||
internal static let audioBitrateNotSupported = L10n.tr("Localizable", "audioBitrateNotSupported", fallback: "The audio bitrate is not supported")
|
internal static let audioBitrateNotSupported = L10n.tr("Localizable", "audioBitrateNotSupported", fallback: "The audio bitrate is not supported")
|
||||||
|
/// Audio book
|
||||||
|
internal static let audioBook = L10n.tr("Localizable", "audioBook", fallback: "Audio book")
|
||||||
|
/// Audio books
|
||||||
|
internal static let audioBooks = L10n.tr("Localizable", "audioBooks", fallback: "Audio books")
|
||||||
/// The number of audio channels is not supported
|
/// The number of audio channels is not supported
|
||||||
internal static let audioChannelsNotSupported = L10n.tr("Localizable", "audioChannelsNotSupported", fallback: "The number of audio channels is not supported")
|
internal static let audioChannelsNotSupported = L10n.tr("Localizable", "audioChannelsNotSupported", fallback: "The number of audio channels is not supported")
|
||||||
/// The audio codec is not supported
|
/// The audio codec is not supported
|
||||||
|
@ -170,6 +184,10 @@ internal enum L10n {
|
||||||
internal static let banner = L10n.tr("Localizable", "banner", fallback: "Banner")
|
internal static let banner = L10n.tr("Localizable", "banner", fallback: "Banner")
|
||||||
/// Bar Buttons
|
/// Bar Buttons
|
||||||
internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons")
|
internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons")
|
||||||
|
/// Plugin folder
|
||||||
|
internal static let basePluginFolder = L10n.tr("Localizable", "basePluginFolder", fallback: "Plugin folder")
|
||||||
|
/// Plugin folders
|
||||||
|
internal static let basePluginFolders = L10n.tr("Localizable", "basePluginFolders", fallback: "Plugin folders")
|
||||||
/// Behavior
|
/// Behavior
|
||||||
internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "Behavior")
|
internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "Behavior")
|
||||||
/// Behind the Scenes
|
/// Behind the Scenes
|
||||||
|
@ -234,6 +252,8 @@ internal enum L10n {
|
||||||
internal static let blockUnratedItemsDescription = L10n.tr("Localizable", "blockUnratedItemsDescription", fallback: "Block items from this user with no or unrecognized rating information.")
|
internal static let blockUnratedItemsDescription = L10n.tr("Localizable", "blockUnratedItemsDescription", fallback: "Block items from this user with no or unrecognized rating information.")
|
||||||
/// Blue
|
/// Blue
|
||||||
internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue")
|
internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue")
|
||||||
|
/// Book
|
||||||
|
internal static let book = L10n.tr("Localizable", "book", fallback: "Book")
|
||||||
/// Books
|
/// Books
|
||||||
internal static let books = L10n.tr("Localizable", "books", fallback: "Books")
|
internal static let books = L10n.tr("Localizable", "books", fallback: "Books")
|
||||||
/// Box
|
/// Box
|
||||||
|
@ -262,8 +282,16 @@ internal enum L10n {
|
||||||
internal static let category = L10n.tr("Localizable", "category", fallback: "Category")
|
internal static let category = L10n.tr("Localizable", "category", fallback: "Category")
|
||||||
/// Change Pin
|
/// Change Pin
|
||||||
internal static let changePin = L10n.tr("Localizable", "changePin", fallback: "Change Pin")
|
internal static let changePin = L10n.tr("Localizable", "changePin", fallback: "Change Pin")
|
||||||
|
/// Channel
|
||||||
|
internal static let channel = L10n.tr("Localizable", "channel", fallback: "Channel")
|
||||||
/// Channel display
|
/// Channel display
|
||||||
internal static let channelDisplay = L10n.tr("Localizable", "channelDisplay", fallback: "Channel display")
|
internal static let channelDisplay = L10n.tr("Localizable", "channelDisplay", fallback: "Channel display")
|
||||||
|
/// Channel folder
|
||||||
|
internal static let channelFolder = L10n.tr("Localizable", "channelFolder", fallback: "Channel folder")
|
||||||
|
/// Channel folder item
|
||||||
|
internal static let channelFolderItem = L10n.tr("Localizable", "channelFolderItem", fallback: "Channel folder item")
|
||||||
|
/// Channel folder items
|
||||||
|
internal static let channelFolderItems = L10n.tr("Localizable", "channelFolderItems", fallback: "Channel folder items")
|
||||||
/// Channels
|
/// Channels
|
||||||
internal static let channels = L10n.tr("Localizable", "channels", fallback: "Channels")
|
internal static let channels = L10n.tr("Localizable", "channels", fallback: "Channels")
|
||||||
/// Chapter
|
/// Chapter
|
||||||
|
@ -282,6 +310,12 @@ internal enum L10n {
|
||||||
internal static let clip = L10n.tr("Localizable", "clip", fallback: "Clip")
|
internal static let clip = L10n.tr("Localizable", "clip", fallback: "Clip")
|
||||||
/// Close
|
/// Close
|
||||||
internal static let close = L10n.tr("Localizable", "close", fallback: "Close")
|
internal static let close = L10n.tr("Localizable", "close", fallback: "Close")
|
||||||
|
/// Collection
|
||||||
|
internal static let collection = L10n.tr("Localizable", "collection", fallback: "Collection")
|
||||||
|
/// Collection folder
|
||||||
|
internal static let collectionFolder = L10n.tr("Localizable", "collectionFolder", fallback: "Collection folder")
|
||||||
|
/// Collection folders
|
||||||
|
internal static let collectionFolders = L10n.tr("Localizable", "collectionFolders", fallback: "Collection folders")
|
||||||
/// Collections
|
/// Collections
|
||||||
internal static let collections = L10n.tr("Localizable", "collections", fallback: "Collections")
|
internal static let collections = L10n.tr("Localizable", "collections", fallback: "Collections")
|
||||||
/// Color
|
/// Color
|
||||||
|
@ -658,6 +692,8 @@ internal enum L10n {
|
||||||
internal static let findMissingDescription = L10n.tr("Localizable", "findMissingDescription", fallback: "Find missing metadata and images.")
|
internal static let findMissingDescription = L10n.tr("Localizable", "findMissingDescription", fallback: "Find missing metadata and images.")
|
||||||
/// Folder
|
/// Folder
|
||||||
internal static let folder = L10n.tr("Localizable", "folder", fallback: "Folder")
|
internal static let folder = L10n.tr("Localizable", "folder", fallback: "Folder")
|
||||||
|
/// Folders
|
||||||
|
internal static let folders = L10n.tr("Localizable", "folders", fallback: "Folders")
|
||||||
/// Force remote media transcoding
|
/// Force remote media transcoding
|
||||||
internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding")
|
internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding")
|
||||||
/// Format
|
/// Format
|
||||||
|
@ -668,6 +704,8 @@ internal enum L10n {
|
||||||
internal static let fullSideBySide = L10n.tr("Localizable", "fullSideBySide", fallback: "Full Side-by-Side")
|
internal static let fullSideBySide = L10n.tr("Localizable", "fullSideBySide", fallback: "Full Side-by-Side")
|
||||||
/// Full Top and Bottom
|
/// Full Top and Bottom
|
||||||
internal static let fullTopAndBottom = L10n.tr("Localizable", "fullTopAndBottom", fallback: "Full Top and Bottom")
|
internal static let fullTopAndBottom = L10n.tr("Localizable", "fullTopAndBottom", fallback: "Full Top and Bottom")
|
||||||
|
/// Genre
|
||||||
|
internal static let genre = L10n.tr("Localizable", "genre", fallback: "Genre")
|
||||||
/// Genres
|
/// Genres
|
||||||
internal static let genres = L10n.tr("Localizable", "genres", fallback: "Genres")
|
internal static let genres = L10n.tr("Localizable", "genres", fallback: "Genres")
|
||||||
/// Categories that describe the themes or styles of media.
|
/// Categories that describe the themes or styles of media.
|
||||||
|
@ -794,6 +832,8 @@ internal enum L10n {
|
||||||
internal static let layout = L10n.tr("Localizable", "layout", fallback: "Layout")
|
internal static let layout = L10n.tr("Localizable", "layout", fallback: "Layout")
|
||||||
/// Learn more
|
/// Learn more
|
||||||
internal static let learnMore = L10n.tr("Localizable", "learnMore", fallback: "Learn more")
|
internal static let learnMore = L10n.tr("Localizable", "learnMore", fallback: "Learn more")
|
||||||
|
/// Learn more...
|
||||||
|
internal static let learnMoreEllipsis = L10n.tr("Localizable", "learnMoreEllipsis", fallback: "Learn more...")
|
||||||
/// Left
|
/// Left
|
||||||
internal static let `left` = L10n.tr("Localizable", "left", fallback: "Left")
|
internal static let `left` = L10n.tr("Localizable", "left", fallback: "Left")
|
||||||
/// Left vertical pan
|
/// Left vertical pan
|
||||||
|
@ -821,11 +861,17 @@ internal enum L10n {
|
||||||
/// Live TV
|
/// Live TV
|
||||||
internal static let liveTV = L10n.tr("Localizable", "liveTV", fallback: "Live TV")
|
internal static let liveTV = L10n.tr("Localizable", "liveTV", fallback: "Live TV")
|
||||||
/// Live TV access
|
/// Live TV access
|
||||||
internal static let liveTvAccess = L10n.tr("Localizable", "liveTvAccess", fallback: "Live TV access")
|
internal static let liveTVAccess = L10n.tr("Localizable", "liveTVAccess", fallback: "Live TV access")
|
||||||
/// Live TV Channels
|
/// Live TV channel
|
||||||
internal static let liveTVChannels = L10n.tr("Localizable", "liveTVChannels", fallback: "Live TV Channels")
|
internal static let liveTVChannel = L10n.tr("Localizable", "liveTVChannel", fallback: "Live TV channel")
|
||||||
/// Live TV Programs
|
/// Live TV channels
|
||||||
internal static let liveTVPrograms = L10n.tr("Localizable", "liveTVPrograms", fallback: "Live TV Programs")
|
internal static let liveTVChannels = L10n.tr("Localizable", "liveTVChannels", fallback: "Live TV channels")
|
||||||
|
/// Live TV program
|
||||||
|
internal static let liveTVProgram = L10n.tr("Localizable", "liveTVProgram", fallback: "Live TV program")
|
||||||
|
/// Live TV programs
|
||||||
|
internal static let liveTVPrograms = L10n.tr("Localizable", "liveTVPrograms", fallback: "Live TV programs")
|
||||||
|
/// Live TV recording management
|
||||||
|
internal static let liveTVRecordingManagement = L10n.tr("Localizable", "liveTVRecordingManagement", fallback: "Live TV recording management")
|
||||||
/// Live TV recording management
|
/// Live TV recording management
|
||||||
internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management")
|
internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management")
|
||||||
/// Loading user failed
|
/// Loading user failed
|
||||||
|
@ -854,6 +900,10 @@ internal enum L10n {
|
||||||
internal static let lyrics = L10n.tr("Localizable", "lyrics", fallback: "Lyrics")
|
internal static let lyrics = L10n.tr("Localizable", "lyrics", fallback: "Lyrics")
|
||||||
/// Management
|
/// Management
|
||||||
internal static let management = L10n.tr("Localizable", "management", fallback: "Management")
|
internal static let management = L10n.tr("Localizable", "management", fallback: "Management")
|
||||||
|
/// Manual playlists folder
|
||||||
|
internal static let manualPlaylistsFolder = L10n.tr("Localizable", "manualPlaylistsFolder", fallback: "Manual playlists folder")
|
||||||
|
/// Manual playlists folders
|
||||||
|
internal static let manualPlaylistsFolders = L10n.tr("Localizable", "manualPlaylistsFolders", fallback: "Manual playlists folders")
|
||||||
/// Maximum Bitrate
|
/// Maximum Bitrate
|
||||||
internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate")
|
internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate")
|
||||||
/// Limits the total number of connections a user can have to the server.
|
/// Limits the total number of connections a user can have to the server.
|
||||||
|
@ -906,12 +956,18 @@ internal enum L10n {
|
||||||
internal static let missingItems = L10n.tr("Localizable", "missingItems", fallback: "Missing Items")
|
internal static let missingItems = L10n.tr("Localizable", "missingItems", fallback: "Missing Items")
|
||||||
/// Mixer
|
/// Mixer
|
||||||
internal static let mixer = L10n.tr("Localizable", "mixer", fallback: "Mixer")
|
internal static let mixer = L10n.tr("Localizable", "mixer", fallback: "Mixer")
|
||||||
|
/// Movie
|
||||||
|
internal static let movie = L10n.tr("Localizable", "movie", fallback: "Movie")
|
||||||
/// Movies
|
/// Movies
|
||||||
internal static let movies = L10n.tr("Localizable", "movies", fallback: "Movies")
|
internal static let movies = L10n.tr("Localizable", "movies", fallback: "Movies")
|
||||||
/// Multi tap
|
/// Multi tap
|
||||||
internal static let multiTap = L10n.tr("Localizable", "multiTap", fallback: "Multi tap")
|
internal static let multiTap = L10n.tr("Localizable", "multiTap", fallback: "Multi tap")
|
||||||
/// Music
|
/// Music
|
||||||
internal static let music = L10n.tr("Localizable", "music", fallback: "Music")
|
internal static let music = L10n.tr("Localizable", "music", fallback: "Music")
|
||||||
|
/// Music video
|
||||||
|
internal static let musicVideo = L10n.tr("Localizable", "musicVideo", fallback: "Music video")
|
||||||
|
/// Music videos
|
||||||
|
internal static let musicVideos = L10n.tr("Localizable", "musicVideos", fallback: "Music videos")
|
||||||
/// MVC
|
/// MVC
|
||||||
internal static let mvc = L10n.tr("Localizable", "mvc", fallback: "MVC")
|
internal static let mvc = L10n.tr("Localizable", "mvc", fallback: "MVC")
|
||||||
/// Name
|
/// Name
|
||||||
|
@ -1026,6 +1082,16 @@ internal enum L10n {
|
||||||
internal static let peopleDescription = L10n.tr("Localizable", "peopleDescription", fallback: "People who helped create or perform specific media.")
|
internal static let peopleDescription = L10n.tr("Localizable", "peopleDescription", fallback: "People who helped create or perform specific media.")
|
||||||
/// Permissions
|
/// Permissions
|
||||||
internal static let permissions = L10n.tr("Localizable", "permissions", fallback: "Permissions")
|
internal static let permissions = L10n.tr("Localizable", "permissions", fallback: "Permissions")
|
||||||
|
/// Person
|
||||||
|
internal static let person = L10n.tr("Localizable", "person", fallback: "Person")
|
||||||
|
/// Photo
|
||||||
|
internal static let photo = L10n.tr("Localizable", "photo", fallback: "Photo")
|
||||||
|
/// Photo album
|
||||||
|
internal static let photoAlbum = L10n.tr("Localizable", "photoAlbum", fallback: "Photo album")
|
||||||
|
/// Photo albums
|
||||||
|
internal static let photoAlbums = L10n.tr("Localizable", "photoAlbums", fallback: "Photo albums")
|
||||||
|
/// Photos
|
||||||
|
internal static let photos = L10n.tr("Localizable", "photos", fallback: "Photos")
|
||||||
/// Pin
|
/// Pin
|
||||||
internal static let pin = L10n.tr("Localizable", "pin", fallback: "Pin")
|
internal static let pin = L10n.tr("Localizable", "pin", fallback: "Pin")
|
||||||
/// Pinch
|
/// Pinch
|
||||||
|
@ -1048,6 +1114,14 @@ internal enum L10n {
|
||||||
internal static let played = L10n.tr("Localizable", "played", fallback: "Played")
|
internal static let played = L10n.tr("Localizable", "played", fallback: "Played")
|
||||||
/// Play From Beginning
|
/// Play From Beginning
|
||||||
internal static let playFromBeginning = L10n.tr("Localizable", "playFromBeginning", fallback: "Play From Beginning")
|
internal static let playFromBeginning = L10n.tr("Localizable", "playFromBeginning", fallback: "Play From Beginning")
|
||||||
|
/// Playlist
|
||||||
|
internal static let playlist = L10n.tr("Localizable", "playlist", fallback: "Playlist")
|
||||||
|
/// Playlists
|
||||||
|
internal static let playlists = L10n.tr("Localizable", "playlists", fallback: "Playlists")
|
||||||
|
/// Playlists folder
|
||||||
|
internal static let playlistsFolder = L10n.tr("Localizable", "playlistsFolder", fallback: "Playlists folder")
|
||||||
|
/// Playlists folders
|
||||||
|
internal static let playlistsFolders = L10n.tr("Localizable", "playlistsFolders", fallback: "Playlists folders")
|
||||||
/// Play Next Item
|
/// Play Next Item
|
||||||
internal static let playNextItem = L10n.tr("Localizable", "playNextItem", fallback: "Play Next Item")
|
internal static let playNextItem = L10n.tr("Localizable", "playNextItem", fallback: "Play Next Item")
|
||||||
/// Play on active
|
/// Play on active
|
||||||
|
@ -1082,6 +1156,8 @@ internal enum L10n {
|
||||||
internal static let profileNotSaved = L10n.tr("Localizable", "profileNotSaved", fallback: "Profile not saved")
|
internal static let profileNotSaved = L10n.tr("Localizable", "profileNotSaved", fallback: "Profile not saved")
|
||||||
/// Profiles
|
/// Profiles
|
||||||
internal static let profiles = L10n.tr("Localizable", "profiles", fallback: "Profiles")
|
internal static let profiles = L10n.tr("Localizable", "profiles", fallback: "Profiles")
|
||||||
|
/// Program
|
||||||
|
internal static let program = L10n.tr("Localizable", "program", fallback: "Program")
|
||||||
/// Programs
|
/// Programs
|
||||||
internal static let programs = L10n.tr("Localizable", "programs", fallback: "Programs")
|
internal static let programs = L10n.tr("Localizable", "programs", fallback: "Programs")
|
||||||
/// Progress
|
/// Progress
|
||||||
|
@ -1122,6 +1198,10 @@ internal enum L10n {
|
||||||
internal static let recentlyAdded = L10n.tr("Localizable", "recentlyAdded", fallback: "Recently Added")
|
internal static let recentlyAdded = L10n.tr("Localizable", "recentlyAdded", fallback: "Recently Added")
|
||||||
/// Recommended
|
/// Recommended
|
||||||
internal static let recommended = L10n.tr("Localizable", "recommended", fallback: "Recommended")
|
internal static let recommended = L10n.tr("Localizable", "recommended", fallback: "Recommended")
|
||||||
|
/// Recording
|
||||||
|
internal static let recording = L10n.tr("Localizable", "recording", fallback: "Recording")
|
||||||
|
/// Recordings
|
||||||
|
internal static let recordings = L10n.tr("Localizable", "recordings", fallback: "Recordings")
|
||||||
/// Red
|
/// Red
|
||||||
internal static let red = L10n.tr("Localizable", "red", fallback: "Red")
|
internal static let red = L10n.tr("Localizable", "red", fallback: "Red")
|
||||||
/// The number of reference frames is not supported
|
/// The number of reference frames is not supported
|
||||||
|
@ -1260,6 +1340,8 @@ internal enum L10n {
|
||||||
internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String {
|
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$@")
|
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")
|
||||||
/// Secondary audio is not supported
|
/// Secondary audio is not supported
|
||||||
internal static let secondaryAudioNotSupported = L10n.tr("Localizable", "secondaryAudioNotSupported", fallback: "Secondary audio is not supported")
|
internal static let secondaryAudioNotSupported = L10n.tr("Localizable", "secondaryAudioNotSupported", fallback: "Secondary audio is not supported")
|
||||||
/// Security
|
/// Security
|
||||||
|
@ -1500,14 +1582,22 @@ internal enum L10n {
|
||||||
internal static let transition = L10n.tr("Localizable", "transition", fallback: "Transition")
|
internal static let transition = L10n.tr("Localizable", "transition", fallback: "Transition")
|
||||||
/// Translator
|
/// Translator
|
||||||
internal static let translator = L10n.tr("Localizable", "translator", fallback: "Translator")
|
internal static let translator = L10n.tr("Localizable", "translator", fallback: "Translator")
|
||||||
/// Trigger already exists
|
/// Trigger already exits
|
||||||
internal static let triggerAlreadyExists = L10n.tr("Localizable", "triggerAlreadyExists", fallback: "Trigger already exists")
|
internal static let triggerAlreadyExists = L10n.tr("Localizable", "triggerAlreadyExists", fallback: "Trigger already exits")
|
||||||
/// Triggers
|
/// Triggers
|
||||||
internal static let triggers = L10n.tr("Localizable", "triggers", fallback: "Triggers")
|
internal static let triggers = L10n.tr("Localizable", "triggers", fallback: "Triggers")
|
||||||
/// TV
|
/// TV
|
||||||
internal static let tv = L10n.tr("Localizable", "tv", fallback: "TV")
|
internal static let tv = L10n.tr("Localizable", "tv", fallback: "TV")
|
||||||
/// TV Access
|
/// TV Access
|
||||||
internal static let tvAccess = L10n.tr("Localizable", "tvAccess", fallback: "TV Access")
|
internal static let tvAccess = L10n.tr("Localizable", "tvAccess", fallback: "TV Access")
|
||||||
|
/// TV channel
|
||||||
|
internal static let tvChannel = L10n.tr("Localizable", "tvChannel", fallback: "TV channel")
|
||||||
|
/// TV channels
|
||||||
|
internal static let tvChannels = L10n.tr("Localizable", "tvChannels", fallback: "TV channels")
|
||||||
|
/// TV program
|
||||||
|
internal static let tvProgram = L10n.tr("Localizable", "tvProgram", fallback: "TV program")
|
||||||
|
/// TV programs
|
||||||
|
internal static let tvPrograms = L10n.tr("Localizable", "tvPrograms", fallback: "TV programs")
|
||||||
/// TV Shows
|
/// TV Shows
|
||||||
internal static let tvShows = L10n.tr("Localizable", "tvShows", fallback: "TV Shows")
|
internal static let tvShows = L10n.tr("Localizable", "tvShows", fallback: "TV Shows")
|
||||||
/// Type
|
/// Type
|
||||||
|
@ -1574,8 +1664,16 @@ internal enum L10n {
|
||||||
internal static func userRequiresDeviceAuthentication(_ p1: Any) -> String {
|
internal static func userRequiresDeviceAuthentication(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "userRequiresDeviceAuthentication", String(describing: p1), fallback: "User %@ requires device authentication")
|
return L10n.tr("Localizable", "userRequiresDeviceAuthentication", String(describing: p1), fallback: "User %@ requires device authentication")
|
||||||
}
|
}
|
||||||
|
/// User root folder
|
||||||
|
internal static let userRootFolder = L10n.tr("Localizable", "userRootFolder", fallback: "User root folder")
|
||||||
|
/// User root folders
|
||||||
|
internal static let userRootFolders = L10n.tr("Localizable", "userRootFolders", fallback: "User root folders")
|
||||||
/// Users
|
/// Users
|
||||||
internal static let users = L10n.tr("Localizable", "users", fallback: "Users")
|
internal static let users = L10n.tr("Localizable", "users", fallback: "Users")
|
||||||
|
/// User view
|
||||||
|
internal static let userView = L10n.tr("Localizable", "userView", fallback: "User view")
|
||||||
|
/// User views
|
||||||
|
internal static let userViews = L10n.tr("Localizable", "userViews", fallback: "User views")
|
||||||
/// Use splashscreen
|
/// Use splashscreen
|
||||||
internal static let useSplashscreen = L10n.tr("Localizable", "useSplashscreen", fallback: "Use splashscreen")
|
internal static let useSplashscreen = L10n.tr("Localizable", "useSplashscreen", fallback: "Use splashscreen")
|
||||||
/// Version
|
/// Version
|
||||||
|
@ -1608,6 +1706,8 @@ internal enum L10n {
|
||||||
internal static let videoRemuxing = L10n.tr("Localizable", "videoRemuxing", fallback: "Video remuxing")
|
internal static let videoRemuxing = L10n.tr("Localizable", "videoRemuxing", fallback: "Video remuxing")
|
||||||
/// The video resolution is not supported
|
/// The video resolution is not supported
|
||||||
internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported")
|
internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported")
|
||||||
|
/// Videos
|
||||||
|
internal static let videos = L10n.tr("Localizable", "videos", fallback: "Videos")
|
||||||
/// Video transcoding
|
/// Video transcoding
|
||||||
internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding")
|
internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding")
|
||||||
/// Some views may need an app restart to update.
|
/// Some views may need an app restart to update.
|
||||||
|
|
|
@ -9,11 +9,20 @@
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import OrderedCollections
|
||||||
|
|
||||||
final class CollectionItemViewModel: ItemViewModel {
|
final class CollectionItemViewModel: ItemViewModel {
|
||||||
|
|
||||||
|
// MARK: - Published Collection Items
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
private(set) var collectionItems: [BaseItemDto] = []
|
private(set) var collectionItems: OrderedDictionary<BaseItemKind, [BaseItemDto]> = [:]
|
||||||
|
|
||||||
|
override var presentPlayButton: Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - On Refresh
|
||||||
|
|
||||||
override func onRefresh() async throws {
|
override func onRefresh() async throws {
|
||||||
let collectionItems = try await self.getCollectionItems()
|
let collectionItems = try await self.getCollectionItems()
|
||||||
|
@ -23,10 +32,13 @@ final class CollectionItemViewModel: ItemViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getCollectionItems() async throws -> [BaseItemDto] {
|
// MARK: - Get Collection Items
|
||||||
|
|
||||||
|
private func getCollectionItems() async throws -> OrderedDictionary<BaseItemKind, [BaseItemDto]> {
|
||||||
var parameters = Paths.GetItemsByUserIDParameters()
|
var parameters = Paths.GetItemsByUserIDParameters()
|
||||||
parameters.fields = .MinimumFields
|
parameters.fields = .MinimumFields
|
||||||
|
parameters.includeItemTypes = BaseItemKind.supportedCases
|
||||||
|
.appending(.episode)
|
||||||
parameters.parentID = item.id
|
parameters.parentID = item.id
|
||||||
|
|
||||||
let request = Paths.getItemsByUserID(
|
let request = Paths.getItemsByUserID(
|
||||||
|
@ -35,6 +47,15 @@ final class CollectionItemViewModel: ItemViewModel {
|
||||||
)
|
)
|
||||||
let response = try await userSession.client.send(request)
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
return response.value.items ?? []
|
let items = response.value.items ?? []
|
||||||
|
|
||||||
|
let result = OrderedDictionary<BaseItemKind?, [BaseItemDto]>(
|
||||||
|
grouping: items,
|
||||||
|
by: \.type
|
||||||
|
)
|
||||||
|
.compactKeys()
|
||||||
|
.sortedKeys { $0.rawValue < $1.rawValue }
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,8 @@ class ItemViewModel: ViewModel, Stateful {
|
||||||
@Published
|
@Published
|
||||||
var state: State = .initial
|
var state: State = .initial
|
||||||
|
|
||||||
|
var presentPlayButton: Bool { true }
|
||||||
|
|
||||||
// tasks
|
// tasks
|
||||||
|
|
||||||
private var toggleIsFavoriteTask: AnyCancellable?
|
private var toggleIsFavoriteTask: AnyCancellable?
|
||||||
|
@ -288,6 +290,7 @@ class ItemViewModel: ViewModel, Stateful {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "Override the `respond` method instead and return `super.respond(to:)`")
|
||||||
func onRefresh() async throws {}
|
func onRefresh() async throws {}
|
||||||
|
|
||||||
private func getFullItem() async throws -> BaseItemDto {
|
private func getFullItem() async throws -> BaseItemDto {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension CollectionItemView {
|
extension CollectionItemView {
|
||||||
|
@ -25,17 +26,23 @@ extension CollectionItemView {
|
||||||
.frame(height: UIScreen.main.bounds.height - 150)
|
.frame(height: UIScreen.main.bounds.height - 150)
|
||||||
.padding(.bottom, 50)
|
.padding(.bottom, 50)
|
||||||
|
|
||||||
if viewModel.collectionItems.isNotEmpty {
|
ForEach(viewModel.collectionItems.elements, id: \.key) { element in
|
||||||
PosterHStack(
|
if element.value.isNotEmpty {
|
||||||
title: L10n.items,
|
PosterHStack(
|
||||||
type: .portrait,
|
title: element.key.pluralDisplayTitle,
|
||||||
items: viewModel.collectionItems
|
type: .portrait,
|
||||||
)
|
items: element.value
|
||||||
.onSelect { item in
|
)
|
||||||
router.route(to: \.item, item)
|
.onSelect { item in
|
||||||
|
router.route(to: \.item, item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if viewModel.similarItems.isNotEmpty {
|
||||||
|
ItemView.SimilarItemsHStack(items: viewModel.similarItems)
|
||||||
|
}
|
||||||
|
|
||||||
ItemView.AboutView(viewModel: viewModel)
|
ItemView.AboutView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
.background {
|
.background {
|
||||||
|
|
|
@ -24,6 +24,7 @@ extension ItemView {
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(alignment: .leading)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.foregroundStyle(Color(UIColor.darkGray))
|
.foregroundStyle(Color(UIColor.darkGray))
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,8 +119,10 @@ extension ItemView {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
ItemView.PlayButton(viewModel: viewModel)
|
if viewModel.presentPlayButton {
|
||||||
.focused($focusedLayer, equals: .playButton)
|
ItemView.PlayButton(viewModel: viewModel)
|
||||||
|
.focused($focusedLayer, equals: .playButton)
|
||||||
|
}
|
||||||
|
|
||||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||||
.frame(width: 440)
|
.frame(width: 440)
|
||||||
|
|
|
@ -88,11 +88,11 @@ struct ServerUserLiveTVAccessView: View {
|
||||||
List {
|
List {
|
||||||
Section(L10n.access) {
|
Section(L10n.access) {
|
||||||
Toggle(
|
Toggle(
|
||||||
L10n.liveTvAccess,
|
L10n.liveTVAccess,
|
||||||
isOn: $tempPolicy.enableLiveTvAccess.coalesce(false)
|
isOn: $tempPolicy.enableLiveTvAccess.coalesce(false)
|
||||||
)
|
)
|
||||||
Toggle(
|
Toggle(
|
||||||
L10n.liveTvRecordingManagement,
|
L10n.liveTVRecordingManagement,
|
||||||
isOn: $tempPolicy.enableLiveTvManagement.coalesce(false)
|
isOn: $tempPolicy.enableLiveTvManagement.coalesce(false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,152 +10,141 @@ import SwiftUI
|
||||||
import WrappingHStack
|
import WrappingHStack
|
||||||
|
|
||||||
extension ItemView {
|
extension ItemView {
|
||||||
|
|
||||||
struct AttributesHStack: View {
|
struct AttributesHStack: View {
|
||||||
@ObservedObject
|
|
||||||
var viewModel: ItemViewModel
|
|
||||||
|
|
||||||
@StoredValue(.User.itemViewAttributes)
|
@StoredValue(.User.itemViewAttributes)
|
||||||
private var itemViewAttributes
|
private var itemViewAttributes
|
||||||
|
|
||||||
// MARK: - Body
|
@ObservedObject
|
||||||
|
private var viewModel: ItemViewModel
|
||||||
|
|
||||||
|
private let alignment: HorizontalAlignment
|
||||||
|
|
||||||
|
init(
|
||||||
|
viewModel: ItemViewModel,
|
||||||
|
alignment: HorizontalAlignment = .center
|
||||||
|
) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.alignment = alignment
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let badges = computeBadges()
|
let badges = computeBadges()
|
||||||
if !badges.isEmpty {
|
|
||||||
WrappingHStack(badges, id: \.self, alignment: .center, spacing: .constant(8), lineSpacing: 8) { badgeItem in
|
if badges.isNotEmpty {
|
||||||
|
WrappingHStack(
|
||||||
|
badges,
|
||||||
|
id: \.self,
|
||||||
|
alignment: alignment,
|
||||||
|
spacing: .constant(8),
|
||||||
|
lineSpacing: 8
|
||||||
|
) { badgeItem in
|
||||||
badgeItem
|
badgeItem
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
}
|
}
|
||||||
.foregroundStyle(Color(UIColor.darkGray))
|
.foregroundStyle(Color(UIColor.darkGray))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.frame(maxWidth: 300)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Compute Badges
|
// MARK: - Compute Badges
|
||||||
|
|
||||||
private func computeBadges() -> [AnyView] {
|
private func computeBadges() -> [AttributeBadge] {
|
||||||
var badges: [AnyView] = []
|
var badges: [AttributeBadge] = []
|
||||||
var processedGroups = Set<ItemViewAttribute>()
|
|
||||||
|
|
||||||
for attribute in itemViewAttributes {
|
for attribute in itemViewAttributes {
|
||||||
|
|
||||||
if processedGroups.contains(attribute) { continue }
|
var badge: AttributeBadge? = nil
|
||||||
processedGroups.insert(attribute)
|
|
||||||
|
|
||||||
switch attribute {
|
switch attribute {
|
||||||
case .ratingCritics:
|
case .ratingCritics:
|
||||||
if let criticRating = viewModel.item.criticRating {
|
if let criticRating = viewModel.item.criticRating {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .outline,
|
||||||
style: .outline,
|
title: Text("\(criticRating, specifier: "%.0f")")
|
||||||
title: Text("\(criticRating, specifier: "%.0f")")
|
) {
|
||||||
) {
|
if criticRating >= 60 {
|
||||||
if criticRating >= 60 {
|
Image(.tomatoFresh)
|
||||||
Image(.tomatoFresh)
|
.symbolRenderingMode(.hierarchical)
|
||||||
.symbolRenderingMode(.hierarchical)
|
} else {
|
||||||
} else {
|
Image(.tomatoRotten)
|
||||||
Image(.tomatoRotten)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
case .ratingCommunity:
|
case .ratingCommunity:
|
||||||
if let communityRating = viewModel.item.communityRating {
|
if let communityRating = viewModel.item.communityRating {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .outline,
|
||||||
style: .outline,
|
title: Text("\(communityRating, specifier: "%.01f")"),
|
||||||
title: Text("\(communityRating, specifier: "%.01f")"),
|
systemName: "star.fill"
|
||||||
systemName: "star.fill"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
case .ratingOfficial:
|
case .ratingOfficial:
|
||||||
if let officialRating = viewModel.item.officialRating {
|
if let officialRating = viewModel.item.officialRating {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .outline,
|
||||||
style: .outline,
|
title: officialRating
|
||||||
title: officialRating
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
case .videoQuality:
|
case .videoQuality:
|
||||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {
|
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {
|
||||||
// Resolution badge (if available). Only one of 4K or HD is shown.
|
// Resolution badge (if available). Only one of 4K or HD is shown.
|
||||||
if mediaStreams.has4KVideo {
|
if mediaStreams.has4KVideo {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .fill,
|
||||||
style: .fill,
|
title: "4K"
|
||||||
title: "4K"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
} else if mediaStreams.hasHDVideo {
|
} else if mediaStreams.hasHDVideo {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .fill,
|
||||||
style: .fill,
|
title: "HD"
|
||||||
title: "HD"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
if mediaStreams.hasDolbyVision {
|
if mediaStreams.hasDolbyVision {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .fill,
|
||||||
style: .fill,
|
title: "DV"
|
||||||
title: "DV"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
if mediaStreams.hasHDRVideo {
|
if mediaStreams.hasHDRVideo {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .fill,
|
||||||
style: .fill,
|
title: "HDR"
|
||||||
title: "HDR"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .audioChannels:
|
case .audioChannels:
|
||||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {
|
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {
|
||||||
if mediaStreams.has51AudioChannelLayout {
|
if mediaStreams.has51AudioChannelLayout {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .fill,
|
||||||
style: .fill,
|
title: "5.1"
|
||||||
title: "5.1"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
if mediaStreams.has71AudioChannelLayout {
|
if mediaStreams.has71AudioChannelLayout {
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .fill,
|
||||||
style: .fill,
|
title: "7.1"
|
||||||
title: "7.1"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .subtitles:
|
case .subtitles:
|
||||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams,
|
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams,
|
||||||
mediaStreams.hasSubtitles
|
mediaStreams.hasSubtitles
|
||||||
{
|
{
|
||||||
let badge = AnyView(
|
badge = AttributeBadge(
|
||||||
AttributeBadge(
|
style: .outline,
|
||||||
style: .outline,
|
title: "CC"
|
||||||
title: "CC"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
badges.append(badge)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let badge {
|
||||||
|
badges.append(badge)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return badges
|
return badges
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,46 +23,34 @@ extension CollectionItemView {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
|
||||||
VStack(alignment: .center) {
|
// MARK: Items
|
||||||
ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 600))
|
|
||||||
.placeholder { source in
|
|
||||||
if let blurHash = source.blurHash {
|
|
||||||
BlurHashView(blurHash: blurHash, size: .Square(length: 8))
|
|
||||||
} else {
|
|
||||||
Color.secondarySystemFill
|
|
||||||
.opacity(0.75)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.failure {
|
|
||||||
SystemImageContentView(systemName: viewModel.item.systemImage)
|
|
||||||
}
|
|
||||||
.posterStyle(.landscape, contentMode: .fill)
|
|
||||||
.frame(maxHeight: 300)
|
|
||||||
.posterShadow()
|
|
||||||
.edgePadding(.horizontal)
|
|
||||||
|
|
||||||
Text(viewModel.item.displayTitle)
|
ForEach(viewModel.collectionItems.elements, id: \.key) { element in
|
||||||
.font(.title2)
|
if element.value.isNotEmpty {
|
||||||
.fontWeight(.bold)
|
PosterHStack(
|
||||||
.multilineTextAlignment(.center)
|
title: element.key.pluralDisplayTitle,
|
||||||
.lineLimit(2)
|
type: .portrait,
|
||||||
.padding(.horizontal)
|
items: element.value
|
||||||
|
)
|
||||||
|
.trailing {
|
||||||
|
SeeAllButton()
|
||||||
|
.onSelect {
|
||||||
|
let viewModel = ItemLibraryViewModel(
|
||||||
|
title: viewModel.item.displayTitle,
|
||||||
|
id: viewModel.item.id,
|
||||||
|
element.value
|
||||||
|
)
|
||||||
|
router.route(to: \.library, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onSelect { item in
|
||||||
|
router.route(to: \.item, item)
|
||||||
|
}
|
||||||
|
|
||||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
RowDivider()
|
||||||
.font(.title)
|
}
|
||||||
.frame(maxWidth: 300)
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Overview
|
|
||||||
|
|
||||||
ItemView.OverviewView(item: viewModel.item)
|
|
||||||
.overviewLineLimit(4)
|
|
||||||
.taglineLineLimit(2)
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
RowDivider()
|
|
||||||
|
|
||||||
// MARK: Genres
|
// MARK: Genres
|
||||||
|
|
||||||
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
|
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
|
||||||
|
@ -79,29 +67,15 @@ extension CollectionItemView {
|
||||||
RowDivider()
|
RowDivider()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Items
|
// MARK: Similar
|
||||||
|
|
||||||
if viewModel.collectionItems.isNotEmpty {
|
if viewModel.similarItems.isNotEmpty {
|
||||||
PosterHStack(
|
ItemView.SimilarItemsHStack(items: viewModel.similarItems)
|
||||||
title: L10n.items,
|
|
||||||
type: .portrait,
|
RowDivider()
|
||||||
items: viewModel.collectionItems
|
|
||||||
)
|
|
||||||
.trailing {
|
|
||||||
SeeAllButton()
|
|
||||||
.onSelect {
|
|
||||||
let viewModel = ItemLibraryViewModel(
|
|
||||||
title: viewModel.item.displayTitle,
|
|
||||||
id: viewModel.item.id,
|
|
||||||
viewModel.collectionItems
|
|
||||||
)
|
|
||||||
router.route(to: \.library, viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onSelect { item in
|
|
||||||
router.route(to: \.item, item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ItemView.AboutView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,26 @@ import SwiftUI
|
||||||
|
|
||||||
struct CollectionItemView: View {
|
struct CollectionItemView: View {
|
||||||
|
|
||||||
|
@Default(.Customization.itemViewType)
|
||||||
|
private var itemViewType
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: CollectionItemViewModel
|
var viewModel: CollectionItemViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
switch itemViewType {
|
||||||
ContentView(viewModel: viewModel)
|
case .compactPoster:
|
||||||
.edgePadding(.bottom)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ extension EpisodeItemView.ContentView {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
ItemView.AttributesHStack(viewModel: viewModel)
|
ItemView.AttributesHStack(viewModel: viewModel, alignment: .center)
|
||||||
|
|
||||||
ItemView.PlayButton(viewModel: viewModel)
|
ItemView.PlayButton(viewModel: viewModel)
|
||||||
.frame(maxWidth: 300)
|
.frame(maxWidth: 300)
|
||||||
|
|
|
@ -23,8 +23,6 @@ extension ItemView {
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: ItemViewModel
|
var viewModel: ItemViewModel
|
||||||
|
|
||||||
@State
|
|
||||||
private var scrollViewOffset: CGFloat = 0
|
|
||||||
@State
|
@State
|
||||||
private var blurHashBottomEdgeColor: Color = .secondarySystemFill
|
private var blurHashBottomEdgeColor: Color = .secondarySystemFill
|
||||||
|
|
||||||
|
@ -132,9 +130,11 @@ extension ItemView.CinematicScrollView {
|
||||||
.foregroundColor(Color(UIColor.lightGray))
|
.foregroundColor(Color(UIColor.lightGray))
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
ItemView.PlayButton(viewModel: viewModel)
|
if viewModel.presentPlayButton {
|
||||||
.frame(maxWidth: 300)
|
ItemView.PlayButton(viewModel: viewModel)
|
||||||
.frame(height: 50)
|
.frame(maxWidth: 300)
|
||||||
|
.frame(height: 50)
|
||||||
|
}
|
||||||
|
|
||||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||||
.font(.title)
|
.font(.title)
|
||||||
|
@ -148,7 +148,7 @@ extension ItemView.CinematicScrollView {
|
||||||
.taglineLineLimit(2)
|
.taglineLineLimit(2)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
ItemView.AttributesHStack(viewModel: viewModel)
|
ItemView.AttributesHStack(viewModel: viewModel, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,13 +131,13 @@ extension ItemView.CompactLogoScrollView {
|
||||||
|
|
||||||
ItemView.AttributesHStack(viewModel: viewModel)
|
ItemView.AttributesHStack(viewModel: viewModel)
|
||||||
|
|
||||||
ItemView.PlayButton(viewModel: viewModel)
|
if viewModel.presentPlayButton {
|
||||||
.frame(maxWidth: 300)
|
ItemView.PlayButton(viewModel: viewModel)
|
||||||
.frame(height: 50)
|
.frame(height: 50)
|
||||||
|
}
|
||||||
|
|
||||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.frame(maxWidth: 300)
|
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .bottom)
|
.frame(maxWidth: .infinity, alignment: .bottom)
|
||||||
|
|
|
@ -133,7 +133,7 @@ extension ItemView.CompactPosterScrollView {
|
||||||
.font(.subheadline.weight(.medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(Color(UIColor.lightGray))
|
.foregroundColor(Color(UIColor.lightGray))
|
||||||
|
|
||||||
ItemView.AttributesHStack(viewModel: viewModel)
|
ItemView.AttributesHStack(viewModel: viewModel, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,8 +155,10 @@ extension ItemView.CompactPosterScrollView {
|
||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
|
|
||||||
ItemView.PlayButton(viewModel: viewModel)
|
if viewModel.presentPlayButton {
|
||||||
.frame(width: 130, height: 40)
|
ItemView.PlayButton(viewModel: viewModel)
|
||||||
|
.frame(width: 130, height: 40)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,34 @@ extension iPadOSCollectionItemView {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
|
||||||
|
// MARK: Items
|
||||||
|
|
||||||
|
ForEach(viewModel.collectionItems.elements, id: \.key) { element in
|
||||||
|
if element.value.isNotEmpty {
|
||||||
|
PosterHStack(
|
||||||
|
title: element.key.pluralDisplayTitle,
|
||||||
|
type: .portrait,
|
||||||
|
items: element.value
|
||||||
|
)
|
||||||
|
.trailing {
|
||||||
|
SeeAllButton()
|
||||||
|
.onSelect {
|
||||||
|
let viewModel = ItemLibraryViewModel(
|
||||||
|
title: viewModel.item.displayTitle,
|
||||||
|
id: viewModel.item.id,
|
||||||
|
element.value
|
||||||
|
)
|
||||||
|
router.route(to: \.library, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onSelect { item in
|
||||||
|
router.route(to: \.item, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Genres
|
// MARK: Genres
|
||||||
|
|
||||||
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
|
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
|
||||||
|
@ -38,17 +66,12 @@ extension iPadOSCollectionItemView {
|
||||||
RowDivider()
|
RowDivider()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Items
|
// MARK: Similar
|
||||||
|
|
||||||
if viewModel.collectionItems.isNotEmpty {
|
if viewModel.similarItems.isNotEmpty {
|
||||||
PosterHStack(
|
ItemView.SimilarItemsHStack(items: viewModel.similarItems)
|
||||||
title: L10n.items,
|
|
||||||
type: .portrait,
|
RowDivider()
|
||||||
items: viewModel.collectionItems
|
|
||||||
)
|
|
||||||
.onSelect { item in
|
|
||||||
router.route(to: \.item, item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemView.AboutView(viewModel: viewModel)
|
ItemView.AboutView(viewModel: viewModel)
|
||||||
|
|
|
@ -110,8 +110,6 @@ extension ItemView.iPadOSCinematicScrollView {
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
HStack(spacing: 30) {
|
HStack(spacing: 30) {
|
||||||
ItemView.AttributesHStack(viewModel: viewModel)
|
|
||||||
|
|
||||||
DotHStack {
|
DotHStack {
|
||||||
if let firstGenre = viewModel.item.genres?.first {
|
if let firstGenre = viewModel.item.genres?.first {
|
||||||
Text(firstGenre)
|
Text(firstGenre)
|
||||||
|
@ -127,28 +125,25 @@ extension ItemView.iPadOSCinematicScrollView {
|
||||||
}
|
}
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(Color(UIColor.lightGray))
|
.foregroundColor(Color(UIColor.lightGray))
|
||||||
|
|
||||||
|
ItemView.AttributesHStack(viewModel: viewModel, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.trailing, 200)
|
.padding(.trailing, 200)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// TODO: remove when/if collections have a different view
|
VStack(spacing: 10) {
|
||||||
|
if viewModel.presentPlayButton {
|
||||||
if !(viewModel is CollectionItemViewModel) {
|
|
||||||
VStack(spacing: 10) {
|
|
||||||
ItemView.PlayButton(viewModel: viewModel)
|
ItemView.PlayButton(viewModel: viewModel)
|
||||||
.frame(height: 50)
|
.frame(height: 50)
|
||||||
|
|
||||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
|
||||||
.font(.title)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
}
|
}
|
||||||
.frame(width: 250)
|
|
||||||
} else {
|
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||||
Color.clear
|
.font(.title)
|
||||||
.frame(width: 250)
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
|
.frame(width: 250)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue