passepartout-apple/Passepartout/App/Constants/Theme.swift

649 lines
15 KiB
Swift
Raw Normal View History

2022-04-12 13:09:14 +00:00
//
// Theme.swift
// Passepartout
//
// Created by Davide De Rosa on 2/24/22.
2024-01-14 13:34:21 +00:00
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
2022-04-12 13:09:14 +00:00
//
// 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/>.
//
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
import LocalAuthentication
2023-12-16 19:58:54 +00:00
#endif
import PassepartoutLibrary
import SwiftUI
2022-04-12 13:09:14 +00:00
extension View {
var themeIdiom: UIUserInterfaceIdiom {
UIDevice.current.userInterfaceIdiom
}
2023-03-17 20:55:47 +00:00
2022-05-19 18:11:08 +00:00
var themeIsiPadPortrait: Bool {
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
#if targetEnvironment(macCatalyst)
false
#else
2022-05-19 18:11:08 +00:00
let device: UIDevice = .current
return device.userInterfaceIdiom == .pad && device.orientation.isPortrait
#endif
2023-12-16 19:58:54 +00:00
#else
false
#endif
}
2023-03-17 20:55:47 +00:00
2022-05-19 18:11:08 +00:00
var themeIsiPadMultitasking: Bool {
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
#if targetEnvironment(macCatalyst)
false
#else
UIDevice.current.userInterfaceIdiom == .pad
#endif
2023-12-16 19:58:54 +00:00
#else
false
#endif
}
2022-04-12 13:09:14 +00:00
}
// MARK: Global
2022-04-12 13:09:14 +00:00
extension View {
func themeGlobal() -> some View {
themeNavigationViewStyle()
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
#if !targetEnvironment(macCatalyst)
.themeLockScreen()
#endif
.themeTint()
.listStyle(themeListStyleValue())
.toggleStyle(themeToggleStyleValue())
.menuStyle(.borderlessButton)
2023-12-16 19:58:54 +00:00
#endif
2023-07-06 17:29:10 +00:00
.withErrorHandler()
2022-04-12 13:09:14 +00:00
}
2023-12-16 19:58:54 +00:00
#if os(tvOS)
func themeTV() -> some View {
GeometryReader { geo in
self
.padding(.horizontal, 0.25 * geo.size.width)
.scrollClipDisabled()
}
}
#endif
func themePrimaryView() -> some View {
#if targetEnvironment(macCatalyst)
navigationBarTitleDisplayMode(.inline)
.themeSidebarListStyle()
2023-12-16 19:58:54 +00:00
#elseif !os(tvOS)
navigationBarTitleDisplayMode(.large)
.navigationTitle(Unlocalized.appName)
.themeSidebarListStyle()
2023-12-16 19:58:54 +00:00
#else
self
#endif
}
func themeSecondaryView() -> some View {
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
navigationBarTitleDisplayMode(.inline)
2023-12-16 19:58:54 +00:00
#else
self
#endif
}
2022-04-12 13:09:14 +00:00
@ViewBuilder
private func themeNavigationViewStyle() -> some View {
switch themeIdiom {
2022-04-12 13:09:14 +00:00
case .phone:
navigationViewStyle(.stack)
default:
navigationViewStyle(.automatic)
}
}
@ViewBuilder
private func themeSidebarListStyle() -> some View {
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
switch themeIdiom {
case .phone:
listStyle(.insetGrouped)
default:
listStyle(.sidebar)
}
2023-12-16 19:58:54 +00:00
#else
self
#endif
2022-04-12 13:09:14 +00:00
}
func themeTint() -> some View {
tint(.accentColor)
}
2024-02-03 10:52:29 +00:00
func themeListSelectionColor(isSelected: Bool) -> some View {
let background = isSelected ? Color.gray.opacity(0.6) : .clear
return listRowBackground(background.themeRounded())
}
private func themeListStyleValue() -> some ListStyle {
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
.insetGrouped
2023-12-16 19:58:54 +00:00
#else
PlainListStyle()
#endif
}
private func themeToggleStyleValue() -> some ToggleStyle {
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
.switch
2023-12-16 19:58:54 +00:00
#else
DefaultToggleStyle()
#endif
2022-04-12 13:09:14 +00:00
}
}
// MARK: Colors
extension View {
fileprivate var themePrimaryBackgroundColor: Color {
2024-02-03 10:52:29 +00:00
Color(.primary)
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
fileprivate var themeSecondaryColor: Color {
2022-04-12 13:09:14 +00:00
.secondary
}
2023-03-17 20:55:47 +00:00
fileprivate var themeLightTextColor: Color {
2024-02-03 10:52:29 +00:00
Color(.lightText)
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
fileprivate var themeErrorColor: Color {
2022-04-12 13:09:14 +00:00
.red
}
private func themeColor(_ string: String?, validator: (String) throws -> Void) -> Color? {
guard let string = string else {
return nil
}
do {
try validator(string)
return nil
} catch {
return themeErrorColor
}
}
}
// MARK: Images
extension View {
var themeAssetsLogoImage: String {
2022-05-20 20:46:59 +00:00
"Logo"
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
2022-05-05 07:51:17 +00:00
var themeCheckmarkImage: String {
"checkmark"
}
2023-03-17 20:55:47 +00:00
2022-05-05 07:51:17 +00:00
var themeShareImage: String {
"square.and.arrow.up"
}
var themeCopyImage: String {
"doc.on.doc"
}
2023-03-17 20:55:47 +00:00
2022-05-05 07:51:17 +00:00
var themeCloseImage: String {
"xmark"
}
2023-03-17 20:55:47 +00:00
2022-05-05 07:51:17 +00:00
var themeConceilImage: String {
"eye.slash"
}
var themeRevealImage: String {
"eye"
}
2023-12-16 19:58:54 +00:00
var themeAppleTVImage: String {
"tv"
}
2022-05-05 07:51:17 +00:00
// MARK: Organizer
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
func themeAssetsProviderImage(_ providerName: ProviderName) -> String {
"providers/\(providerName)"
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
func themeAssetsCountryImage(_ countryCode: String) -> String {
"flags/\(countryCode.lowercased())"
}
var themeProviderImage: String {
2022-04-27 11:40:05 +00:00
"externaldrive.connected.to.line.below"
}
2023-03-17 20:55:47 +00:00
var themeHostFilesImage: String {
2022-04-27 11:40:05 +00:00
"folder"
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
var themeHostTextImage: String {
"text.justify"
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
var themeSettingsImage: String {
"gearshape"
}
2022-05-05 07:51:17 +00:00
var themeDonateImage: String {
"giftcard"
}
2022-05-05 07:51:17 +00:00
var themeRedditImage: String {
"person.3"
}
var themeWriteReviewImage: String {
"star"
2022-05-05 07:51:17 +00:00
}
2023-03-17 20:55:47 +00:00
var themeAddMenuImage: String {
2022-04-12 13:09:14 +00:00
"plus"
}
2023-03-17 20:55:47 +00:00
var themeProfileActiveImage: String {
2022-05-03 17:24:46 +00:00
"checkmark.circle"
}
var themeProfileConnectedImage: String {
"circle.fill"
}
var themeProfileInactiveImage: String {
2022-05-03 17:24:46 +00:00
"circle"
}
2022-05-05 07:51:17 +00:00
// MARK: Profile
var themeSettingsMenuImage: String {
"ellipsis.circle"
2022-04-12 13:09:14 +00:00
}
var themeReconnectImage: String {
"arrow.clockwise"
}
2022-04-12 13:09:14 +00:00
var themeShortcutsImage: String {
2022-04-27 11:40:05 +00:00
"mic"
2022-04-12 13:09:14 +00:00
}
2022-05-05 07:51:17 +00:00
var themeRenameProfileImage: String {
"highlighter"
// "character.cursor.ibeam"
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
2022-05-05 07:51:17 +00:00
var themeDuplicateImage: String {
"doc.on.doc"
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
var themeUninstallImage: String {
"arrow.uturn.down"
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeDeleteImage: String {
2022-04-27 11:40:05 +00:00
"trash"
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeVPNProtocolImage: String {
2022-04-27 11:40:05 +00:00
"bolt"
2022-04-12 13:09:14 +00:00
// "waveform.path.ecg"
// "message.and.waveform.fill"
// "pc"
// "captions.bubble.fill"
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeEndpointImage: String {
"link"
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeAccountImage: String {
2022-04-27 11:40:05 +00:00
"person"
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeProviderLocationImage: String {
2022-04-27 11:40:05 +00:00
"location"
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeProviderPresetImage: String {
"slider.horizontal.3"
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeNetworkSettingsImage: String {
// "network"
"globe"
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeOnDemandImage: String {
"wifi"
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeDiagnosticsImage: String {
"bandage.fill"
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var themeFAQImage: String {
2022-04-27 11:40:05 +00:00
"questionmark.diamond"
2022-04-12 13:09:14 +00:00
}
func themeFavoritesImage(_ active: Bool) -> String {
active ? "bookmark.fill" : "bookmark"
}
func themeFavoriteActionImage(_ doFavorite: Bool) -> String {
doFavorite ? "bookmark" : "bookmark.slash.fill"
}
}
extension String {
var asAssetImage: Image {
Image(self)
}
var asSystemImage: Image {
Image(systemName: self)
}
}
// MARK: Styles
extension View {
func themeAccentForegroundStyle() -> some View {
foregroundColor(.accentColor)
}
var themePrimaryBackground: some View {
themePrimaryBackgroundColor
.ignoresSafeArea()
}
func themeSecondaryTextStyle() -> some View {
foregroundColor(themeSecondaryColor)
}
2023-03-17 20:55:47 +00:00
func themeLightTextStyle() -> some View {
foregroundColor(themeLightTextColor)
}
2023-03-17 20:55:47 +00:00
func themePrimaryTintStyle() -> some View {
tint(themePrimaryBackgroundColor)
}
func themeDestructiveTintStyle() -> some View {
tint(themeErrorColor)
}
2022-04-23 09:27:17 +00:00
func themeTextButtonStyle() -> some View {
accentColor(.primary)
}
func themeLongTextStyle() -> some View {
lineLimit(1)
.truncationMode(.middle)
}
2023-03-17 20:55:47 +00:00
func themeRawTextStyle() -> some View {
disableAutocorrection(true)
.autocapitalization(.none)
}
func themeInformativeTextStyle() -> some View {
multilineTextAlignment(.center)
.font(.title)
.foregroundColor(themeSecondaryColor)
}
func themeCellTitleStyle() -> some View {
font(.headline)
}
func themeCellSubtitleStyle() -> some View {
font(.subheadline)
}
func themeDebugLogStyle() -> some View {
font(.system(size: 13, weight: .medium, design: .monospaced))
}
2024-02-03 10:52:29 +00:00
func themeRounded() -> some View {
clipShape(.rect(cornerRadius: 10.0))
}
}
// MARK: Shortcuts
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
extension ShortcutType {
var themeImageName: String {
switch self {
case .enableVPN:
return "power"
case .disableVPN:
return "xmark"
case .reconnectVPN:
return "arrow.clockwise"
}
}
}
2023-12-16 19:58:54 +00:00
#endif
2022-04-23 10:08:24 +00:00
// MARK: Animations
2023-03-17 20:55:47 +00:00
2022-04-23 10:08:24 +00:00
extension View {
func themeAnimation<V: Equatable>(on value: V) -> some View {
animation(.default, value: value)
}
}
extension Binding {
func themeAnimation() -> Binding<Value> {
animation(.default)
}
}
2022-04-12 13:09:14 +00:00
// MARK: Shortcuts
extension View {
func themeCloseItem(presentationMode: Binding<PresentationMode>) -> some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
themeCloseImage.asSystemImage
}
}
}
func themeCloseItem(isPresented: Binding<Bool>) -> some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
Button {
isPresented.wrappedValue = false
} label: {
themeCloseImage.asSystemImage
}
}
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
func themeSaveButtonLabel() -> some View {
Text(L10n.Global.Strings.save)
}
func themeSecureField(_ placeholder: String, text: Binding<String>, contentType: UITextContentType = .password) -> some View {
RevealingSecureField(placeholder, text: text) {
themeConceilImage.asSystemImage
.themeAccentForegroundStyle()
} revealImage: {
themeRevealImage.asSystemImage
.themeAccentForegroundStyle()
}.textContentType(contentType)
.themeRawTextStyle()
}
2022-04-12 13:09:14 +00:00
func themeTextPicker<T: Hashable>(_ title: String, selection: Binding<T>, values: [T], description: @escaping (T) -> String) -> some View {
StyledPicker(title: title, selection: selection, values: values) {
Text(description($0))
} selectionLabel: {
Text(description($0))
.foregroundColor(themeSecondaryColor)
} listStyle: {
themeListStyleValue()
2022-04-12 13:09:14 +00:00
}
}
func themeLongContentLinkDefault(_ title: String, content: Binding<String>) -> some View {
LongContentLink(title, content: content) {
Text($0)
.foregroundColor(themeSecondaryColor)
}
}
2022-04-12 13:09:14 +00:00
func themeLongContentLink(_ title: String, content: Binding<String>, withPreview preview: String? = nil) -> some View {
LongContentLink(title, content: content, preview: preview) {
Text(preview != nil ? $0 : "")
.foregroundColor(themeSecondaryColor)
}
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
@ViewBuilder
func themeErrorMessage(_ message: String?) -> some View {
if let message = message {
if message.last != "." {
Text("\(message).")
.foregroundColor(themeErrorColor)
} else {
Text(message)
.foregroundColor(themeErrorColor)
}
} else {
EmptyView()
}
}
}
// MARK: Lock screen
2023-12-16 19:58:54 +00:00
#if !os(tvOS)
extension View {
func themeLockScreen() -> some View {
@AppStorage(AppPreference.locksInBackground.key) var locksInBackground = false
return LockableView(
locksInBackground: $locksInBackground,
content: {
self
},
lockedContent: LogoView.init,
unlockBlock: Self.themeUnlockScreenBlock
)
}
private static func themeUnlockScreenBlock() 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,
localizedReason: L10n.Global.Messages.unlockApp
)
return isAuthorized
} catch {
return false
}
}
}
2023-12-16 19:58:54 +00:00
#endif
2022-04-12 13:09:14 +00:00
// MARK: Validation
extension View {
func themeValidProfileName() -> some View {
themeRawTextStyle()
2022-04-25 13:20:19 +00:00
}
func themeValidURL(_ urlString: String?) -> some View {
2022-04-12 13:09:14 +00:00
themeValidating(urlString, validator: Validators.url)
.keyboardType(.asciiCapable)
.themeRawTextStyle()
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
func themeValidIPAddress(_ ipAddress: String?) -> some View {
2022-04-12 13:09:14 +00:00
themeValidating(ipAddress, validator: Validators.ipAddress)
.keyboardType(.numbersAndPunctuation)
.themeRawTextStyle()
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
func themeValidSocketPort(_ port: String?) -> some View {
themeValidating(port, validator: Validators.socketPort)
.keyboardType(.numberPad)
2022-04-12 13:09:14 +00:00
}
func themeValidDomainName(_ domainName: String?) -> some View {
2022-04-12 13:09:14 +00:00
themeValidating(domainName, validator: Validators.domainName)
.keyboardType(.asciiCapable)
.themeRawTextStyle()
2022-04-12 13:09:14 +00:00
}
func themeValidWildcardDomainName(_ domainName: String?) -> some View {
themeValidating(domainName, validator: Validators.wildcardDomainName)
.keyboardType(.asciiCapable)
.themeRawTextStyle()
}
func themeValidDNSOverTLSServerName(_ string: String?) -> some View {
themeValidating(string, validator: Validators.dnsOverTLSServerName)
.keyboardType(.asciiCapable)
.themeRawTextStyle()
}
func themeValidSSID(_ text: String?) -> some View {
2022-04-12 13:09:14 +00:00
themeValidating(text, validator: Validators.notEmpty)
.keyboardType(.asciiCapable)
.themeRawTextStyle()
2022-04-12 13:09:14 +00:00
}
private func themeValidating(_ string: String?, validator: (String) throws -> Void) -> some View {
foregroundColor(themeColor(string, validator: validator))
}
}
// MARK: Hacks
extension View {
2023-09-08 20:18:41 +00:00
@available(*, deprecated, message: "Mitigates multiline text truncation (1.0 does not work though)")
2022-04-12 13:09:14 +00:00
func xxxThemeTruncation() -> some View {
minimumScaleFactor(0.5)
}
}