iOS/iPadOS - Library List View (#542)

This commit is contained in:
Ethan Pippin 2022-08-29 08:58:38 -06:00 committed by GitHub
parent d078d71393
commit 3b755adf87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 29 deletions

View File

@ -0,0 +1,25 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Defaults
import Foundation
enum LibraryViewType: String, CaseIterable, Defaults.Serializable {
case grid
case list
// TODO: localize after organization
var localizedName: String {
switch self {
case .grid:
return "Grid"
case .list:
return "List"
}
}
}

View File

@ -37,11 +37,15 @@ extension Defaults.Keys {
static let latestInLibraryPosterType = Key<PosterType>("latestInLibraryPosterType", default: .portrait, suite: .generalSuite)
static let recommendedPosterType = Key<PosterType>("recommendedPosterType", default: .portrait, suite: .generalSuite)
static let searchPosterType = Key<PosterType>("searchPosterType", default: .portrait, suite: .generalSuite)
static let libraryPosterType = Key<PosterType>("libraryPosterType", default: .portrait, suite: .generalSuite)
enum Episodes {
static let useSeriesLandscapeBackdrop = Key<Bool>("useSeriesBackdrop", default: true, suite: .generalSuite)
}
enum Library {
static let viewType = Key<LibraryViewType>("Customization.Library.viewType", default: .grid, suite: .generalSuite)
static let gridPosterType = Key<PosterType>("Customization.Library.gridPosterType", default: .portrait, suite: .generalSuite)
}
}
// Video player / overlay settings

View File

@ -13,8 +13,8 @@ import UIKit
final class LibraryViewModel: ViewModel {
@Default(.Customization.libraryPosterType)
var libraryPosterType
@Default(.Customization.Library.gridPosterType)
var libraryGridPosterType
@Published
var items: [BaseItemDto] = []
@ -35,8 +35,8 @@ final class LibraryViewModel: ViewModel {
var studio: NameGuidPair?
private var pageItemSize: Int {
let height = libraryPosterType == .portrait ? libraryPosterType.width * 1.5 : libraryPosterType.width / 1.77
return UIScreen.itemsFillableOnScreen(width: libraryPosterType.width, height: height)
let height = libraryGridPosterType == .portrait ? libraryGridPosterType.width * 1.5 : libraryGridPosterType.width / 1.77
return UIScreen.itemsFillableOnScreen(width: libraryGridPosterType.width, height: height)
}
var enabledFilterType: [FilterType] {

View File

@ -20,7 +20,7 @@ struct LibraryView: View {
@State
private var scrollViewOffset: CGPoint = .zero
@Default(.Customization.libraryPosterType)
@Default(.Customization.Library.gridPosterType)
var libraryPosterType
@ViewBuilder

View File

@ -83,7 +83,6 @@
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ABFDEA2679753200886593 /* ConnectToServerView.swift */; };
53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 53ABFDEC26799D7700886593 /* ActivityIndicator */; };
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CD2A3F268A49C2002ABD4E /* ItemView.swift */; };
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; };
53EE24E6265060780068F029 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* SearchView.swift */; };
5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
@ -306,6 +305,11 @@
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; };
E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.swift */; };
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */; };
E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryViewType.swift */; };
E13F05ED28BC9000003499D2 /* LibraryViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryViewType.swift */; };
E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */; };
E13F05F228BC9016003499D2 /* LibraryItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */; };
E13F05F328BC9016003499D2 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05F028BC9016003499D2 /* LibraryView.swift */; };
E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546776289AF46E00087E35 /* CollectionItemView.swift */; };
E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546779289AF48200087E35 /* CollectionItemContentView.swift */; };
E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.swift */; };
@ -616,7 +620,6 @@
53ABFDEA2679753200886593 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = "<group>"; };
53CD2A3F268A49C2002ABD4E /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = "<group>"; };
53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
53EE24E5265060780068F029 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSize.swift; sourceTree = "<group>"; };
@ -769,6 +772,9 @@
E13DD3F82717E961009D4DAF /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = "<group>"; };
E13DD3FB2717EAE8009D4DAF /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = "<group>"; };
E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListCoordinator.swift; sourceTree = "<group>"; };
E13F05EB28BC9000003499D2 /* LibraryViewType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryViewType.swift; sourceTree = "<group>"; };
E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryItemRow.swift; sourceTree = "<group>"; };
E13F05F028BC9016003499D2 /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
E1546776289AF46E00087E35 /* CollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemView.swift; sourceTree = "<group>"; };
E1546779289AF48200087E35 /* CollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = "<group>"; };
E168BD08289A4162001A6922 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
@ -1151,6 +1157,7 @@
E19169CD272514760085832A /* HTTPScheme.swift */,
E1C925F328875037002A7A66 /* ItemViewType.swift */,
E1E1644328BC60C600323B0A /* LibraryItem.swift */,
E13F05EB28BC9000003499D2 /* LibraryViewType.swift */,
E1AA331E2782639D00F6439C /* OverlayType.swift */,
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
@ -1681,7 +1688,7 @@
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */,
E14F7D0A26DB3714007C3AE6 /* ItemView */,
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
E13F05EE28BC9016003499D2 /* LibraryView */,
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */,
C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */,
C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */,
@ -1700,6 +1707,15 @@
path = Views;
sourceTree = "<group>";
};
E13F05EE28BC9016003499D2 /* LibraryView */ = {
isa = PBXGroup;
children = (
E1C55AB228BD051700A9AD88 /* Components */,
E13F05F028BC9016003499D2 /* LibraryView.swift */,
);
path = LibraryView;
sourceTree = "<group>";
};
E14F7D0A26DB3714007C3AE6 /* ItemView */ = {
isa = PBXGroup;
children = (
@ -2008,6 +2024,14 @@
path = ContinueWatchingView;
sourceTree = "<group>";
};
E1C55AB228BD051700A9AD88 /* Components */ = {
isa = PBXGroup;
children = (
E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */,
);
path = Components;
sourceTree = "<group>";
};
E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */ = {
isa = PBXGroup;
children = (
@ -2472,6 +2496,7 @@
62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */,
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */,
E13F05F228BC9016003499D2 /* LibraryItemRow.swift in Sources */,
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */,
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
@ -2527,6 +2552,7 @@
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */,
C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */,
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,
E13F05ED28BC9000003499D2 /* LibraryViewType.swift in Sources */,
E18E021C2887492B0022598C /* BlurView.swift in Sources */,
E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */,
E10D87E327852FD000BD264C /* EpisodesRowManager.swift in Sources */,
@ -2646,7 +2672,6 @@
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */,
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
E11CEB8D28999B4A003E74C7 /* FontExtensions.swift in Sources */,
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */,
@ -2654,6 +2679,7 @@
E18CE0B228A229E70092E7F1 /* UserDtoExtensions.swift in Sources */,
E18E01F0288747230022598C /* AttributeHStack.swift in Sources */,
6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */,
E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */,
E18E0205288749200022598C /* AppIcon.swift in Sources */,
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
E18E01AB288746AF0022598C /* PillHStack.swift in Sources */,
@ -2753,7 +2779,9 @@
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */,
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
E13F05F328BC9016003499D2 /* LibraryView.swift in Sources */,
E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */,
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,

View File

@ -0,0 +1,58 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import SwiftUI
struct LibraryItemRow: View {
@EnvironmentObject
private var router: LibraryCoordinator.Router
let item: BaseItemDto
var body: some View {
Button {
router.route(to: \.item, item)
} label: {
HStack(alignment: .bottom) {
PosterButton(item: item, type: .portrait)
.scaleItem(0.6)
.content { _ in }
VStack(alignment: .leading) {
Text(item.displayName)
.foregroundColor(.primary)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
DotHStack {
if let premiereYear = item.premiereDateYear {
Text(premiereYear)
}
if let runtime = item.getItemRuntime() {
Text(runtime)
}
if let officialRating = item.officialRating {
Text(officialRating)
}
}
.font(.caption)
.foregroundColor(Color(UIColor.lightGray))
}
.padding(.vertical)
Spacer()
}
}
}
}

View File

@ -13,12 +13,14 @@ import SwiftUI
struct LibraryView: View {
@EnvironmentObject
private var libraryRouter: LibraryCoordinator.Router
private var router: LibraryCoordinator.Router
@ObservedObject
var viewModel: LibraryViewModel
@Default(.Customization.libraryPosterType)
private var libraryPosterType
@Default(.Customization.Library.gridPosterType)
private var libraryGridPosterType
@Default(.Customization.Library.viewType)
private var libraryViewType
@ViewBuilder
private var loadingView: some View {
@ -31,21 +33,41 @@ struct LibraryView: View {
}
private var gridLayout: NSCollectionLayoutSection.GridLayoutMode {
if libraryPosterType == .landscape && UIDevice.isPhone {
if libraryGridPosterType == .landscape && UIDevice.isPhone {
return .fixedNumberOfColumns(2)
} else {
return .adaptive(withMinItemSize: libraryPosterType.width + (UIDevice.isIPad ? 10 : 0))
return .adaptive(withMinItemSize: libraryGridPosterType.width + (UIDevice.isIPad ? 10 : 0))
}
}
@ViewBuilder
private var libraryItemsView: some View {
private var libraryListView: some View {
CollectionView(items: viewModel.items) { _, item, _ in
PosterButton(item: item, type: libraryPosterType)
LibraryItemRow(item: item)
.padding()
}
.layout { _, layoutEnvironment in
.list(using: .init(appearance: .plain), layoutEnvironment: layoutEnvironment)
}
.willReachEdge(insets: .init(top: 0, leading: 0, bottom: 200, trailing: 0)) { edge in
if !viewModel.isLoading && edge == .bottom {
viewModel.requestNextPageAsync()
}
}
.configure { configuration in
configuration.showsVerticalScrollIndicator = false
}
.ignoresSafeArea()
}
@ViewBuilder
private var libraryGridView: some View {
CollectionView(items: viewModel.items) { _, item, _ in
PosterButton(item: item, type: libraryGridPosterType)
.onSelect { item in
libraryRouter.route(to: \.item, item)
router.route(to: \.item, item)
}
.scaleItem(libraryPosterType == .landscape && UIDevice.isPhone ? 0.8 : 1)
.scaleItem(libraryGridPosterType == .landscape && UIDevice.isPhone ? 0.8 : 1)
}
.layout { _, layoutEnvironment in
.grid(
@ -71,15 +93,35 @@ struct LibraryView: View {
} else if viewModel.items.isEmpty {
noResultsView
} else {
libraryItemsView
switch libraryViewType {
case .grid:
libraryGridView
case .list:
libraryListView
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
switch libraryViewType {
case .grid:
libraryViewType = .list
case .list:
libraryViewType = .grid
}
} label: {
switch libraryViewType {
case .grid:
Image(systemName: "list.dash")
case .list:
Image(systemName: "square.grid.2x2")
}
}
Button {
libraryRouter
router
.route(to: \.filter, (
filters: $viewModel.filters,
enabledFilterType: viewModel.enabledFilterType,

View File

@ -33,12 +33,15 @@ struct CustomizeViewsSettings: View {
var recommendedPosterType
@Default(.Customization.searchPosterType)
var searchPosterType
@Default(.Customization.libraryPosterType)
var libraryPosterType
@Default(.Customization.Episodes.useSeriesLandscapeBackdrop)
var useSeriesLandscapeBackdrop
@Default(.Customization.Library.gridPosterType)
var libraryGridPosterType
@Default(.Customization.Library.viewType)
var libraryViewType
var body: some View {
List {
Section {
@ -96,12 +99,6 @@ struct CustomizeViewsSettings: View {
Text(type.localizedName).tag(type.rawValue)
}
}
Picker(L10n.library, selection: $libraryPosterType) {
ForEach(PosterType.allCases, id: \.self) { type in
Text(type.localizedName).tag(type.rawValue)
}
}
} header: {
// TODO: localize after organization
Text("Posters")
@ -114,6 +111,23 @@ struct CustomizeViewsSettings: View {
// TODO: localize after organization
Text("Episode Landscape Poster")
}
Section {
Picker(L10n.library, selection: $libraryGridPosterType) {
ForEach(PosterType.allCases, id: \.self) { type in
Text(type.localizedName).tag(type.rawValue)
}
}
Picker(L10n.items, selection: $libraryViewType) {
ForEach(LibraryViewType.allCases, id: \.self) { type in
Text(type.localizedName).tag(type.rawValue)
}
}
} header: {
// TODO: localize after organization
Text("Library")
}
}
.navigationTitle(L10n.customize)
}