2024-09-23 13:02:26 +00:00
|
|
|
//
|
|
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
//
|
|
|
|
|
2024-09-25 17:32:07 +00:00
|
|
|
import CommonLibrary
|
2024-10-29 13:30:41 +00:00
|
|
|
#if canImport(LocalAuthentication)
|
2024-09-25 17:32:07 +00:00
|
|
|
import LocalAuthentication
|
2024-10-29 13:30:41 +00:00
|
|
|
#endif
|
2024-09-23 13:02:26 +00:00
|
|
|
import SwiftUI
|
|
|
|
import UtilsLibrary
|
|
|
|
|
|
|
|
// MARK: - Modifiers
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
#if !os(tvOS)
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
struct ThemeWindowModifier: ViewModifier {
|
|
|
|
let size: CGSize
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ThemeNavigationDetailModifier: ViewModifier {
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ThemeFormModifier: ViewModifier {
|
|
|
|
func body(content: Content) -> some View {
|
|
|
|
content
|
|
|
|
.formStyle(.grouped)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ThemeBooleanModalModifier<Modal>: 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)
|
2024-10-30 09:18:39 +00:00
|
|
|
.themeLockScreen(theme)
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var modalSize: CGSize? {
|
|
|
|
isRoot ? theme.rootModalSize : theme.secondaryModalSize
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ThemeItemModalModifier<Modal, T>: 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)
|
2024-10-30 09:18:39 +00:00
|
|
|
.themeLockScreen(theme)
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var modalSize: CGSize? {
|
|
|
|
isRoot ? theme.rootModalSize : theme.secondaryModalSize
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-31 09:02:21 +00:00
|
|
|
struct ThemeBooleanPopoverModifier<Popover>: ViewModifier, SizeClassProviding where Popover: View {
|
2024-10-18 16:12:28 +00:00
|
|
|
|
|
|
|
@EnvironmentObject
|
|
|
|
private var theme: Theme
|
|
|
|
|
2024-10-26 18:28:02 +00:00
|
|
|
@Environment(\.horizontalSizeClass)
|
2024-10-31 09:02:21 +00:00
|
|
|
var hsClass
|
2024-10-26 18:28:02 +00:00
|
|
|
|
|
|
|
@Environment(\.verticalSizeClass)
|
2024-10-31 09:02:21 +00:00
|
|
|
var vsClass
|
2024-10-26 18:28:02 +00:00
|
|
|
|
2024-10-18 16:12:28 +00:00
|
|
|
@Binding
|
|
|
|
var isPresented: Bool
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
let popover: Popover
|
|
|
|
|
|
|
|
func body(content: Content) -> some View {
|
2024-10-31 09:02:21 +00:00
|
|
|
if isBigDevice {
|
2024-10-26 18:28:02 +00:00
|
|
|
content
|
|
|
|
.popover(isPresented: $isPresented) {
|
|
|
|
popover
|
|
|
|
.frame(minWidth: theme.popoverSize?.width, minHeight: theme.popoverSize?.height)
|
2024-10-30 09:18:39 +00:00
|
|
|
.themeLockScreen(theme)
|
2024-10-26 18:28:02 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
content
|
|
|
|
.sheet(isPresented: $isPresented) {
|
|
|
|
popover
|
2024-10-30 09:18:39 +00:00
|
|
|
.themeLockScreen(theme)
|
2024-10-26 18:28:02 +00:00
|
|
|
}
|
|
|
|
}
|
2024-10-18 16:12:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-03 21:25:51 +00:00
|
|
|
struct ThemeConfirmationModifier: ViewModifier {
|
|
|
|
|
|
|
|
@Binding
|
|
|
|
var isPresented: Bool
|
|
|
|
|
|
|
|
let title: String
|
|
|
|
|
2024-10-30 13:53:35 +00:00
|
|
|
let isDestructive: Bool
|
|
|
|
|
2024-10-03 21:25:51 +00:00
|
|
|
let action: () -> Void
|
|
|
|
|
|
|
|
func body(content: Content) -> some View {
|
|
|
|
content
|
2024-10-30 13:53:35 +00:00
|
|
|
.confirmationDialog(title, isPresented: $isPresented, titleVisibility: .visible) {
|
|
|
|
Button(Strings.Theme.Confirmation.ok, role: isDestructive ? .destructive : nil, action: action)
|
2024-10-30 14:20:18 +00:00
|
|
|
Text(Strings.Theme.Confirmation.cancel)
|
2024-10-03 21:25:51 +00:00
|
|
|
} message: {
|
|
|
|
Text(Strings.Theme.Confirmation.message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-28 10:47:33 +00:00
|
|
|
struct ThemeNavigationStackModifier: ViewModifier {
|
|
|
|
|
|
|
|
@Environment(\.dismiss)
|
|
|
|
private var dismiss
|
|
|
|
|
|
|
|
let condition: Bool
|
|
|
|
|
|
|
|
let closable: Bool
|
|
|
|
|
|
|
|
@Binding
|
|
|
|
var path: NavigationPath
|
|
|
|
|
|
|
|
func body(content: Content) -> some View {
|
|
|
|
if condition {
|
|
|
|
NavigationStack(path: $path) {
|
|
|
|
content
|
|
|
|
.toolbar {
|
|
|
|
if closable {
|
|
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
|
|
Button {
|
|
|
|
dismiss()
|
|
|
|
} label: {
|
|
|
|
ThemeImage(.close)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
content
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
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<T>: 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-28 20:38:26 +00:00
|
|
|
struct ThemeTrailingValueModifier: ViewModifier {
|
|
|
|
let value: CustomStringConvertible?
|
|
|
|
|
|
|
|
let truncationMode: Text.TruncationMode
|
|
|
|
|
|
|
|
func body(content: Content) -> some View {
|
|
|
|
LabeledContent {
|
|
|
|
if let value {
|
|
|
|
Spacer()
|
|
|
|
Text(value.description)
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
.lineLimit(1)
|
|
|
|
.truncationMode(truncationMode)
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
content
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-03 15:03:53 +00:00
|
|
|
struct ThemeSectionWithHeaderFooterModifier: ViewModifier {
|
|
|
|
let header: String?
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-25 17:32:07 +00:00
|
|
|
struct ThemeLockScreenModifier: ViewModifier {
|
|
|
|
|
|
|
|
@AppStorage(AppPreference.locksInBackground.key)
|
|
|
|
private var locksInBackground = false
|
|
|
|
|
2024-10-30 09:18:39 +00:00
|
|
|
@ObservedObject
|
|
|
|
var theme: Theme
|
|
|
|
|
2024-09-25 17:32:07 +00:00
|
|
|
func body(content: Content) -> some View {
|
|
|
|
LockableView(
|
2024-10-30 11:25:33 +00:00
|
|
|
locksInBackground: locksInBackground,
|
2024-09-25 17:32:07 +00:00
|
|
|
content: {
|
|
|
|
content
|
|
|
|
},
|
|
|
|
lockedContent: LogoView.init,
|
|
|
|
unlockBlock: Self.unlockScreenBlock
|
|
|
|
)
|
2024-10-30 09:18:39 +00:00
|
|
|
.environmentObject(theme)
|
2024-09-25 17:32:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private static func unlockScreenBlock() async -> Bool {
|
|
|
|
let context = LAContext()
|
|
|
|
let policy: LAPolicy = .deviceOwnerAuthentication
|
|
|
|
var error: NSError?
|
|
|
|
guard context.canEvaluatePolicy(policy, error: &error) else {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
let isAuthorized = try await context.evaluatePolicy(
|
|
|
|
policy,
|
2024-10-30 14:20:18 +00:00
|
|
|
localizedReason: Strings.Theme.LockScreen.reason
|
2024-09-25 17:32:07 +00:00
|
|
|
)
|
|
|
|
return isAuthorized
|
|
|
|
} catch {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-03 15:50:19 +00:00
|
|
|
struct ThemeTipModifier: ViewModifier {
|
|
|
|
let text: String
|
|
|
|
|
|
|
|
let edge: Edge
|
|
|
|
|
|
|
|
@State
|
|
|
|
private var isPresenting = false
|
|
|
|
|
|
|
|
func body(content: Content) -> some View {
|
|
|
|
HStack {
|
|
|
|
content
|
|
|
|
Button {
|
|
|
|
isPresenting = true
|
|
|
|
} label: {
|
|
|
|
ThemeImage(.tip)
|
|
|
|
}
|
2024-10-04 00:57:09 +00:00
|
|
|
.imageScale(.large)
|
2024-10-03 15:50:19 +00:00
|
|
|
.buttonStyle(.borderless)
|
2024-10-03 15:53:19 +00:00
|
|
|
.popover(isPresented: $isPresenting, arrowEdge: edge) {
|
|
|
|
VStack {
|
|
|
|
Text(text)
|
|
|
|
.font(.body)
|
|
|
|
.foregroundStyle(.primary)
|
|
|
|
.lineLimit(nil)
|
|
|
|
.multilineTextAlignment(.leading)
|
|
|
|
.frame(width: 150.0)
|
|
|
|
}
|
|
|
|
.padding(12)
|
2024-10-03 15:50:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
#endif
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
// MARK: - Views
|
|
|
|
|
|
|
|
public enum ThemeAnimationCategory: CaseIterable {
|
2024-10-10 22:24:06 +00:00
|
|
|
case diagnostics
|
|
|
|
|
|
|
|
case modules
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
case profiles
|
|
|
|
|
|
|
|
case profilesLayout
|
|
|
|
|
2024-10-10 22:24:06 +00:00
|
|
|
case providers
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeImage: View {
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
@EnvironmentObject
|
|
|
|
private var theme: Theme
|
|
|
|
|
|
|
|
private let name: Theme.ImageName
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public init(_ name: Theme.ImageName) {
|
2024-09-23 13:02:26 +00:00
|
|
|
self.name = name
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public var body: some View {
|
2024-10-26 19:51:18 +00:00
|
|
|
Image(systemName: theme.systemImageName(name))
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeImageLabel: View {
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
@EnvironmentObject
|
|
|
|
private var theme: Theme
|
|
|
|
|
|
|
|
private let title: String
|
|
|
|
|
|
|
|
private let name: Theme.ImageName
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public init(_ title: String, _ name: Theme.ImageName) {
|
2024-09-23 13:02:26 +00:00
|
|
|
self.title = title
|
|
|
|
self.name = name
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public var body: some View {
|
2024-09-23 13:02:26 +00:00
|
|
|
Label {
|
|
|
|
Text(title)
|
|
|
|
} icon: {
|
|
|
|
ThemeImage(name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeCountryFlag: View {
|
|
|
|
private let code: String?
|
|
|
|
|
|
|
|
private let placeholderTip: String?
|
|
|
|
|
|
|
|
private let countryTip: ((String) -> String?)?
|
|
|
|
|
2024-10-31 00:15:07 +00:00
|
|
|
public init(_ code: String?, placeholderTip: String? = nil, countryTip: ((String) -> String?)? = nil) {
|
2024-10-29 13:30:41 +00:00
|
|
|
self.code = code
|
|
|
|
self.placeholderTip = placeholderTip
|
|
|
|
self.countryTip = countryTip
|
|
|
|
}
|
|
|
|
|
|
|
|
public var body: some View {
|
2024-10-31 00:15:07 +00:00
|
|
|
if let code {
|
|
|
|
text(withString: code.asCountryCodeEmoji, tip: countryTip?(code))
|
|
|
|
} else {
|
|
|
|
text(withString: "🌐", tip: placeholderTip)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
private func text(withString string: String, tip: String?) -> some View {
|
|
|
|
if let tip {
|
|
|
|
Text(verbatim: string)
|
|
|
|
.help(tip)
|
|
|
|
} else {
|
|
|
|
Text(verbatim: string)
|
2024-10-29 13:30:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#if !os(tvOS)
|
|
|
|
|
|
|
|
public struct ThemeMenuImage: View {
|
2024-10-29 10:40:11 +00:00
|
|
|
|
|
|
|
@EnvironmentObject
|
|
|
|
private var theme: Theme
|
|
|
|
|
|
|
|
private let name: Theme.MenuImageName
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public init(_ name: Theme.MenuImageName) {
|
2024-10-29 10:40:11 +00:00
|
|
|
self.name = name
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public var body: some View {
|
2024-10-29 10:40:11 +00:00
|
|
|
Image(theme.menuImageName(name))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeDisclosableMenu<Content, Label>: View where Content: View, Label: View {
|
2024-10-22 13:06:13 +00:00
|
|
|
|
|
|
|
@ViewBuilder
|
2024-10-29 13:30:41 +00:00
|
|
|
private let content: () -> Content
|
2024-10-22 13:06:13 +00:00
|
|
|
|
|
|
|
@ViewBuilder
|
2024-10-29 13:30:41 +00:00
|
|
|
private let label: () -> Label
|
2024-10-22 13:06:13 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public init(content: @escaping () -> Content, label: @escaping () -> Label) {
|
|
|
|
self.content = content
|
|
|
|
self.label = label
|
|
|
|
}
|
|
|
|
|
|
|
|
public var body: some View {
|
2024-10-23 15:17:20 +00:00
|
|
|
Menu(content: content) {
|
2024-10-22 13:06:13 +00:00
|
|
|
HStack(alignment: .firstTextBaseline) {
|
2024-10-29 13:30:41 +00:00
|
|
|
label()
|
2024-10-22 13:06:13 +00:00
|
|
|
ThemeImage(.disclose)
|
|
|
|
}
|
|
|
|
.contentShape(.rect)
|
|
|
|
}
|
|
|
|
.foregroundStyle(.primary)
|
|
|
|
#if os(macOS)
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeCopiableText<Value, ValueView>: View where Value: CustomStringConvertible, ValueView: View {
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
@EnvironmentObject
|
|
|
|
private var theme: Theme
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
private let title: String?
|
|
|
|
|
|
|
|
private let value: Value
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
private let isMultiLine: Bool
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
private let valueView: (Value) -> ValueView
|
2024-10-04 08:37:10 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public init(
|
|
|
|
title: String? = nil,
|
|
|
|
value: Value,
|
|
|
|
isMultiLine: Bool = true,
|
|
|
|
valueView: @escaping (Value) -> ValueView
|
|
|
|
) {
|
|
|
|
self.title = title
|
|
|
|
self.value = value
|
|
|
|
self.isMultiLine = isMultiLine
|
|
|
|
self.valueView = valueView
|
|
|
|
}
|
2024-10-09 19:40:56 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public var body: some View {
|
2024-09-23 13:02:26 +00:00
|
|
|
HStack {
|
|
|
|
if let title {
|
|
|
|
Text(title)
|
|
|
|
Spacer()
|
|
|
|
}
|
2024-10-09 19:40:56 +00:00
|
|
|
valueView(value)
|
2024-09-23 13:02:26 +00:00
|
|
|
.foregroundStyle(title == nil ? theme.titleColor : theme.valueColor)
|
2024-10-04 08:37:10 +00:00
|
|
|
.themeMultiLine(isMultiLine)
|
2024-09-23 13:02:26 +00:00
|
|
|
if title == nil {
|
|
|
|
Spacer()
|
|
|
|
}
|
|
|
|
Button {
|
2024-10-11 17:45:58 +00:00
|
|
|
copyToPasteboard(value.description)
|
2024-09-23 13:02:26 +00:00
|
|
|
} label: {
|
|
|
|
ThemeImage(.copy)
|
|
|
|
}
|
2024-10-01 13:45:25 +00:00
|
|
|
// TODO: #584, necessary to avoid cell selection
|
2024-09-23 13:02:26 +00:00
|
|
|
.buttonStyle(.borderless)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeTappableText: View {
|
|
|
|
private let title: String
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
private let action: () -> Void
|
|
|
|
|
|
|
|
public init(title: String, action: @escaping () -> Void) {
|
|
|
|
self.title = title
|
|
|
|
self.action = action
|
|
|
|
}
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
var commonView: some View {
|
|
|
|
Button(action: action) {
|
|
|
|
Text(title)
|
|
|
|
.themeTruncating()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeTextField: View {
|
|
|
|
private let title: String?
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
@Binding
|
2024-10-29 13:30:41 +00:00
|
|
|
private var text: String
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
private let placeholder: String
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public init(_ title: String, text: Binding<String>, placeholder: String) {
|
2024-09-23 13:02:26 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeSecureField: View {
|
|
|
|
private let title: String?
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
@Binding
|
2024-10-29 13:30:41 +00:00
|
|
|
private var text: String
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
private let placeholder: String
|
|
|
|
|
|
|
|
public init(title: String?, text: Binding<String>, placeholder: String) {
|
|
|
|
self.title = title
|
|
|
|
_text = text
|
|
|
|
self.placeholder = placeholder
|
|
|
|
}
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
@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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct ThemeRemovableItemRow<ItemView>: View where ItemView: View {
|
|
|
|
private let isEditing: Bool
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
@ViewBuilder
|
2024-10-29 13:30:41 +00:00
|
|
|
private let itemView: () -> ItemView
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
let removeAction: () -> Void
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public init(
|
|
|
|
isEditing: Bool,
|
|
|
|
@ViewBuilder itemView: @escaping () -> ItemView,
|
|
|
|
removeAction: @escaping () -> Void
|
|
|
|
) {
|
|
|
|
self.isEditing = isEditing
|
|
|
|
self.itemView = itemView
|
|
|
|
self.removeAction = removeAction
|
|
|
|
}
|
|
|
|
|
|
|
|
public var body: some View {
|
2024-09-23 13:02:26 +00:00
|
|
|
RemovableItemRow(
|
|
|
|
isEditing: isEditing,
|
|
|
|
itemView: itemView,
|
|
|
|
removeView: removeView
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public enum ThemeEditableListSection {
|
|
|
|
public struct RemoveLabel: View {
|
2024-09-23 13:02:26 +00:00
|
|
|
let action: () -> Void
|
2024-10-29 13:30:41 +00:00
|
|
|
|
|
|
|
public init(action: @escaping () -> Void) {
|
|
|
|
self.action = action
|
|
|
|
}
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
|
2024-10-29 13:30:41 +00:00
|
|
|
public struct EditLabel: View {
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
2024-10-29 13:30:41 +00:00
|
|
|
|
|
|
|
#endif
|