Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] [tvOS] Letter Picker / Filters #1407

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// 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 LetterPickerBar {

struct LetterPickerButton: View {

@Default(.accentColor)
private var accentColor

@Environment(\.isSelected)
private var isSelected

@FocusState
private var isFocused

private let letter: ItemLetter
private let size: CGFloat
private let viewModel: FilterViewModel

init(letter: ItemLetter, size: CGFloat, viewModel: FilterViewModel) {
self.letter = letter
self.size = size
self.viewModel = viewModel
}

var body: some View {
Button {
if viewModel.currentFilters.letter.contains(letter) {
viewModel.send(.update(.letter, []))
} else {
viewModel.send(.update(.letter, [ItemLetter(stringLiteral: letter.value).asAnyItemFilter]))
}
} label: {
ZStack {
RoundedRectangle(cornerRadius: 5)
.foregroundStyle(isFocused ? .lightGray : (isSelected ? accentColor : .clear))
.shadow(color: isFocused || isSelected ? .black : .clear, radius: 2, x: 0, y: 2)
.frame(width: size, height: size)

Text(letter.value)
.font(.footnote.weight(.regular))
.foregroundStyle(isFocused ? .black : (isSelected ? .white : .white))
.shadow(color: isFocused || isSelected ? .clear : .black, radius: 2, x: 0, y: 2)
.frame(width: size, height: size, alignment: .center)
}
}
.buttonStyle(.borderless)
.focused($isFocused)
}
}
}
49 changes: 49 additions & 0 deletions Swiftfin tvOS/Components/LetterPickerBar/LetterPickerBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// 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

struct LetterPickerBar: View {

@ObservedObject
var viewModel: FilterViewModel

// MARK: - Body

@ViewBuilder
var body: some View {
VStack(spacing: 0) {
Spacer()

ForEach(ItemLetter.allCases, id: \.hashValue) { filterLetter in
LetterPickerButton(
letter: filterLetter,
size: LetterPickerBar.size,
viewModel: viewModel
)
.environment(\.isSelected, viewModel.currentFilters.letter.contains(filterLetter))
}

Spacer()
}
.scrollIfLargerThanContainer()
.frame(width: LetterPickerBar.size, alignment: .center)
}

// MARK: - Letter Button Size

static var size: CGFloat {
String().height(
withConstrainedWidth: CGFloat.greatestFiniteMagnitude,
font: UIFont.preferredFont(
forTextStyle: .footnote
)
)
}
}
72 changes: 31 additions & 41 deletions Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
@Default
private var defaultPosterType: PosterDisplayType

@Default(.Customization.Library.letterPickerEnabled)
private var letterPickerEnabled
@Default(.Customization.Library.letterPickerOrientation)
private var letterPickerOrientation

@EnvironmentObject
private var router: LibraryCoordinator<Element>.Router

Expand Down Expand Up @@ -275,7 +280,7 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
listItemView(item: item, posterType: posterType)
}
}
.onReachedBottomEdge(offset: .rows(3)) {
.onReachedBottomEdge(offset: .offset(300)) {
viewModel.send(.getNextPage)
}
.proxy(collectionVGridProxy)
Expand All @@ -300,49 +305,34 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
}
}

// MARK: Content View

@ViewBuilder
private var contentView: some View {

innerContent
// These exist here to alleviate type-checker issues
.onChange(of: posterType) {
setCustomLayout()
Group {
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)
}
.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()
}*/
} else {
innerContent
}
}
// These exist here to alleviate type-checker issues
.onChange(of: posterType) {
setCustomLayout()
}
.onChange(of: displayType) {
setCustomLayout()
}
.onChange(of: listColumnCount) {
setCustomLayout()
}
}

// MARK: Body
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// 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 FiltersSection: View {

@Default(.Customization.Library.letterPickerEnabled)
var letterPickerEnabled
@Default(.Customization.Library.letterPickerOrientation)
var letterPickerOrientation

