//
// 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 {
}
}