Skip to content

Commit

Permalink
Still VERY much a work in progress but I think this looks nice. The f…
Browse files Browse the repository at this point in the history
…ilter drawer is "done" but the spacing needs work. Next work be adding the Filter views that pop up and allow you to select them. Currently, selecting anything except for "Reset" will just crash Swiftfin since the views don't go anywhere.
  • Loading branch information
JPKribs committed Feb 27, 2025
1 parent ca2abcb commit 4cad34f
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 5 deletions.
6 changes: 6 additions & 0 deletions Shared/Coordinators/LibraryCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ final class LibraryCoordinator<Element: Poster>: NavigationCoordinatable {
var item = makeItem
@Route(.push)
var library = makeLibrary
@Route(.fullScreen)
var filter = makeFilter
#else
@Route(.push)
var item = makeItem
Expand Down Expand Up @@ -52,6 +54,10 @@ final class LibraryCoordinator<Element: Poster>: NavigationCoordinatable {
func makeLibrary(viewModel: PagingLibraryViewModel<BaseItemDto>) -> NavigationViewCoordinator<LibraryCoordinator<BaseItemDto>> {
NavigationViewCoordinator(LibraryCoordinator<BaseItemDto>(viewModel: viewModel))
}

func makeFilter(parameters: FilterCoordinator.Parameters) -> NavigationViewCoordinator<FilterCoordinator> {
NavigationViewCoordinator(FilterCoordinator(parameters: parameters))
}
#else
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
Expand Down
21 changes: 20 additions & 1 deletion Shared/Objects/ItemFilter/ItemFilterType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ enum ItemFilterType: String, CaseIterable, Defaults.Serializable {
}
}

extension ItemFilterType: Displayable {
extension ItemFilterType: Displayable, SystemImageable {

var displayTitle: String {
switch self {
Expand All @@ -68,4 +68,23 @@ extension ItemFilterType: Displayable {
L10n.years
}
}

var systemImage: String {
switch self {
case .genres:
"theatermasks"
case .letter:
"character"
case .sortBy:
"line.3.horizontal.decrease"
case .sortOrder:
"arrow.up.arrow.down"
case .tags:
"tag"
case .traits:
"arrowtriangle.down"
case .years:
"calendar"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// 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 LeadingBarFilterDrawer {

struct FilterDrawerButton: View {

// MARK: - Defaults

@Default(.accentColor)
private var accentColor

// MARK: - Environment Variables

@Environment(\.isSelected)
private var isSelected

// MARK: - Focus State

@FocusState
private var isFocused: Bool

// MARK: - Button Variables

private let systemName: String?
private let title: String
private var onSelect: () -> Void

// MARK: - Collapsing Variables

private let expandedWidth: CGFloat
private let collapsedWidth: CGFloat = 75

// MARK: - Initializer

init(systemName: String?, title: String, expandedWidth: CGFloat, onSelect: @escaping () -> Void) {
self.systemName = systemName
self.title = title
self.expandedWidth = expandedWidth
self.onSelect = onSelect
}

// MARK: - Body

var body: some View {
Button {
onSelect()
} label: {
HStack(spacing: 8) {
if let systemName = systemName {
Image(systemName: systemName)
.frame(width: collapsedWidth, alignment: .center)
.focusable(false)
}
if isFocused {
Text(title)
.transition(.move(edge: .leading).combined(with: .opacity))
Spacer(minLength: 0)
}
}
.font(.footnote.weight(.semibold))
.foregroundColor(isFocused ? .primary : .secondary)
.frame(
width: isFocused ? expandedWidth : collapsedWidth,
height: collapsedWidth,
alignment: .leading
)
.background {
Capsule()
.foregroundColor(isSelected ? accentColor : Color.secondarySystemFill)
.brightness(isFocused ? 0.25 : 0)
.opacity(0.5)
}
.overlay {
Capsule()
.stroke(isSelected ? accentColor : Color.secondarySystemFill, lineWidth: 1)
.brightness(isFocused ? 0.25 : 0)
}
.animation(.easeInOut(duration: 0.25), value: isFocused)
}
.frame(width: collapsedWidth, height: collapsedWidth, alignment: .leading)
.buttonStyle(.borderless)
.focused($isFocused)
}
}
}

extension LeadingBarFilterDrawer.FilterDrawerButton {

init(systemName: String, title: String) {
self.init(
systemName: systemName,
title: title,
expandedWidth: 200,
onSelect: {}
)
}

func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// 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 JellyfinAPI
import SwiftUI

struct LeadingBarFilterDrawer: View {

@ObservedObject
private var viewModel: FilterViewModel

private var filterTypes: [ItemFilterType]
private var onSelect: (FilterCoordinator.Parameters) -> Void

var body: some View {
VStack {
if viewModel.currentFilters.hasFilters {
FilterDrawerButton(
systemName: "line.3.horizontal.decrease.circle.fill",
title: L10n.reset
)
.onSelect {
viewModel.send(.reset())
}
.environment(\.isSelected, true)
}

ForEach(filterTypes, id: \.self) { type in
FilterDrawerButton(
systemName: type.systemImage,
title: type.displayTitle
)
.onSelect {
onSelect(.init(type: type, viewModel: viewModel))
}
.environment(
\.isSelected,
viewModel.isFilterSelected(type: type)
)
}
}
.padding(.horizontal)
.padding(.vertical, 1)
}
}

extension LeadingBarFilterDrawer {

init(viewModel: FilterViewModel, types: [ItemFilterType]) {
self.init(
viewModel: viewModel,
filterTypes: types,
onSelect: { _ in }
)
}

func onSelect(_ action: @escaping (FilterCoordinator.Parameters) -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}
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

struct LeadingBarFilterDrawerModifier<Filters: View>: ViewModifier {
let filters: () -> Filters

// Define collapsed and expanded widths
private let collapsedWidth: CGFloat = 75
private let expandedWidth: CGFloat = 200

// Use @State to track focus state and determine current width
@State private var isExpanded: Bool = false

private var filterDrawerWidth: CGFloat {
isExpanded ? expandedWidth : collapsedWidth
}

func body(content: Content) -> some View {
ZStack(alignment: .leading) {
content
.padding(.leading, filterDrawerWidth + 10)

filters()
.padding(.leading, 10)
}
}
}
24 changes: 24 additions & 0 deletions Swiftfin tvOS/Extensions/View/View-tvOS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,28 @@ extension View {
)
)
}

@ViewBuilder
func leadingBarFilterDrawer<Filters: View>(@ViewBuilder _ filters: @escaping () -> Filters) -> some View {
modifier(LeadingBarFilterDrawerModifier(filters: filters))
}

@ViewBuilder
func leadingBarFilterDrawer(
viewModel: FilterViewModel,
types: [ItemFilterType],
onSelect: @escaping (FilterCoordinator.Parameters) -> Void
) -> some View {
if types.isEmpty {
self
} else {
leadingBarFilterDrawer {
LeadingBarFilterDrawer(
viewModel: viewModel,
types: types
)
.onSelect(onSelect)
}
}
}
}
8 changes: 8 additions & 0 deletions Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,14 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
innerContent
}
}
.ifLet(viewModel.filterViewModel) { view, filterViewModel in
view.leadingBarFilterDrawer(
viewModel: filterViewModel,
types: enabledDrawerFilters
) {
router.route(to: \.filter, $0)
}
}
// These exist here to alleviate type-checker issues
.onChange(of: posterType) {
setCustomLayout()
Expand Down
Loading

0 comments on commit 4cad34f

Please sign in to comment.