var body: some View {
Section(L10n.filters) {

Toggle(L10n.letterPicker, isOn: $letterPickerEnabled)

if letterPickerEnabled {
InlineEnumToggle(
title: L10n.orientation,
selection: $letterPickerOrientation
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ struct CustomizeViewsSettings: View {
Toggle(L10n.showMissingEpisodes, isOn: $shouldShowMissingEpisodes)
}

FiltersSection()

Section(L10n.posters) {

ChevronButton(L10n.indicators)
Expand Down
28 changes: 28 additions & 0 deletions Swiftfin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@
4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; };
4EE767082D13403F009658F0 /* RemoteSearchResultRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */; };
4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */; };
4EEACAA42D420FEF00F1D54D /* LetterPickerBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEACAA22D420FEF00F1D54D /* LetterPickerBar.swift */; };
4EEACAA52D420FEF00F1D54D /* LetterPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEACAA02D420FEF00F1D54D /* LetterPickerButton.swift */; };
4EEACAA72D4210FD00F1D54D /* FiltersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEACAA62D4210F900F1D54D /* FiltersSection.swift */; };
4EECA4E32D2C7D530080A863 /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */; };
4EECA4E62D2C7D650080A863 /* PhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */; };
4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */; };
Expand Down Expand Up @@ -1448,6 +1451,9 @@
4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResult.swift; sourceTree = "<group>"; };
4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResultRow.swift; sourceTree = "<group>"; };
4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResultView.swift; sourceTree = "<group>"; };
4EEACAA02D420FEF00F1D54D /* LetterPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerButton.swift; sourceTree = "<group>"; };
4EEACAA22D420FEF00F1D54D /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = "<group>"; };
4EEACAA62D4210F900F1D54D /* FiltersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersSection.swift; sourceTree = "<group>"; };
4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = "<group>"; };
4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCropView.swift; sourceTree = "<group>"; };
4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImageCropView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2616,6 +2622,7 @@
4E699BBE2CB3474C007CBD5D /* Sections */ = {
isa = PBXGroup;
children = (
4EEACAA62D4210F900F1D54D /* FiltersSection.swift */,
4E699BBF2CB34775007CBD5D /* HomeSection.swift */,
4E97D1822D064748004B89AD /* ItemSection.swift */,
4EAE340B2D42B852006FBAD3 /* LibrarySection.swift */,
Expand Down Expand Up @@ -2989,6 +2996,23 @@
path = Components;
sourceTree = "<group>";
};
4EEACAA12D420FEF00F1D54D /* Components */ = {
isa = PBXGroup;
children = (
4EEACAA02D420FEF00F1D54D /* LetterPickerButton.swift */,
);
path = Components;
sourceTree = "<group>";
};
4EEACAA32D420FEF00F1D54D /* LetterPickerBar */ = {
isa = PBXGroup;
children = (
4EEACAA12D420FEF00F1D54D /* Components */,
4EEACAA22D420FEF00F1D54D /* LetterPickerBar.swift */,
);
path = LetterPickerBar;
sourceTree = "<group>";
};
4EECA4E12D2C7D450080A863 /* PhotoPickerView */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3368,6 +3392,7 @@
4EF0DCA82D49751B005A5194 /* ErrorView.swift */,
E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */,
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */,
4EEACAA32D420FEF00F1D54D /* LetterPickerBar */,
E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */,
E10E842B29A589860064EA49 /* NonePosterButton.swift */,
4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */,
Expand Down Expand Up @@ -5977,6 +6002,8 @@
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
C46DD8D32A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift in Sources */,
E1549661296CA2EF00C4EF88 /* SwiftfinDefaults.swift in Sources */,
4EEACAA42D420FEF00F1D54D /* LetterPickerBar.swift in Sources */,
4EEACAA52D420FEF00F1D54D /* LetterPickerButton.swift in Sources */,
E158C8D12A31947500C527C5 /* MediaSourceInfoView.swift in Sources */,
E11BDF782B8513B40045C54A /* ItemGenre.swift in Sources */,
4E01446C2D0292E200193038 /* Trie.swift in Sources */,
Expand Down Expand Up @@ -6105,6 +6132,7 @@
BD0BA22C2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */,
E1575EA2293E7B1E001665B1 /* Color.swift in Sources */,
E12E30F5296392EC0022FAC9 /* EnumPickerView.swift in Sources */,
4EEACAA72D4210FD00F1D54D /* FiltersSection.swift in Sources */,
E1575E72293E77B5001665B1 /* Utilities.swift in Sources */,
E164A7F72BE4816500A54B18 /* SelectUserServerSelection.swift in Sources */,
E1575E84293E7A00001665B1 /* PrimaryAppIcon.swift in Sources */,
Expand Down