349 lines
8.2 KiB
Swift
349 lines
8.2 KiB
Swift
|
//
|
||
|
// Theme+Views.swift
|
||
|
// Passepartout
|
||
|
//
|
||
|
// Created by Davide De Rosa on 11/1/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/>.
|
||
|
//
|
||
|
|
||
|
import SwiftUI
|
||
|
import UtilsLibrary
|
||
|
|
||
|
// MARK: Shortcuts
|
||
|
|
||
|
#if !os(tvOS)
|
||
|
extension Theme {
|
||
|
public func listSection<ItemView: View, T: EditableValue>(
|
||
|
_ title: String,
|
||
|
addTitle: String,
|
||
|
originalItems: Binding<[T]>,
|
||
|
emptyValue: (() async -> T)? = nil,
|
||
|
@ViewBuilder itemLabel: @escaping (Bool, Binding<T>) -> ItemView
|
||
|
) -> some View {
|
||
|
EditableListSection(
|
||
|
title,
|
||
|
addTitle: addTitle,
|
||
|
originalItems: originalItems,
|
||
|
emptyValue: emptyValue,
|
||
|
itemLabel: itemLabel,
|
||
|
removeLabel: ThemeEditableListSection.RemoveLabel.init(action:),
|
||
|
editLabel: ThemeEditableListSection.EditLabel.init
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
// MARK: - Views
|
||
|
|
||
|
public struct ThemeImage: View {
|
||
|
|
||
|
@EnvironmentObject
|
||
|
private var theme: Theme
|
||
|
|
||
|
private let name: Theme.ImageName
|
||
|
|
||
|
public init(_ name: Theme.ImageName) {
|
||
|
self.name = name
|
||
|
}
|
||
|
|
||
|
public var body: some View {
|
||
|
Image(systemName: theme.systemImageName(name))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct ThemeImageLabel: View {
|
||
|
|
||
|
@EnvironmentObject
|
||
|
private var theme: Theme
|
||
|
|
||
|
private let title: String
|
||
|
|
||
|
private let name: Theme.ImageName
|
||
|
|
||
|
public init(_ title: String, _ name: Theme.ImageName) {
|
||
|
self.title = title
|
||
|
self.name = name
|
||
|
}
|
||
|
|
||
|
public var body: some View {
|
||
|
Label {
|
||
|
Text(title)
|
||
|
} icon: {
|
||
|
ThemeImage(name)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct ThemeCountryFlag: View {
|
||
|
private let code: String?
|
||
|
|
||
|
private let placeholderTip: String?
|
||
|
|
||
|
private let countryTip: ((String) -> String?)?
|
||
|
|
||
|
public init(_ code: String?, placeholderTip: String? = nil, countryTip: ((String) -> String?)? = nil) {
|
||
|
self.code = code
|
||
|
self.placeholderTip = placeholderTip
|
||
|
self.countryTip = countryTip
|
||
|
}
|
||
|
|
||
|
public var body: some View {
|
||
|
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)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct ThemeTextField: View {
|
||
|
private let title: String?
|
||
|
|
||
|
@Binding
|
||
|
private var text: String
|
||
|
|
||
|
private let placeholder: String
|
||
|
|
||
|
public init(_ title: String, text: Binding<String>, 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))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct ThemeSecureField: View {
|
||
|
private let title: String?
|
||
|
|
||
|
@Binding
|
||
|
private var text: String
|
||
|
|
||
|
private let placeholder: String
|
||
|
|
||
|
public init(title: String?, text: Binding<String>, 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 {
|
||
|
RevealingSecureField(title ?? "", text: $text, prompt: Text(placeholder), imageWidth: 30.0) {
|
||
|
ThemeImage(.hide)
|
||
|
.foregroundStyle(Color.accentColor)
|
||
|
} revealImage: {
|
||
|
ThemeImage(.show)
|
||
|
.foregroundStyle(Color.accentColor)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#if !os(tvOS)
|
||
|
|
||
|
public struct ThemeMenuImage: View {
|
||
|
|
||
|
@EnvironmentObject
|
||
|
private var theme: Theme
|
||
|
|
||
|
private let name: Theme.MenuImageName
|
||
|
|
||
|
public init(_ name: Theme.MenuImageName) {
|
||
|
self.name = name
|
||
|
}
|
||
|
|
||
|
public var body: some View {
|
||
|
Image(theme.menuImageName(name))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct ThemeDisclosableMenu<Content, Label>: View where Content: View, Label: View {
|
||
|
|
||
|
@ViewBuilder
|
||
|
private let content: () -> Content
|
||
|
|
||
|
@ViewBuilder
|
||
|
private let label: () -> Label
|
||
|
|
||
|
public init(content: @escaping () -> Content, label: @escaping () -> Label) {
|
||
|
self.content = content
|
||
|
self.label = label
|
||
|
}
|
||
|
|
||
|
public var body: some View {
|
||
|
Menu(content: content) {
|
||
|
HStack(alignment: .firstTextBaseline) {
|
||
|
label()
|
||
|
ThemeImage(.disclose)
|
||
|
}
|
||
|
.contentShape(.rect)
|
||
|
}
|
||
|
.foregroundStyle(.primary)
|
||
|
#if os(macOS)
|
||
|
.buttonStyle(.plain)
|
||
|
#endif
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct ThemeCopiableText<Value, ValueView>: View where Value: CustomStringConvertible, ValueView: View {
|
||
|
|
||
|
@EnvironmentObject
|
||
|
private var theme: Theme
|
||
|
|
||
|
private let title: String?
|
||
|
|
||
|
private let value: Value
|
||
|
|
||
|
private let isMultiLine: Bool
|
||
|
|
||
|
private let valueView: (Value) -> ValueView
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
public var body: some View {
|
||
|
HStack {
|
||
|
if let title {
|
||
|
Text(title)
|
||
|
Spacer()
|
||
|
}
|
||
|
valueView(value)
|
||
|
.foregroundStyle(title == nil ? theme.titleColor : theme.valueColor)
|
||
|
.themeMultiLine(isMultiLine)
|
||
|
if title == nil {
|
||
|
Spacer()
|
||
|
}
|
||
|
Button {
|
||
|
copyToPasteboard(value.description)
|
||
|
} label: {
|
||
|
ThemeImage(.copy)
|
||
|
}
|
||
|
// TODO: #584, necessary to avoid cell selection
|
||
|
.buttonStyle(.borderless)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct ThemeTappableText: View {
|
||
|
private let title: String
|
||
|
|
||
|
private let action: () -> Void
|
||
|
|
||
|
public init(title: String, action: @escaping () -> Void) {
|
||
|
self.title = title
|
||
|
self.action = action
|
||
|
}
|
||
|
|
||
|
var commonView: some View {
|
||
|
Button(action: action) {
|
||
|
Text(title)
|
||
|
.themeTruncating()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct ThemeRemovableItemRow<ItemView>: View where ItemView: View {
|
||
|
private let isEditing: Bool
|
||
|
|
||
|
@ViewBuilder
|
||
|
private let itemView: () -> ItemView
|
||
|
|
||
|
let removeAction: () -> Void
|
||
|
|
||
|
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 {
|
||
|
RemovableItemRow(
|
||
|
isEditing: isEditing,
|
||
|
itemView: itemView,
|
||
|
removeView: removeView
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public enum ThemeEditableListSection {
|
||
|
public struct RemoveLabel: View {
|
||
|
let action: () -> Void
|
||
|
|
||
|
public init(action: @escaping () -> Void) {
|
||
|
self.action = action
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct EditLabel: View {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|