[tvOS] PagingLibraryView - Mirror iOS "Hot Reload" Functionality (#1408)
* Update missing strings. Mirror iOS setting changes to make sure tvOS PagingLIbraryView updates with changes as well. Lay the groundwork for filtering. * Revert back to rows over offset * Reorder a bit. * Move default layout to no values in closure. For whatever reason, I cannot do this to the non-defaults. * 95% there * Move `onChange`s to the `innerContent` to alleviate Type Checker issues * All Value-less closures are moved with teh exception of viewModel.filterViewModel?.currentFilters * Prepare for future letter picker location
This commit is contained in:
parent
b0b604c4ad
commit
4ca788338d
|
@ -1018,6 +1018,14 @@ internal enum L10n {
|
||||||
internal static let regular = L10n.tr("Localizable", "regular", fallback: "Regular")
|
internal static let regular = L10n.tr("Localizable", "regular", fallback: "Regular")
|
||||||
/// Release Date
|
/// Release Date
|
||||||
internal static let releaseDate = L10n.tr("Localizable", "releaseDate", fallback: "Release Date")
|
internal static let releaseDate = L10n.tr("Localizable", "releaseDate", fallback: "Release Date")
|
||||||
|
/// Remember layout
|
||||||
|
internal static let rememberLayout = L10n.tr("Localizable", "rememberLayout", fallback: "Remember layout")
|
||||||
|
/// Remember layout for individual libraries
|
||||||
|
internal static let rememberLayoutFooter = L10n.tr("Localizable", "rememberLayoutFooter", fallback: "Remember layout for individual libraries")
|
||||||
|
/// Remember sorting
|
||||||
|
internal static let rememberSorting = L10n.tr("Localizable", "rememberSorting", fallback: "Remember sorting")
|
||||||
|
/// Remember sorting for individual libraries
|
||||||
|
internal static let rememberSortingFooter = L10n.tr("Localizable", "rememberSortingFooter", fallback: "Remember sorting for individual libraries")
|
||||||
/// Remixer
|
/// Remixer
|
||||||
internal static let remixer = L10n.tr("Localizable", "remixer", fallback: "Remixer")
|
internal static let remixer = L10n.tr("Localizable", "remixer", fallback: "Remixer")
|
||||||
/// Remote connections
|
/// Remote connections
|
||||||
|
|
|
@ -12,32 +12,45 @@ import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// TODO: Figure out proper tab bar handling with the collection offset
|
// TODO: Figure out proper tab bar handling with the collection offset
|
||||||
// TODO: list columns
|
|
||||||
// TODO: list row view (LibraryRow)
|
|
||||||
// TODO: fix paging for next item focusing the tab
|
// TODO: fix paging for next item focusing the tab
|
||||||
|
|
||||||
struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
|
|
||||||
@Default(.Customization.Library.cinematicBackground)
|
@Default(.Customization.Library.cinematicBackground)
|
||||||
private var cinematicBackground
|
private var cinematicBackground
|
||||||
@Default(.Customization.Library.posterType)
|
@Default(.Customization.Library.enabledDrawerFilters)
|
||||||
private var posterType
|
private var enabledDrawerFilters
|
||||||
@Default(.Customization.Library.displayType)
|
@Default(.Customization.Library.rememberLayout)
|
||||||
private var viewType
|
private var rememberLayout
|
||||||
@Default(.Customization.showPosterLabels)
|
|
||||||
private var showPosterLabels
|
@Default
|
||||||
|
private var defaultDisplayType: LibraryDisplayType
|
||||||
|
@Default
|
||||||
|
private var defaultListColumnCount: Int
|
||||||
|
@Default
|
||||||
|
private var defaultPosterType: PosterDisplayType
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: LibraryCoordinator<Element>.Router
|
private var router: LibraryCoordinator<Element>.Router
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var focusedItem: Element?
|
private var focusedItem: Element?
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var presentBackground = false
|
private var presentBackground = false
|
||||||
@State
|
@State
|
||||||
private var layout: CollectionVGridLayout
|
private var layout: CollectionVGridLayout
|
||||||
|
@State
|
||||||
|
private var safeArea: EdgeInsets = .zero
|
||||||
|
|
||||||
|
@StoredValue
|
||||||
|
private var displayType: LibraryDisplayType
|
||||||
|
@StoredValue
|
||||||
|
private var listColumnCount: Int
|
||||||
|
@StoredValue
|
||||||
|
private var posterType: PosterDisplayType
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var collectionVGridProxy: CollectionVGridProxy = .init()
|
||||||
@StateObject
|
@StateObject
|
||||||
private var viewModel: PagingLibraryViewModel<Element>
|
private var viewModel: PagingLibraryViewModel<Element>
|
||||||
|
|
||||||
|
@ -45,22 +58,33 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
private var cinematicBackgroundViewModel: CinematicBackgroundView<Element>.ViewModel = .init()
|
private var cinematicBackgroundViewModel: CinematicBackgroundView<Element>.ViewModel = .init()
|
||||||
|
|
||||||
init(viewModel: PagingLibraryViewModel<Element>) {
|
init(viewModel: PagingLibraryViewModel<Element>) {
|
||||||
|
|
||||||
|
self._defaultDisplayType = Default(.Customization.Library.displayType)
|
||||||
|
self._defaultListColumnCount = Default(.Customization.Library.listColumnCount)
|
||||||
|
self._defaultPosterType = Default(.Customization.Library.posterType)
|
||||||
|
|
||||||
|
self._displayType = StoredValue(.User.libraryDisplayType(parentID: viewModel.parent?.id))
|
||||||
|
self._listColumnCount = StoredValue(.User.libraryListColumnCount(parentID: viewModel.parent?.id))
|
||||||
|
self._posterType = StoredValue(.User.libraryPosterType(parentID: viewModel.parent?.id))
|
||||||
|
|
||||||
self._viewModel = StateObject(wrappedValue: viewModel)
|
self._viewModel = StateObject(wrappedValue: viewModel)
|
||||||
|
|
||||||
let initialPosterType = Defaults[.Customization.Library.posterType]
|
let initialDisplayType = Defaults[.Customization.Library.rememberLayout] ? _displayType.wrappedValue : _defaultDisplayType
|
||||||
let initialViewType = Defaults[.Customization.Library.displayType]
|
.wrappedValue
|
||||||
let listColumnCount = Defaults[.Customization.Library.listColumnCount]
|
let initialListColumnCount = Defaults[.Customization.Library.rememberLayout] ? _listColumnCount
|
||||||
|
.wrappedValue : _defaultListColumnCount.wrappedValue
|
||||||
|
let initialPosterType = Defaults[.Customization.Library.rememberLayout] ? _posterType.wrappedValue : _defaultPosterType.wrappedValue
|
||||||
|
|
||||||
self._layout = State(
|
self._layout = State(
|
||||||
initialValue: Self.makeLayout(
|
initialValue: Self.makeLayout(
|
||||||
posterType: initialPosterType,
|
posterType: initialPosterType,
|
||||||
displayType: initialViewType,
|
viewType: initialDisplayType,
|
||||||
listColumnCount: listColumnCount
|
listColumnCount: initialListColumnCount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: onSelect
|
// MARK: On Select
|
||||||
|
|
||||||
private func onSelect(_ element: Element) {
|
private func onSelect(_ element: Element) {
|
||||||
switch element {
|
switch element {
|
||||||
|
@ -73,29 +97,36 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Select Item
|
||||||
|
|
||||||
private func select(item: BaseItemDto) {
|
private func select(item: BaseItemDto) {
|
||||||
switch item.type {
|
switch item.type {
|
||||||
case .collectionFolder, .folder:
|
case .collectionFolder, .folder:
|
||||||
let viewModel = ItemLibraryViewModel(parent: item)
|
let viewModel = ItemLibraryViewModel(parent: item)
|
||||||
router.route(to: \.library, viewModel)
|
router.route(to: \.library, viewModel)
|
||||||
|
case .person:
|
||||||
|
let viewModel = ItemLibraryViewModel(parent: item)
|
||||||
|
router.route(to: \.library, viewModel)
|
||||||
default:
|
default:
|
||||||
router.route(to: \.item, item)
|
router.route(to: \.item, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Select Person
|
||||||
|
|
||||||
private func select(person: BaseItemPerson) {
|
private func select(person: BaseItemPerson) {
|
||||||
let viewModel = ItemLibraryViewModel(parent: person)
|
let viewModel = ItemLibraryViewModel(parent: person)
|
||||||
router.route(to: \.library, viewModel)
|
router.route(to: \.library, viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: layout
|
// MARK: Make Layout
|
||||||
|
|
||||||
private static func makeLayout(
|
private static func makeLayout(
|
||||||
posterType: PosterDisplayType,
|
posterType: PosterDisplayType,
|
||||||
displayType: LibraryDisplayType,
|
viewType: LibraryDisplayType,
|
||||||
listColumnCount: Int
|
listColumnCount: Int
|
||||||
) -> CollectionVGridLayout {
|
) -> CollectionVGridLayout {
|
||||||
switch (posterType, displayType) {
|
switch (posterType, viewType) {
|
||||||
case (.landscape, .grid):
|
case (.landscape, .grid):
|
||||||
return .columns(5, insets: .init(50), itemSpacing: 50, lineSpacing: 50)
|
return .columns(5, insets: .init(50), itemSpacing: 50, lineSpacing: 50)
|
||||||
case (.portrait, .grid):
|
case (.portrait, .grid):
|
||||||
|
@ -105,6 +136,47 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Set Default Layout
|
||||||
|
|
||||||
|
private func setDefaultLayout() {
|
||||||
|
layout = Self.makeLayout(
|
||||||
|
posterType: defaultPosterType,
|
||||||
|
viewType: defaultDisplayType,
|
||||||
|
listColumnCount: defaultListColumnCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Set Custom Layout
|
||||||
|
|
||||||
|
private func setCustomLayout() {
|
||||||
|
layout = Self.makeLayout(
|
||||||
|
posterType: posterType,
|
||||||
|
viewType: displayType,
|
||||||
|
listColumnCount: listColumnCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Set Cinematic Background
|
||||||
|
|
||||||
|
private func setCinematicBackground() {
|
||||||
|
guard let focusedItem else {
|
||||||
|
withAnimation {
|
||||||
|
presentBackground = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cinematicBackgroundViewModel.select(item: focusedItem)
|
||||||
|
|
||||||
|
if !presentBackground {
|
||||||
|
withAnimation {
|
||||||
|
presentBackground = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Landscape Grid Item View
|
||||||
|
|
||||||
private func landscapeGridItemView(item: Element) -> some View {
|
private func landscapeGridItemView(item: Element) -> some View {
|
||||||
PosterButton(item: item, type: .landscape)
|
PosterButton(item: item, type: .landscape)
|
||||||
.content {
|
.content {
|
||||||
|
@ -112,6 +184,11 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
PosterButton.TitleContentView(item: item)
|
PosterButton.TitleContentView(item: item)
|
||||||
.backport
|
.backport
|
||||||
.lineLimit(1, reservesSpace: true)
|
.lineLimit(1, reservesSpace: true)
|
||||||
|
} else if viewModel.parent?.libraryType == .folder {
|
||||||
|
PosterButton.TitleContentView(item: item)
|
||||||
|
.backport
|
||||||
|
.lineLimit(1, reservesSpace: true)
|
||||||
|
.hidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFocusChanged { newValue in
|
.onFocusChanged { newValue in
|
||||||
|
@ -124,6 +201,9 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Portrait Grid Item View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private func portraitGridItemView(item: Element) -> some View {
|
private func portraitGridItemView(item: Element) -> some View {
|
||||||
PosterButton(item: item, type: .portrait)
|
PosterButton(item: item, type: .portrait)
|
||||||
.content {
|
.content {
|
||||||
|
@ -131,6 +211,11 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
PosterButton.TitleContentView(item: item)
|
PosterButton.TitleContentView(item: item)
|
||||||
.backport
|
.backport
|
||||||
.lineLimit(1, reservesSpace: true)
|
.lineLimit(1, reservesSpace: true)
|
||||||
|
} else if viewModel.parent?.libraryType == .folder {
|
||||||
|
PosterButton.TitleContentView(item: item)
|
||||||
|
.backport
|
||||||
|
.lineLimit(1, reservesSpace: true)
|
||||||
|
.hidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFocusChanged { newValue in
|
.onFocusChanged { newValue in
|
||||||
|
@ -143,6 +228,8 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: List Item View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func listItemView(item: Element, posterType: PosterDisplayType) -> some View {
|
private func listItemView(item: Element, posterType: PosterDisplayType) -> some View {
|
||||||
LibraryRow(item: item, posterType: posterType)
|
LibraryRow(item: item, posterType: posterType)
|
||||||
|
@ -156,13 +243,31 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Error View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var contentView: some View {
|
private func errorView(with error: some Error) -> some View {
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
/* ErrorView(error: error)
|
||||||
|
.onRetry {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Grid View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var gridView: some View {
|
||||||
CollectionVGrid(
|
CollectionVGrid(
|
||||||
uniqueElements: viewModel.elements,
|
uniqueElements: viewModel.elements,
|
||||||
layout: layout
|
layout: layout
|
||||||
) { item in
|
) { item in
|
||||||
switch (posterType, viewType) {
|
|
||||||
|
let displayType = Defaults[.Customization.Library.rememberLayout] ? _displayType.wrappedValue : _defaultDisplayType
|
||||||
|
.wrappedValue
|
||||||
|
let posterType = Defaults[.Customization.Library.rememberLayout] ? _posterType.wrappedValue : _defaultPosterType.wrappedValue
|
||||||
|
|
||||||
|
switch (posterType, displayType) {
|
||||||
case (.landscape, .grid):
|
case (.landscape, .grid):
|
||||||
landscapeGridItemView(item: item)
|
landscapeGridItemView(item: item)
|
||||||
case (.portrait, .grid):
|
case (.portrait, .grid):
|
||||||
|
@ -174,55 +279,146 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
|
||||||
.onReachedBottomEdge(offset: .rows(3)) {
|
.onReachedBottomEdge(offset: .rows(3)) {
|
||||||
viewModel.send(.getNextPage)
|
viewModel.send(.getNextPage)
|
||||||
}
|
}
|
||||||
|
.proxy(collectionVGridProxy)
|
||||||
|
.scrollIndicatorsVisible(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Inner Content View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var innerContent: some View {
|
||||||
|
switch viewModel.state {
|
||||||
|
case .content:
|
||||||
|
if viewModel.elements.isEmpty {
|
||||||
|
L10n.noResults.text
|
||||||
|
} else {
|
||||||
|
gridView
|
||||||
|
}
|
||||||
|
case .initial, .refreshing:
|
||||||
|
ProgressView()
|
||||||
|
default:
|
||||||
|
AssertionFailureView("Expected view for unexpected state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Content View
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var contentView: some View {
|
||||||
|
|
||||||
|
innerContent
|
||||||
|
// These exist here to alleviate type-checker issues
|
||||||
|
.onChange(of: posterType) {
|
||||||
|
setCustomLayout()
|
||||||
|
}
|
||||||
|
.onChange(of: displayType) {
|
||||||
|
setCustomLayout()
|
||||||
|
}
|
||||||
|
.onChange(of: listColumnCount) {
|
||||||
|
setCustomLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for LetterPicker. Enable when ready
|
||||||
|
|
||||||
|
/* if letterPickerEnabled, let filterViewModel = viewModel.filterViewModel {
|
||||||
|
ZStack(alignment: letterPickerOrientation.alignment) {
|
||||||
|
innerContent
|
||||||
|
.padding(letterPickerOrientation.edge, LetterPickerBar.size + 10)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
LetterPickerBar(viewModel: filterViewModel)
|
||||||
|
.padding(.top, safeArea.top)
|
||||||
|
.padding(.bottom, safeArea.bottom)
|
||||||
|
.padding(letterPickerOrientation.edge, 10)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
innerContent
|
||||||
|
}
|
||||||
|
// These exist here to alleviate type-checker issues
|
||||||
|
.onChange(of: posterType) {
|
||||||
|
setCustomLayout()
|
||||||
|
}
|
||||||
|
.onChange(of: displayType) {
|
||||||
|
setCustomLayout()
|
||||||
|
}
|
||||||
|
.onChange(of: listColumnCount) {
|
||||||
|
setCustomLayout()
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
Color.clear
|
||||||
|
|
||||||
if cinematicBackground {
|
if cinematicBackground {
|
||||||
CinematicBackgroundView(viewModel: cinematicBackgroundViewModel)
|
CinematicBackgroundView(viewModel: cinematicBackgroundViewModel)
|
||||||
.visible(presentBackground)
|
.visible(presentBackground)
|
||||||
.blurred()
|
.blurred()
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedView {
|
switch viewModel.state {
|
||||||
Group {
|
case .content, .initial, .refreshing:
|
||||||
switch viewModel.state {
|
contentView
|
||||||
case let .error(error):
|
case let .error(error):
|
||||||
Text(error.localizedDescription)
|
errorView(with: error)
|
||||||
case .initial, .refreshing:
|
}
|
||||||
ProgressView()
|
}
|
||||||
case .content:
|
.animation(.linear(duration: 0.1), value: viewModel.state)
|
||||||
if viewModel.elements.isEmpty {
|
.ignoresSafeArea()
|
||||||
L10n.noResults.text
|
.navigationTitle(viewModel.parent?.displayTitle ?? "")
|
||||||
} else {
|
.onChange(of: focusedItem) {
|
||||||
contentView
|
setCinematicBackground()
|
||||||
}
|
}
|
||||||
}
|
.onChange(of: rememberLayout) {
|
||||||
|
if rememberLayout {
|
||||||
|
setCustomLayout()
|
||||||
|
} else {
|
||||||
|
setDefaultLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: defaultPosterType) {
|
||||||
|
guard !Defaults[.Customization.Library.rememberLayout] else { return }
|
||||||
|
setDefaultLayout()
|
||||||
|
}
|
||||||
|
.onChange(of: defaultDisplayType) {
|
||||||
|
guard !Defaults[.Customization.Library.rememberLayout] else { return }
|
||||||
|
setDefaultLayout()
|
||||||
|
}
|
||||||
|
.onChange(of: defaultListColumnCount) {
|
||||||
|
guard !Defaults[.Customization.Library.rememberLayout] else { return }
|
||||||
|
setDefaultLayout()
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.filterViewModel?.currentFilters) { _, newValue in
|
||||||
|
guard let newValue, let id = viewModel.parent?.id else { return }
|
||||||
|
|
||||||
|
if Defaults[.Customization.Library.rememberSort] {
|
||||||
|
let newStoredFilters = StoredValues[.User.libraryFilters(parentID: id)]
|
||||||
|
.mutating(\.sortBy, with: newValue.sortBy)
|
||||||
|
.mutating(\.sortOrder, with: newValue.sortOrder)
|
||||||
|
|
||||||
|
StoredValues[.User.libraryFilters(parentID: id)] = newStoredFilters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(viewModel.events) { event in
|
||||||
|
switch event {
|
||||||
|
case let .gotRandomItem(item):
|
||||||
|
switch item {
|
||||||
|
case let item as BaseItemDto:
|
||||||
|
router.route(to: \.item, item)
|
||||||
|
case let item as BaseItemPerson:
|
||||||
|
let viewModel = ItemLibraryViewModel(parent: item, filters: .default)
|
||||||
|
router.route(to: \.library, viewModel)
|
||||||
|
default:
|
||||||
|
assertionFailure("Used an unexpected type within a `PagingLibaryView`?")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
|
||||||
.navigationTitle(viewModel.parent?.displayTitle ?? "")
|
|
||||||
.onFirstAppear {
|
.onFirstAppear {
|
||||||
if viewModel.state == .initial {
|
if viewModel.state == .initial {
|
||||||
viewModel.send(.refresh)
|
viewModel.send(.refresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: focusedItem) { _, newValue in
|
|
||||||
guard let newValue else {
|
|
||||||
withAnimation {
|
|
||||||
presentBackground = false
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cinematicBackgroundViewModel.select(item: newValue)
|
|
||||||
|
|
||||||
if !presentBackground {
|
|
||||||
withAnimation {
|
|
||||||
presentBackground = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension CustomizeViewsSettings {
|
||||||
|
|
||||||
|
struct LibrarySection: View {
|
||||||
|
|
||||||
|
@Default(.Customization.Library.randomImage)
|
||||||
|
private var libraryRandomImage
|
||||||
|
@Default(.Customization.Library.showFavorites)
|
||||||
|
private var showFavorites
|
||||||
|
|
||||||
|
@Default(.Customization.Library.cinematicBackground)
|
||||||
|
private var cinematicBackground
|
||||||
|
@Default(.Customization.Library.displayType)
|
||||||
|
private var libraryDisplayType
|
||||||
|
@Default(.Customization.Library.posterType)
|
||||||
|
private var libraryPosterType
|
||||||
|
@Default(.Customization.Library.listColumnCount)
|
||||||
|
private var listColumnCount
|
||||||
|
|
||||||
|
@Default(.Customization.Library.rememberLayout)
|
||||||
|
private var rememberLibraryLayout
|
||||||
|
@Default(.Customization.Library.rememberSort)
|
||||||
|
private var rememberLibrarySort
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: CustomizeSettingsCoordinator.Router
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isPresentingNextUpDays = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(L10n.media) {
|
||||||
|
|
||||||
|
Toggle(L10n.randomImage, isOn: $libraryRandomImage)
|
||||||
|
|
||||||
|
Toggle(L10n.showFavorites, isOn: $showFavorites)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(L10n.library) {
|
||||||
|
Toggle(L10n.cinematicBackground, isOn: $cinematicBackground)
|
||||||
|
|
||||||
|
InlineEnumToggle(title: L10n.posters, selection: $libraryPosterType)
|
||||||
|
|
||||||
|
InlineEnumToggle(title: L10n.library, selection: $libraryDisplayType)
|
||||||
|
|
||||||
|
if libraryDisplayType == .list {
|
||||||
|
ChevronButton(
|
||||||
|
L10n.columns,
|
||||||
|
subtitle: listColumnCount.description
|
||||||
|
)
|
||||||
|
.onSelect {
|
||||||
|
router.route(to: \.listColumnSettings, $listColumnCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Toggle(L10n.rememberLayout, isOn: $rememberLibraryLayout)
|
||||||
|
} footer: {
|
||||||
|
Text(L10n.rememberLayoutFooter)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Toggle(L10n.rememberSorting, isOn: $rememberLibrarySort)
|
||||||
|
} footer: {
|
||||||
|
Text(L10n.rememberSortingFooter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,19 +31,6 @@ struct CustomizeViewsSettings: View {
|
||||||
@Default(.Customization.Library.displayType)
|
@Default(.Customization.Library.displayType)
|
||||||
private var libraryViewType
|
private var libraryViewType
|
||||||
|
|
||||||
@Default(.Customization.Library.cinematicBackground)
|
|
||||||
private var cinematicBackground
|
|
||||||
@Default(.Customization.Library.randomImage)
|
|
||||||
private var libraryRandomImage
|
|
||||||
@Default(.Customization.Library.showFavorites)
|
|
||||||
private var showFavorites
|
|
||||||
@Default(.Customization.Library.displayType)
|
|
||||||
private var libraryDisplayType
|
|
||||||
@Default(.Customization.Library.posterType)
|
|
||||||
private var libraryPosterType
|
|
||||||
@Default(.Customization.Library.listColumnCount)
|
|
||||||
private var listColumnCount
|
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: CustomizeSettingsCoordinator.Router
|
private var router: CustomizeSettingsCoordinator.Router
|
||||||
|
|
||||||
|
@ -84,26 +71,7 @@ struct CustomizeViewsSettings: View {
|
||||||
InlineEnumToggle(title: L10n.search, selection: $searchPosterType)
|
InlineEnumToggle(title: L10n.search, selection: $searchPosterType)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(L10n.library) {
|
LibrarySection()
|
||||||
|
|
||||||
Toggle(L10n.cinematicBackground, isOn: $cinematicBackground)
|
|
||||||
|
|
||||||
Toggle(L10n.randomImage, isOn: $libraryRandomImage)
|
|
||||||
|
|
||||||
Toggle(L10n.showFavorites, isOn: $showFavorites)
|
|
||||||
|
|
||||||
InlineEnumToggle(title: L10n.posters, selection: $libraryPosterType)
|
|
||||||
InlineEnumToggle(title: L10n.library, selection: $libraryDisplayType)
|
|
||||||
if libraryDisplayType == .list {
|
|
||||||
ChevronButton(
|
|
||||||
L10n.columns,
|
|
||||||
subtitle: listColumnCount.description
|
|
||||||
)
|
|
||||||
.onSelect {
|
|
||||||
router.route(to: \.listColumnSettings, $listColumnCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemSection()
|
ItemSection()
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,7 @@
|
||||||
4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */; };
|
4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */; };
|
||||||
4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */; };
|
4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */; };
|
||||||
4EA78B252D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */; };
|
4EA78B252D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */; };
|
||||||
|
4EAE340C2D42B857006FBAD3 /* LibrarySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EAE340B2D42B852006FBAD3 /* LibrarySection.swift */; };
|
||||||
4EB132EF2D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; };
|
4EB132EF2D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; };
|
||||||
4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; };
|
4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; };
|
||||||
4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; };
|
4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; };
|
||||||
|
@ -1342,6 +1343,7 @@
|
||||||
4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoPickerView.swift; sourceTree = "<group>"; };
|
4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoPickerView.swift; sourceTree = "<group>"; };
|
||||||
4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCropView.swift; sourceTree = "<group>"; };
|
4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCropView.swift; sourceTree = "<group>"; };
|
||||||
4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagePickerCoordinator.swift; sourceTree = "<group>"; };
|
4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagePickerCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
4EAE340B2D42B852006FBAD3 /* LibrarySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySection.swift; sourceTree = "<group>"; };
|
||||||
4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageType.swift; sourceTree = "<group>"; };
|
4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageType.swift; sourceTree = "<group>"; };
|
||||||
4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = "<group>"; };
|
4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = "<group>"; };
|
||||||
4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = "<group>"; };
|
4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2524,6 +2526,7 @@
|
||||||
children = (
|
children = (
|
||||||
4E699BBF2CB34775007CBD5D /* HomeSection.swift */,
|
4E699BBF2CB34775007CBD5D /* HomeSection.swift */,
|
||||||
4E97D1822D064748004B89AD /* ItemSection.swift */,
|
4E97D1822D064748004B89AD /* ItemSection.swift */,
|
||||||
|
4EAE340B2D42B852006FBAD3 /* LibrarySection.swift */,
|
||||||
);
|
);
|
||||||
path = Sections;
|
path = Sections;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -5638,6 +5641,7 @@
|
||||||
4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */,
|
4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */,
|
||||||
E1B4E4372CA7795200DC49DE /* OrderedDictionary.swift in Sources */,
|
E1B4E4372CA7795200DC49DE /* OrderedDictionary.swift in Sources */,
|
||||||
E1AD104E26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
E1AD104E26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
||||||
|
4EAE340C2D42B857006FBAD3 /* LibrarySection.swift in Sources */,
|
||||||
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */,
|
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */,
|
||||||
4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */,
|
4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */,
|
||||||
62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
||||||
|
|
|
@ -163,15 +163,15 @@ struct CustomizeViewsSettings: View {
|
||||||
HomeSection()
|
HomeSection()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle("Remember layout", isOn: $rememberLibraryLayout)
|
Toggle(L10n.rememberLayout, isOn: $rememberLibraryLayout)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Remember layout for individual libraries")
|
Text(L10n.rememberLayoutFooter)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle("Remember sorting", isOn: $rememberLibrarySort)
|
Toggle(L10n.rememberSorting, isOn: $rememberLibrarySort)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Remember sorting for individual libraries")
|
Text(L10n.rememberSortingFooter)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
|
|
@ -1456,6 +1456,18 @@
|
||||||
/// Release Date
|
/// Release Date
|
||||||
"releaseDate" = "Release Date";
|
"releaseDate" = "Release Date";
|
||||||
|
|
||||||
|
/// Remember layout
|
||||||
|
"rememberLayout" = "Remember layout";
|
||||||
|
|
||||||
|
/// Remember layout for individual libraries
|
||||||
|
"rememberLayoutFooter" = "Remember layout for individual libraries";
|
||||||
|
|
||||||
|
/// Remember sorting
|
||||||
|
"rememberSorting" = "Remember sorting";
|
||||||
|
|
||||||
|
/// Remember sorting for individual libraries
|
||||||
|
"rememberSortingFooter" = "Remember sorting for individual libraries";
|
||||||
|
|
||||||
/// Remixer
|
/// Remixer
|
||||||
"remixer" = "Remixer";
|
"remixer" = "Remixer";
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue