// // Theme+UI.swift // Passepartout // // Created by Davide De Rosa on 8/28/24. // Copyright (c) 2024 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn // // This file is part of Passepartout. // // Passepartout is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Passepartout is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Passepartout. If not, see . // import SwiftUI import UtilsLibrary // MARK: - Modifiers struct ThemeWindowModifier: ViewModifier { let size: CGSize } struct ThemeNavigationDetailModifier: ViewModifier { } struct ThemeFormModifier: ViewModifier { func body(content: Content) -> some View { content .formStyle(.grouped) } } struct ThemeBooleanModalModifier: ViewModifier where Modal: View { @EnvironmentObject private var theme: Theme @Binding var isPresented: Bool let isRoot: Bool let isInteractive: Bool let modal: () -> Modal func body(content: Content) -> some View { content .sheet(isPresented: $isPresented) { modal() .frame(minWidth: modalSize?.width, minHeight: modalSize?.height) .interactiveDismissDisabled(!isInteractive) } } private var modalSize: CGSize? { isRoot ? theme.rootModalSize : theme.secondaryModalSize } } struct ThemeItemModalModifier: ViewModifier where Modal: View, T: Identifiable { @EnvironmentObject private var theme: Theme @Binding var item: T? let isRoot: Bool let isInteractive: Bool let modal: (T) -> Modal func body(content: Content) -> some View { content .sheet(item: $item) { modal($0) .frame(minWidth: modalSize?.width, minHeight: modalSize?.height) .interactiveDismissDisabled(!isInteractive) } } private var modalSize: CGSize? { isRoot ? theme.rootModalSize : theme.secondaryModalSize } } struct ThemePlainButtonModifier: ViewModifier { let action: () -> Void } struct ThemeManualInputModifier: ViewModifier { } struct ThemeEmptyMessageModifier: ViewModifier { @EnvironmentObject private var theme: Theme func body(content: Content) -> some View { VStack { Spacer() content .font(theme.emptyMessageFont) .foregroundStyle(theme.emptyMessageColor) Spacer() } } } struct ThemeErrorModifier: ViewModifier { @EnvironmentObject private var theme: Theme let isError: Bool func body(content: Content) -> some View { content .foregroundStyle(isError ? theme.errorColor : theme.titleColor) } } struct ThemeAnimationModifier: ViewModifier where T: Equatable { @EnvironmentObject private var theme: Theme let value: T let category: ThemeAnimationCategory func body(content: Content) -> some View { content .animation(theme.animation(for: category), value: value) } } struct ThemeSectionWithFooterModifier: ViewModifier { let footer: String? } struct ThemeGridSectionModifier: ViewModifier { @EnvironmentObject private var theme: Theme let title: String? func body(content: Content) -> some View { if let title { Text(title) .font(theme.gridHeaderStyle) .fontWeight(theme.relevantWeight) .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading) .padding(.bottom, theme.gridHeaderBottom) } content .padding(.bottom) .padding(.bottom) } } struct ThemeGridCellModifier: ViewModifier { @EnvironmentObject private var theme: Theme let isSelected: Bool func body(content: Content) -> some View { content .padding() .background(isSelected ? theme.gridCellActiveColor : theme.gridCellColor) .clipShape(.rect(cornerRadius: theme.gridRadius)) } } struct ThemeHoverListRowModifier: ViewModifier { func body(content: Content) -> some View { content .frame(maxHeight: .infinity) .listRowInsets(.init()) } } // MARK: - Views public enum ThemeAnimationCategory: CaseIterable { case profiles case profilesLayout case modules case diagnostics } struct ThemeImage: View { @EnvironmentObject private var theme: Theme private let name: Theme.ImageName init(_ name: Theme.ImageName) { self.name = name } var body: some View { Image(systemName: theme.systemImage(name)) } } struct ThemeImageLabel: View { @EnvironmentObject private var theme: Theme private let title: String private let name: Theme.ImageName init(_ title: String, _ name: Theme.ImageName) { self.title = title self.name = name } var body: some View { Label { Text(title) } icon: { ThemeImage(name) } } } struct ThemeCopiableText: View { @EnvironmentObject private var theme: Theme var title: String? let value: String var body: some View { HStack { if let title { Text(title) Spacer() } Text(value) .foregroundStyle(title == nil ? theme.titleColor : theme.valueColor) .themeTruncating() if title == nil { Spacer() } Button { copyToPasteboard(value) } label: { ThemeImage(.copy) } // TODO: #584 menu, necessary to avoid cell selection .buttonStyle(.borderless) } } } struct ThemeTappableText: View { let title: String let action: () -> Void var commonView: some View { Button(action: action) { Text(title) .themeTruncating() } } } struct ThemeTextField: View { let title: String? @Binding var text: String let placeholder: String init(_ title: String, text: Binding, placeholder: String) { self.title = title _text = text self.placeholder = placeholder } @ViewBuilder var commonView: some View { if let title { LabeledContent { fieldView } label: { Text(title) } } else { fieldView } } private var fieldView: some View { TextField(title ?? "", text: $text, prompt: Text(placeholder)) } } struct ThemeSecureField: View { let title: String? @Binding var text: String let placeholder: String @ViewBuilder var commonView: some View { if let title { LabeledContent { fieldView } label: { Text(title) } } else { fieldView } } private var fieldView: some View { RevealingSecureField(title ?? "", text: $text, prompt: Text(placeholder), imageWidth: 30.0) { ThemeImage(.hide) .foregroundStyle(Color.accentColor) } revealImage: { ThemeImage(.show) .foregroundStyle(Color.accentColor) } } } struct ThemeRemovableItemRow: View where ItemView: View { let isEditing: Bool @ViewBuilder let itemView: () -> ItemView let removeAction: () -> Void var body: some View { RemovableItemRow( isEditing: isEditing, itemView: itemView, removeView: removeView ) } } enum ThemeEditableListSection { struct RemoveLabel: View { let action: () -> Void } struct EditLabel: View { } }