This commit is contained in:
Ethan Pippin 2024-05-28 01:23:58 -06:00 committed by GitHub
parent fd4052ed53
commit 257091ba9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 260 additions and 153 deletions

View File

@ -60,6 +60,7 @@ struct SystemImageContentView: View {
self.title = title
}
@ViewBuilder
private var imageView: some View {
Image(systemName: systemName)
.resizable()

View File

@ -35,6 +35,7 @@ struct TruncatedText: View {
private var seeMoreType: SeeMoreType
private let text: String
@ViewBuilder
private var textView: some View {
ZStack(alignment: .bottomTrailing) {
Text(text)

View File

@ -144,6 +144,7 @@ struct PagingLibraryView<Element: Poster>: View {
Button(item.displayTitle)
}
@ViewBuilder
private var contentView: some View {
CollectionVGrid(
$viewModel.elements,

View File

@ -19,6 +19,7 @@ struct ChannelLibraryView: View {
@StateObject
private var viewModel = ChannelLibraryViewModel()
@ViewBuilder
private var contentView: some View {
CollectionVGrid(
$viewModel.elements,

View File

@ -25,6 +25,7 @@ extension ChannelLibraryView {
private var onSelect: () -> Void
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
@ViewBuilder
private var channelLogo: some View {
VStack {
ZStack {

View File

@ -100,6 +100,7 @@ struct ConnectToServerView: View {
.buttonStyle(.plain)
}
@ViewBuilder
private var localServersSection: some View {
Section(L10n.localServers) {
if viewModel.localServers.isEmpty {

View File

@ -19,6 +19,7 @@ struct HomeView: View {
@StateObject
private var viewModel = HomeViewModel()
@ViewBuilder
private var contentView: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {

View File

@ -53,6 +53,7 @@ extension MediaView {
}
}
@ViewBuilder
private var titleLabel: some View {
Text(mediaType.displayTitle)
.font(.title2)

View File

@ -20,6 +20,7 @@ struct MediaView: View {
@StateObject
private var viewModel = MediaViewModel()
@ViewBuilder
private var contentView: some View {
CollectionVGrid(
$viewModel.mediaItems,

View File

@ -21,6 +21,7 @@ struct ProgramsView: View {
@StateObject
private var programsViewModel = ProgramsViewModel()
@ViewBuilder
private var contentView: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 20) {

View File

@ -26,6 +26,7 @@ struct SearchView: View {
@State
private var searchQuery = ""
@ViewBuilder
private var suggestionsView: some View {
VStack(spacing: 20) {
ForEach(viewModel.suggestions) { item in
@ -38,6 +39,7 @@ struct SearchView: View {
}
}
@ViewBuilder
private var resultsView: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 20) {

View File

@ -48,6 +48,7 @@ extension SelectUserView {
return isSelected ? .primary : .secondary
}
@ViewBuilder
private var personView: some View {
ZStack {
Color.secondarySystemFill

View File

@ -29,6 +29,7 @@ extension UserSignInView {
self.action = action
}
@ViewBuilder
private var personView: some View {
ZStack {
Color.secondarySystemFill

View File

@ -702,6 +702,7 @@
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; };
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; };
E1A8FDEC2C0574A800D0A51C /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A8FDEB2C0574A800D0A51C /* ListRow.swift */; };
E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButton.swift */; };
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDto.swift */; };
@ -1378,6 +1379,7 @@
E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = "<group>"; };
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; };
E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
E1A8FDEB2C0574A800D0A51C /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
E1AA331E2782639D00F6439C /* OverlayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayType.swift; sourceTree = "<group>"; };
E1AD104C26D96CE3003E4A08 /* BaseItemDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDto.swift; sourceTree = "<group>"; };
@ -2073,6 +2075,7 @@
E178B0752BE435D70023651B /* HourMinutePicker.swift */,
E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */,
4E16FD4E2C0183B500110147 /* LetterPickerBar */,
E1A8FDEB2C0574A800D0A51C /* ListRow.swift */,
E1AEFA362BE317E200CFAFD8 /* ListRowButton.swift */,
E1FE69AF28C2DA4A0021BC93 /* NavigationBarFilterDrawer */,
E1DE84132B9531C1008CCE21 /* OrderedSectionSelectorView.swift */,
@ -4361,6 +4364,7 @@
E1BAFE102BE921270069C4D7 /* SwiftfinApp+ValueObservation.swift in Sources */,
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
E10B1E8F2BD7728400A92EAF /* QuickConnectView.swift in Sources */,
E1A8FDEC2C0574A800D0A51C /* ListRow.swift in Sources */,
E1DD55372B6EE533007501C0 /* Task.swift in Sources */,
E1194F4E2BEABA9100888DB6 /* NavigationBarCloseButton.swift in Sources */,
E113133428BE988200930F75 /* NavigationBarFilterDrawer.swift in Sources */,

View File

@ -0,0 +1,77 @@
//
// 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) 2024 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: come up with better name along with `ListRowButton`
// Meant to be used when making a custom list without `List` or `Form`
struct ListRow<Leading: View, Content: View>: View {
@State
private var contentSize: CGSize = .zero
private let leading: () -> Leading
private let content: () -> Content
private var action: () -> Void
private var insets: EdgeInsets
private var isSeparatorVisible: Bool
var body: some View {
ZStack(alignment: .bottomTrailing) {
Button {
action()
} label: {
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
leading()
content()
.frame(maxHeight: .infinity)
.trackingSize($contentSize)
}
.padding(.top, insets.top)
.padding(.bottom, insets.bottom)
.padding(.leading, insets.leading)
.padding(.trailing, insets.trailing)
}
.foregroundStyle(.primary, .secondary)
Color.secondarySystemFill
.frame(width: contentSize.width, height: 1)
.padding(.trailing, insets.trailing)
.visible(isSeparatorVisible)
}
}
}
extension ListRow {
init(
insets: EdgeInsets = .zero,
@ViewBuilder leading: @escaping () -> Leading,
@ViewBuilder content: @escaping () -> Content
) {
self.init(
leading: leading,
content: content,
action: {},
insets: insets,
isSeparatorVisible: true
)
}
func isSeparatorVisible(_ isVisible: Bool) -> Self {
copy(modifying: \.isSeparatorVisible, with: isVisible)
}
func onSelect(perform action: @escaping () -> Void) -> Self {
copy(modifying: \.action, with: action)
}
}

View File

@ -8,6 +8,9 @@
import SwiftUI
// TODO: come up with better name along with `ListRow`
// Meant to be used within `List` or `Form`
struct ListRowButton: View {
let title: String

View File

@ -95,6 +95,7 @@ struct ChannelLibraryView: View {
}
}
@ViewBuilder
private var contentView: some View {
CollectionVGrid(
$viewModel.elements,

View File

@ -32,6 +32,7 @@ extension ChannelLibraryView {
private var onSelect: () -> Void
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
@ViewBuilder
private var channelLogo: some View {
VStack {
ZStack {

View File

@ -93,6 +93,7 @@ struct ConnectToServerView: View {
.buttonStyle(.plain)
}
@ViewBuilder
private var localServersSection: some View {
Section(L10n.localServers) {
if viewModel.localServers.isEmpty {

View File

@ -29,6 +29,7 @@ struct HomeView: View {
@StateObject
private var viewModel = HomeViewModel()
@ViewBuilder
private var contentView: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {

View File

@ -97,6 +97,7 @@ extension ItemView {
return CGSize(width: width, height: height)
}
@ViewBuilder
private var imageView: some View {
ZStack {
Color.clear

View File

@ -62,6 +62,7 @@ extension MediaView {
}
}
@ViewBuilder
private var titleLabel: some View {
Text(mediaType.displayTitle)
.font(.title2)

View File

@ -33,6 +33,7 @@ struct MediaView: View {
.columns(2)
}
@ViewBuilder
private var contentView: some View {
CollectionVGrid(
$viewModel.mediaItems,

View File

@ -18,7 +18,7 @@ extension PagingLibraryView {
private var contentWidth: CGFloat = 0
private let item: Element
private var onSelect: () -> Void
private var action: () -> Void
private let posterType: PosterDisplayType
private func imageView(from element: Element) -> ImageView {
@ -68,54 +68,51 @@ extension PagingLibraryView {
}
}
@ViewBuilder
private var rowContent: some View {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(item.displayTitle)
.font(posterType == .landscape ? .subheadline : .callout)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(2)
.multilineTextAlignment(.leading)
accessoryView
.font(.caption)
.foregroundColor(Color(UIColor.lightGray))
}
Spacer()
}
}
@ViewBuilder
private var rowLeading: some View {
ZStack {
Color.clear
imageView(from: item)
.failure {
SystemImageContentView(systemName: item.systemImage)
}
}
.posterStyle(posterType)
.frame(width: posterType == .landscape ? 110 : 60)
.posterShadow()
.padding(.vertical, 8)
}
// MARK: body
var body: some View {
ZStack(alignment: .bottomTrailing) {
Button {
onSelect()
} label: {
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
ZStack {
Color.clear
imageView(from: item)
.failure {
SystemImageContentView(systemName: item.systemImage)
}
}
.posterStyle(posterType)
.frame(width: posterType == .landscape ? 110 : 60)
.posterShadow()
.padding(.vertical, 8)
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(item.displayTitle)
.font(posterType == .landscape ? .subheadline : .callout)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(2)
.multilineTextAlignment(.leading)
accessoryView
.font(.caption)
.foregroundColor(Color(UIColor.lightGray))
}
Spacer()
}
.frame(maxWidth: .infinity)
.onSizeChanged { newSize in
contentWidth = newSize.width
}
}
}
Color.secondarySystemFill
.frame(width: contentWidth, height: 1)
ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) {
rowLeading
} content: {
rowContent
}
.edgePadding(.horizontal)
.onSelect(perform: action)
}
}
}
@ -125,12 +122,12 @@ extension PagingLibraryView.LibraryRow {
init(item: Element, posterType: PosterDisplayType) {
self.init(
item: item,
onSelect: {},
action: {},
posterType: posterType
)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
func onSelect(perform action: @escaping () -> Void) -> Self {
copy(modifying: \.action, with: action)
}
}

View File

@ -31,6 +31,7 @@ struct ProgramsView: View {
}
}
@ViewBuilder
private var liveTVSectionScrollView: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
@ -47,6 +48,7 @@ struct ProgramsView: View {
// TODO: probably make own pill view
// - see if could merge with item view pills
@ViewBuilder
private func liveTVSectionPill(title: String, systemImage: String, onSelect: @escaping () -> Void) -> some View {
Button {
onSelect()
@ -62,6 +64,7 @@ struct ProgramsView: View {
}
}
@ViewBuilder
private var contentView: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 20) {

View File

@ -40,6 +40,7 @@ struct SearchView: View {
}
}
@ViewBuilder
private var suggestionsView: some View {
VStack(spacing: 20) {
ForEach(viewModel.suggestions) { item in
@ -50,6 +51,7 @@ struct SearchView: View {
}
}
@ViewBuilder
private var resultsView: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 20) {

View File

@ -44,6 +44,7 @@ extension SelectUserView {
self.servers = servers
}
@ViewBuilder
private var content: some View {
VStack(alignment: .center) {
ZStack {

View File

@ -44,39 +44,54 @@ extension SelectUserView {
self.servers = servers
}
private var content: some View {
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
@ViewBuilder
private var rowContent: some View {
HStack {
ZStack {
Group {
if colorScheme == .light {
Color.secondarySystemFill
} else {
Color.tertiarySystemBackground
}
Text("Add User")
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(isEnabled ? .primary : .secondary)
.lineLimit(2)
.multilineTextAlignment(.leading)
Spacer()
}
}
@ViewBuilder
private var rowLeading: some View {
ZStack {
Group {
if colorScheme == .light {
Color.secondarySystemFill
} else {
Color.tertiarySystemBackground
}
.posterShadow()
RelativeSystemImageView(systemName: "plus")
.foregroundStyle(.secondary)
}
.aspectRatio(1, contentMode: .fill)
.clipShape(.circle)
.frame(width: 80)
.padding(.vertical, 8)
.posterShadow()
HStack {
RelativeSystemImageView(systemName: "plus")
.foregroundStyle(.secondary)
}
.aspectRatio(1, contentMode: .fill)
.clipShape(.circle)
.frame(width: 80)
.padding(.vertical, 8)
}
Text("Add User")
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(isEnabled ? .primary : .secondary)
.lineLimit(2)
.multilineTextAlignment(.leading)
Spacer()
@ViewBuilder
private var content: some View {
ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) {
rowLeading
} content: {
rowContent
}
.isSeparatorVisible(false)
.onSelect {
if let selectedServer {
action(selectedServer)
}
.frame(maxWidth: .infinity)
}
}
@ -100,15 +115,9 @@ extension SelectUserView {
.disabled(!isEnabled)
.foregroundStyle(.primary, .secondary)
} else {
Button {
if let selectedServer {
action(selectedServer)
}
} label: {
content
}
.disabled(!isEnabled)
.foregroundStyle(.primary, .secondary)
content
.disabled(!isEnabled)
.foregroundStyle(.primary, .secondary)
}
}
}

View File

@ -50,6 +50,7 @@ extension SelectUserView {
return isSelected ? .primary : .secondary
}
@ViewBuilder
private var personView: some View {
ZStack {
Group {

View File

@ -24,9 +24,6 @@ extension SelectUserView {
@Environment(\.isSelected)
private var isSelected
@State
private var contentSize: CGSize = .zero
private let user: UserState
private let server: ServerState
private let showServer: Bool
@ -53,6 +50,7 @@ extension SelectUserView {
return isSelected ? .primary : .secondary
}
@ViewBuilder
private var personView: some View {
ZStack {
Group {
@ -98,73 +96,62 @@ extension SelectUserView {
.clipShape(.circle)
}
@ViewBuilder
private var rowContent: some View {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(user.username)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(labelForegroundStyle)
.lineLimit(2)
.multilineTextAlignment(.leading)
if showServer {
Text(server.name)
.font(.footnote)
.foregroundColor(Color(UIColor.lightGray))
}
}
Spacer()
if isEditing, isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.backport
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: 24, height: 24)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
} else if isEditing {
Image(systemName: "circle")
.resizable()
.backport
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: 24, height: 24)
.foregroundStyle(.secondary)
}
}
}
var body: some View {
ZStack(alignment: .bottomTrailing) {
Button {
action()
} label: {
ZStack {
Color.clear
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
userImage
.frame(width: 80)
.padding(.vertical, 8)
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(user.username)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(labelForegroundStyle)
.lineLimit(2)
.multilineTextAlignment(.leading)
if showServer {
Text(server.name)
.font(.footnote)
.foregroundColor(Color(UIColor.lightGray))
}
}
Spacer()
if isEditing, isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.backport
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: 24, height: 24)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
} else if isEditing {
Image(systemName: "circle")
.resizable()
.backport
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: 24, height: 24)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity)
.trackingSize($contentSize)
}
}
ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) {
userImage
.frame(width: 80)
.padding(.vertical, 8)
} content: {
rowContent
}
.onSelect(perform: action)
.contextMenu {
Button("Delete", role: .destructive) {
onDelete()
}
.contextMenu {
Button("Delete", role: .destructive) {
onDelete()
}
}
.foregroundStyle(.primary, .secondary)
Color.secondarySystemFill
.frame(width: contentSize.width, height: 1)
}
}
}

View File

@ -317,7 +317,6 @@ struct SelectUserView: View {
listItemView(for: item)
}
}
.edgePadding()
}
}
@ -353,6 +352,7 @@ struct SelectUserView: View {
}
}
@ViewBuilder
private var deleteUsersButton: some View {
Button {
isPresentingConfirmDeleteUsers = true
@ -451,6 +451,7 @@ struct SelectUserView: View {
// MARK: emptyView
@ViewBuilder
private var emptyView: some View {
VStack(spacing: 10) {
L10n.connectToJellyfinServerStart.text

View File

@ -30,6 +30,7 @@ extension UserSignInView {
self.action = action
}
@ViewBuilder
private var personView: some View {
ZStack {
Group {