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
|
||||
|
||||
struct AttributeBadge<Content: View>: View {
|
||||
struct AttributeBadge: View {
|
||||
|
||||
@Environment(\.font)
|
||||
private var font
|
||||
|
@ -19,7 +19,7 @@ struct AttributeBadge<Content: View>: View {
|
|||
}
|
||||
|
||||
private let style: AttributeStyle
|
||||
private let content: () -> Content
|
||||
private let content: () -> any View
|
||||
|
||||
private var usedFont: Font {
|
||||
font ?? .caption.weight(.semibold)
|
||||
|
@ -29,6 +29,7 @@ struct AttributeBadge<Content: View>: View {
|
|||
private var innerBody: some View {
|
||||
if style == .fill {
|
||||
content()
|
||||
.eraseToAnyView()
|
||||
.padding(.init(vertical: 1, horizontal: 4))
|
||||
.hidden()
|
||||
.background {
|
||||
|
@ -36,11 +37,13 @@ struct AttributeBadge<Content: View>: View {
|
|||
.cornerRadius(2)
|
||||
.inverseMask {
|
||||
content()
|
||||
.eraseToAnyView()
|
||||
.padding(.init(vertical: 1, horizontal: 4))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
.eraseToAnyView()
|
||||
.foregroundStyle(Color(UIColor.lightGray))
|
||||
.padding(.init(vertical: 1, horizontal: 4))
|
||||
.overlay(
|
||||
|
@ -57,7 +60,7 @@ struct AttributeBadge<Content: View>: View {
|
|||
}
|
||||
}
|
||||
|
||||
extension AttributeBadge where Content == Text {
|
||||
extension AttributeBadge {
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
|
@ -76,9 +79,6 @@ extension AttributeBadge where Content == Text {
|
|||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AttributeBadge where Content == Label<Text, Image> {
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
|
|
|
@ -24,14 +24,171 @@ extension BaseItemKind: SupportedCaseIterable {
|
|||
|
||||
extension BaseItemKind: ItemFilter {
|
||||
|
||||
// TODO: localize
|
||||
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 {
|
||||
|
||||
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] {
|
||||
[.boxSet, .movie, .person, .series]
|
||||
}
|
||||
|
|
|
@ -13,4 +13,21 @@ extension OrderedDictionary {
|
|||
var isNotEmpty: Bool {
|
||||
!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 {
|
||||
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
|
||||
internal static let aired = L10n.tr("Localizable", "aired", fallback: "Aired")
|
||||
/// Aired episode order
|
||||
|
@ -84,6 +88,10 @@ internal enum L10n {
|
|||
internal static let album = L10n.tr("Localizable", "album", fallback: "Album")
|
||||
/// 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
|
||||
internal static let all = L10n.tr("Localizable", "all", fallback: "All")
|
||||
/// All Audiences
|
||||
|
@ -128,6 +136,8 @@ internal enum L10n {
|
|||
internal static let art = L10n.tr("Localizable", "art", fallback: "Art")
|
||||
/// Artist
|
||||
internal static let artist = L10n.tr("Localizable", "artist", fallback: "Artist")
|
||||
/// Artists
|
||||
internal static let artists = L10n.tr("Localizable", "artists", fallback: "Artists")
|
||||
/// Ascending
|
||||
internal static let ascending = L10n.tr("Localizable", "ascending", fallback: "Ascending")
|
||||
/// 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")
|
||||
/// 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
|
||||
internal static let audioChannelsNotSupported = L10n.tr("Localizable", "audioChannelsNotSupported", fallback: "The number of audio channels 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")
|
||||
/// 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
|
||||
internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "Behavior")
|
||||
/// 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.")
|
||||
/// Blue
|
||||
internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue")
|
||||
/// Book
|
||||
internal static let book = L10n.tr("Localizable", "book", fallback: "Book")
|
||||
/// Books
|
||||
internal static let books = L10n.tr("Localizable", "books", fallback: "Books")
|
||||
/// Box
|
||||
|
@ -262,8 +282,16 @@ internal enum L10n {
|
|||
internal static let category = L10n.tr("Localizable", "category", fallback: "Category")
|
||||
/// 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
|
||||
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
|
||||
internal static let channels = L10n.tr("Localizable", "channels", fallback: "Channels")
|
||||
/// Chapter
|
||||
|
@ -282,6 +310,12 @@ internal enum L10n {
|
|||
internal static let clip = L10n.tr("Localizable", "clip", fallback: "Clip")
|
||||
/// 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
|
||||
internal static let collections = L10n.tr("Localizable", "collections", fallback: "Collections")
|
||||
/// Color
|
||||
|
@ -658,6 +692,8 @@ internal enum L10n {
|
|||
internal static let findMissingDescription = L10n.tr("Localizable", "findMissingDescription", fallback: "Find missing metadata and images.")
|
||||
/// 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
|
||||
internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding")
|
||||
/// Format
|
||||
|
@ -668,6 +704,8 @@ internal enum L10n {
|
|||
internal static let fullSideBySide = L10n.tr("Localizable", "fullSideBySide", fallback: "Full Side-by-Side")
|
||||
/// 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
|
||||
internal static let genres = L10n.tr("Localizable", "genres", fallback: "Genres")
|
||||
/// 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")
|
||||
/// 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
|
||||
internal static let `left` = L10n.tr("Localizable", "left", fallback: "Left")
|
||||
/// Left vertical pan
|
||||
|
@ -821,11 +861,17 @@ internal enum L10n {
|
|||
/// Live TV
|
||||
internal static let liveTV = L10n.tr("Localizable", "liveTV", fallback: "Live TV")
|
||||
/// Live TV access
|
||||
internal static let liveTvAccess = L10n.tr("Localizable", "liveTvAccess", fallback: "Live TV access")
|
||||
/// Live TV Channels
|
||||
internal static let liveTVChannels = L10n.tr("Localizable", "liveTVChannels", fallback: "Live TV Channels")
|
||||
/// Live TV Programs
|
||||
internal static let liveTVPrograms = L10n.tr("Localizable", "liveTVPrograms", fallback: "Live TV Programs")
|
||||
internal static let liveTVAccess = L10n.tr("Localizable", "liveTVAccess", fallback: "Live TV access")
|
||||
/// Live TV channel
|
||||
internal static let liveTVChannel = L10n.tr("Localizable", "liveTVChannel", fallback: "Live TV channel")
|
||||
/// Live TV channels
|
||||
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
|
||||
internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management")
|
||||
/// Loading user failed
|
||||
|
@ -854,6 +900,10 @@ internal enum L10n {
|
|||
internal static let lyrics = L10n.tr("Localizable", "lyrics", fallback: "Lyrics")
|
||||
/// 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
|
||||
internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate")
|
||||
/// 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")
|
||||
/// Mixer
|
||||
internal static let mixer = L10n.tr("Localizable", "mixer", fallback: "Mixer")
|
||||
/// Movie
|
||||
internal static let movie = L10n.tr("Localizable", "movie", fallback: "Movie")
|
||||
/// Movies
|
||||
internal static let movies = L10n.tr("Localizable", "movies", fallback: "Movies")
|
||||
/// Multi tap
|
||||
internal static let multiTap = L10n.tr("Localizable", "multiTap", fallback: "Multi tap")
|
||||
/// 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
|
||||
internal static let mvc = L10n.tr("Localizable", "mvc", fallback: "MVC")
|
||||
/// 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.")
|
||||
/// 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
|
||||
internal static let pin = L10n.tr("Localizable", "pin", fallback: "Pin")
|
||||
/// Pinch
|
||||
|
@ -1048,6 +1114,14 @@ internal enum L10n {
|
|||
internal static let played = L10n.tr("Localizable", "played", fallback: "Played")
|
||||
/// 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
|
||||
internal static let playNextItem = L10n.tr("Localizable", "playNextItem", fallback: "Play Next Item")
|
||||
/// Play on active
|
||||
|
@ -1082,6 +1156,8 @@ internal enum L10n {
|
|||
internal static let profileNotSaved = L10n.tr("Localizable", "profileNotSaved", fallback: "Profile not saved")
|
||||
/// Profiles
|
||||
internal static let profiles = L10n.tr("Localizable", "profiles", fallback: "Profiles")
|
||||
/// Program
|
||||
internal static let program = L10n.tr("Localizable", "program", fallback: "Program")
|
||||
/// Programs
|
||||
internal static let programs = L10n.tr("Localizable", "programs", fallback: "Programs")
|
||||
/// Progress
|
||||
|
@ -1122,6 +1198,10 @@ internal enum L10n {
|
|||
internal static let recentlyAdded = L10n.tr("Localizable", "recentlyAdded", fallback: "Recently Added")
|
||||
/// 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
|
||||
internal static let red = L10n.tr("Localizable", "red", fallback: "Red")
|
||||
/// The number of reference frames is not supported
|
||||
|
@ -1260,6 +1340,8 @@ internal enum L10n {
|
|||
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")
|
||||
/// Secondary audio is not supported
|
||||
internal static let secondaryAudioNotSupported = L10n.tr("Localizable", "secondaryAudioNotSupported", fallback: "Secondary audio is not supported")
|
||||
/// Security
|
||||
|
@ -1500,14 +1582,22 @@ internal enum L10n {
|
|||
internal static let transition = L10n.tr("Localizable", "transition", fallback: "Transition")
|
||||
/// Translator
|
||||
internal static let translator = L10n.tr("Localizable", "translator", fallback: "Translator")
|
||||
/// Trigger already exists
|
||||
internal static let triggerAlreadyExists = L10n.tr("Localizable", "triggerAlreadyExists", fallback: "Trigger already exists")
|
||||
/// Trigger already exits
|
||||
internal static let triggerAlreadyExists = L10n.tr("Localizable", "triggerAlreadyExists", fallback: "Trigger already exits")
|
||||
/// Triggers
|
||||
internal static let triggers = L10n.tr("Localizable", "triggers", fallback: "Triggers")
|
||||
/// TV
|
||||
internal static let tv = L10n.tr("Localizable", "tv", fallback: "TV")
|
||||
/// 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
|
||||
internal static let tvShows = L10n.tr("Localizable", "tvShows", fallback: "TV Shows")
|
||||
/// Type
|
||||
|
@ -1574,8 +1664,16 @@ internal enum L10n {
|
|||
internal static func userRequiresDeviceAuthentication(_ p1: Any) -> String {
|
||||
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
|
||||
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
|
||||
internal static let useSplashscreen = L10n.tr("Localizable", "useSplashscreen", fallback: "Use splashscreen")
|
||||
/// Version
|
||||
|
@ -1608,6 +1706,8 @@ internal enum L10n {
|
|||
internal static let videoRemuxing = L10n.tr("Localizable", "videoRemuxing", fallback: "Video remuxing")
|
||||
/// 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
|
||||
internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding")
|
||||
/// Some views may need an app restart to update.
|
||||
|
|
|
@ -9,11 +9,20 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import OrderedCollections
|
||||
|
||||
final class CollectionItemViewModel: ItemViewModel {
|
||||
|
||||
// MARK: - Published Collection Items
|
||||
|
||||
@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 {
|
||||
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()
|
||||
parameters.fields = .MinimumFields
|
||||
parameters.includeItemTypes = BaseItemKind.supportedCases
|
||||
.appending(.episode)
|
||||
parameters.parentID = item.id
|
||||
|
||||
let request = Paths.getItemsByUserID(
|
||||
|
@ -35,6 +47,15 @@ final class CollectionItemViewModel: ItemViewModel {
|
|||
)
|
||||
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
|
||||
var state: State = .initial
|
||||
|
||||
var presentPlayButton: Bool { true }
|
||||
|
||||
// tasks
|
||||
|
||||
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 {}
|
||||
|
||||
private func getFullItem() async throws -> BaseItemDto {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
extension CollectionItemView {
|
||||
|
@ -25,17 +26,23 @@ extension CollectionItemView {
|
|||
.frame(height: UIScreen.main.bounds.height - 150)
|
||||
.padding(.bottom, 50)
|
||||
|
||||
if viewModel.collectionItems.isNotEmpty {
|
||||
PosterHStack(
|
||||
title: L10n.items,
|
||||
type: .portrait,
|
||||
items: viewModel.collectionItems
|
||||
)
|
||||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
ForEach(viewModel.collectionItems.elements, id: \.key) { element in
|
||||
if element.value.isNotEmpty {
|
||||
PosterHStack(
|
||||
title: element.key.pluralDisplayTitle,
|
||||
type: .portrait,
|
||||
items: element.value
|
||||
)
|
||||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.similarItems.isNotEmpty {
|
||||
ItemView.SimilarItemsHStack(items: viewModel.similarItems)
|
||||
}
|
||||
|
||||
ItemView.AboutView(viewModel: viewModel)
|
||||
}
|
||||
.background {
|
||||
|
|
|
@ -24,6 +24,7 @@ extension ItemView {
|
|||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
}
|
||||
.frame(alignment: .leading)
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(Color(UIColor.darkGray))
|
||||
}
|
||||
|
|
|
@ -119,8 +119,10 @@ extension ItemView {
|
|||
Spacer()
|
||||
|
||||
VStack {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.focused($focusedLayer, equals: .playButton)
|
||||
if viewModel.presentPlayButton {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.focused($focusedLayer, equals: .playButton)
|
||||
}
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.frame(width: 440)
|
||||
|
|
|
@ -88,11 +88,11 @@ struct ServerUserLiveTVAccessView: View {
|
|||
List {
|
||||
Section(L10n.access) {
|
||||
Toggle(
|
||||
L10n.liveTvAccess,
|
||||
L10n.liveTVAccess,
|
||||
isOn: $tempPolicy.enableLiveTvAccess.coalesce(false)
|
||||
)
|
||||
Toggle(
|
||||
L10n.liveTvRecordingManagement,
|
||||
L10n.liveTVRecordingManagement,
|
||||
isOn: $tempPolicy.enableLiveTvManagement.coalesce(false)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,152 +10,141 @@ import SwiftUI
|
|||
import WrappingHStack
|
||||
|
||||
extension ItemView {
|
||||
|
||||
struct AttributesHStack: View {
|
||||
@ObservedObject
|
||||
var viewModel: ItemViewModel
|
||||
|
||||
@StoredValue(.User.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 {
|
||||
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
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
.foregroundStyle(Color(UIColor.darkGray))
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: 300)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Compute Badges
|
||||
|
||||
private func computeBadges() -> [AnyView] {
|
||||
var badges: [AnyView] = []
|
||||
var processedGroups = Set<ItemViewAttribute>()
|
||||
private func computeBadges() -> [AttributeBadge] {
|
||||
var badges: [AttributeBadge] = []
|
||||
|
||||
for attribute in itemViewAttributes {
|
||||
|
||||
if processedGroups.contains(attribute) { continue }
|
||||
processedGroups.insert(attribute)
|
||||
var badge: AttributeBadge? = nil
|
||||
|
||||
switch attribute {
|
||||
case .ratingCritics:
|
||||
if let criticRating = viewModel.item.criticRating {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: Text("\(criticRating, specifier: "%.0f")")
|
||||
) {
|
||||
if criticRating >= 60 {
|
||||
Image(.tomatoFresh)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else {
|
||||
Image(.tomatoRotten)
|
||||
}
|
||||
badge = AttributeBadge(
|
||||
style: .outline,
|
||||
title: Text("\(criticRating, specifier: "%.0f")")
|
||||
) {
|
||||
if criticRating >= 60 {
|
||||
Image(.tomatoFresh)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else {
|
||||
Image(.tomatoRotten)
|
||||
}
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
}
|
||||
case .ratingCommunity:
|
||||
if let communityRating = viewModel.item.communityRating {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: Text("\(communityRating, specifier: "%.01f")"),
|
||||
systemName: "star.fill"
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .outline,
|
||||
title: Text("\(communityRating, specifier: "%.01f")"),
|
||||
systemName: "star.fill"
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
case .ratingOfficial:
|
||||
if let officialRating = viewModel.item.officialRating {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: officialRating
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .outline,
|
||||
title: officialRating
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
case .videoQuality:
|
||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {
|
||||
// Resolution badge (if available). Only one of 4K or HD is shown.
|
||||
if mediaStreams.has4KVideo {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "4K"
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .fill,
|
||||
title: "4K"
|
||||
)
|
||||
badges.append(badge)
|
||||
} else if mediaStreams.hasHDVideo {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "HD"
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .fill,
|
||||
title: "HD"
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
if mediaStreams.hasDolbyVision {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "DV"
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .fill,
|
||||
title: "DV"
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
if mediaStreams.hasHDRVideo {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "HDR"
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .fill,
|
||||
title: "HDR"
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
}
|
||||
case .audioChannels:
|
||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {
|
||||
if mediaStreams.has51AudioChannelLayout {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "5.1"
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .fill,
|
||||
title: "5.1"
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
if mediaStreams.has71AudioChannelLayout {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "7.1"
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .fill,
|
||||
title: "7.1"
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
}
|
||||
case .subtitles:
|
||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams,
|
||||
mediaStreams.hasSubtitles
|
||||
{
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: "CC"
|
||||
)
|
||||
badge = AttributeBadge(
|
||||
style: .outline,
|
||||
title: "CC"
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
}
|
||||
|
||||
if let badge {
|
||||
badges.append(badge)
|
||||
}
|
||||
}
|
||||
|
||||
return badges
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,46 +23,34 @@ extension CollectionItemView {
|
|||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
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)
|
||||
// MARK: Items
|
||||
|
||||
Text(viewModel.item.displayTitle)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.padding(.horizontal)
|
||||
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)
|
||||
}
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
.frame(maxWidth: 300)
|
||||
.foregroundStyle(.primary)
|
||||
RowDivider()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Overview
|
||||
|
||||
ItemView.OverviewView(item: viewModel.item)
|
||||
.overviewLineLimit(4)
|
||||
.taglineLineLimit(2)
|
||||
.padding(.horizontal)
|
||||
|
||||
RowDivider()
|
||||
|
||||
// MARK: Genres
|
||||
|
||||
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
|
||||
|
@ -79,29 +67,15 @@ extension CollectionItemView {
|
|||
RowDivider()
|
||||
}
|
||||
|
||||
// MARK: Items
|
||||
// MARK: Similar
|
||||
|
||||
if viewModel.collectionItems.isNotEmpty {
|
||||
PosterHStack(
|
||||
title: L10n.items,
|
||||
type: .portrait,
|
||||
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)
|
||||
}
|
||||
if viewModel.similarItems.isNotEmpty {
|
||||
ItemView.SimilarItemsHStack(items: viewModel.similarItems)
|
||||
|
||||
RowDivider()
|
||||
}
|
||||
|
||||
ItemView.AboutView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,26 @@ import SwiftUI
|
|||
|
||||
struct CollectionItemView: View {
|
||||
|
||||
@Default(.Customization.itemViewType)
|
||||
private var itemViewType
|
||||
|
||||
@ObservedObject
|
||||
var viewModel: CollectionItemViewModel
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
ContentView(viewModel: viewModel)
|
||||
.edgePadding(.bottom)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ extension EpisodeItemView.ContentView {
|
|||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal)
|
||||
|
||||
ItemView.AttributesHStack(viewModel: viewModel)
|
||||
ItemView.AttributesHStack(viewModel: viewModel, alignment: .center)
|
||||
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(maxWidth: 300)
|
||||
|
|
|
@ -23,8 +23,6 @@ extension ItemView {
|
|||
@ObservedObject
|
||||
var viewModel: ItemViewModel
|
||||
|
||||
@State
|
||||
private var scrollViewOffset: CGFloat = 0
|
||||
@State
|
||||
private var blurHashBottomEdgeColor: Color = .secondarySystemFill
|
||||
|
||||
|
@ -132,9 +130,11 @@ extension ItemView.CinematicScrollView {
|
|||
.foregroundColor(Color(UIColor.lightGray))
|
||||
.padding(.horizontal)
|
||||
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(maxWidth: 300)
|
||||
.frame(height: 50)
|
||||
if viewModel.presentPlayButton {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(maxWidth: 300)
|
||||
.frame(height: 50)
|
||||
}
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
|
@ -148,7 +148,7 @@ extension ItemView.CinematicScrollView {
|
|||
.taglineLineLimit(2)
|
||||
.foregroundColor(.white)
|
||||
|
||||
ItemView.AttributesHStack(viewModel: viewModel)
|
||||
ItemView.AttributesHStack(viewModel: viewModel, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,13 +131,13 @@ extension ItemView.CompactLogoScrollView {
|
|||
|
||||
ItemView.AttributesHStack(viewModel: viewModel)
|
||||
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(maxWidth: 300)
|
||||
.frame(height: 50)
|
||||
if viewModel.presentPlayButton {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(height: 50)
|
||||
}
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
.frame(maxWidth: 300)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .bottom)
|
||||
|
|
|
@ -133,7 +133,7 @@ extension ItemView.CompactPosterScrollView {
|
|||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(Color(UIColor.lightGray))
|
||||
|
||||
ItemView.AttributesHStack(viewModel: viewModel)
|
||||
ItemView.AttributesHStack(viewModel: viewModel, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,8 +155,10 @@ extension ItemView.CompactPosterScrollView {
|
|||
|
||||
HStack(alignment: .center) {
|
||||
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(width: 130, height: 40)
|
||||
if viewModel.presentPlayButton {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(width: 130, height: 40)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
|
|
|
@ -22,6 +22,34 @@ extension iPadOSCollectionItemView {
|
|||
var body: some View {
|
||||
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
|
||||
|
||||
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
|
||||
|
@ -38,17 +66,12 @@ extension iPadOSCollectionItemView {
|
|||
RowDivider()
|
||||
}
|
||||
|
||||
// MARK: Items
|
||||
// MARK: Similar
|
||||
|
||||
if viewModel.collectionItems.isNotEmpty {
|
||||
PosterHStack(
|
||||
title: L10n.items,
|
||||
type: .portrait,
|
||||
items: viewModel.collectionItems
|
||||
)
|
||||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
if viewModel.similarItems.isNotEmpty {
|
||||
ItemView.SimilarItemsHStack(items: viewModel.similarItems)
|
||||
|
||||
RowDivider()
|
||||
}
|
||||
|
||||
ItemView.AboutView(viewModel: viewModel)
|
||||
|
|
|
@ -110,8 +110,6 @@ extension ItemView.iPadOSCinematicScrollView {
|
|||
.foregroundColor(.white)
|
||||
|
||||
HStack(spacing: 30) {
|
||||
ItemView.AttributesHStack(viewModel: viewModel)
|
||||
|
||||
DotHStack {
|
||||
if let firstGenre = viewModel.item.genres?.first {
|
||||
Text(firstGenre)
|
||||
|
@ -127,28 +125,25 @@ extension ItemView.iPadOSCinematicScrollView {
|
|||
}
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color(UIColor.lightGray))
|
||||
|
||||
ItemView.AttributesHStack(viewModel: viewModel, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 200)
|
||||
|
||||
Spacer()
|
||||
|
||||
// TODO: remove when/if collections have a different view
|
||||
|
||||
if !(viewModel is CollectionItemViewModel) {
|
||||
VStack(spacing: 10) {
|
||||
VStack(spacing: 10) {
|
||||
if viewModel.presentPlayButton {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(height: 50)
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(width: 250)
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(width: 250)
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(width: 250)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue