// // Theme.swift // Passepartout // // Created by Davide De Rosa on 2/24/22. // Copyright (c) 2022 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 PassepartoutCore extension Color { init(red: Double, green: Double, blue: Double, brightness: Double) { self.init( red: red * brightness, green: green * brightness, blue: blue * brightness ) } } extension View { @available(iOS 14, *) func themeConfigureNavigationBarAppearance() { let navBackgroundColor = Asset.Assets.primaryColor.color let titleAttributes: [NSAttributedString.Key: Any] = [ .foregroundColor: Asset.Assets.lightTextColor.color ] let navBarAppearance = UINavigationBarAppearance() navBarAppearance.configureWithOpaqueBackground() navBarAppearance.backgroundColor = navBackgroundColor navBarAppearance.titleTextAttributes = titleAttributes navBarAppearance.largeTitleTextAttributes = titleAttributes let navBar = UINavigationBar.appearance() navBar.standardAppearance = navBarAppearance navBar.compactAppearance = navBarAppearance navBar.scrollEdgeAppearance = navBarAppearance // UITableView.appearance().backgroundColor = .clear } @available(iOS 14, *) var themeIdiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } } // MARK: Styles extension View { func themeGlobal() -> some View { let color = themeAccentColor return accentColor(color) .toggleStyle(SwitchToggleStyle(tint: color)) .listStyle(.insetGrouped) .themeNavigationViewStyle() } @ViewBuilder private func themeNavigationViewStyle() -> some View { switch UIDevice.current.userInterfaceIdiom { case .phone: navigationViewStyle(.stack) default: navigationViewStyle(.automatic) } } @ViewBuilder func themePrimaryView() -> some View { switch UIDevice.current.userInterfaceIdiom { case .phone: navigationBarTitleDisplayMode(.large) default: themeSecondaryView() } } func themeSecondaryView() -> some View { navigationBarTitleDisplayMode(.inline) } func themeLongText() -> some View { lineLimit(1) .truncationMode(.middle) } } // MARK: Colors extension View { var themeAccentColor: Color { Color(Asset.Assets.accentColor.color) } var themePrimaryBackgroundColor: Color { Color(Asset.Assets.primaryColor.color) } var themePrimaryBackground: some View { themePrimaryBackgroundColor .ignoresSafeArea() } var themeSecondaryColor: Color { .secondary } var themeLightTextColor: Color { Color(Asset.Assets.lightTextColor.color) } var themeErrorColor: Color { .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: Fonts extension View { func themeDebugLogFont() -> some View { font(.system(size: 13, weight: .medium, design: .monospaced)) } } // MARK: Images extension View { var themeAssetsLogoImage: String { "logo" } func themeAssetsProviderImage(_ providerName: ProviderName) -> String { "providers/\(providerName)" } func themeAssetsCountryImage(_ countryCode: String) -> String { "flags/\(countryCode.lowercased())" } var themeHostImage: String { "folder.fill" } var themeProviderImage: String { "externaldrive.connected.to.line.below.fill" } var themeAddProfileImage: String { "plus" } var themeCheckmarkImage: String { "checkmark" } var themeStatusImage: String { "network" } var themeShortcutsImage: String { "mic.fill" } var themeDonateImage: String { "giftcard.fill" } var themeRedditImage: String { "person.3.fill" } var themeWriteReviewImage: String { "heart.fill" } var themeAboutImage: String { "info.circle" } var themeDeleteImage: String { "trash.fill" } var themeRenameProfileImage: String { "highlighter" // "character.cursor.ibeam" } var themeVPNProtocolImage: String { "bolt.fill" // "waveform.path.ecg" // "message.and.waveform.fill" // "pc" // "captions.bubble.fill" } var themeEndpointImage: String { "link" } var themeAccountImage: String { "person.fill" } var themeProviderLocationImage: String { "location.fill" } var themeProviderPresetImage: String { "slider.horizontal.3" } var themeProviderRefreshImage: String { "arrow.clockwise" } var themeNetworkSettingsImage: String { // "network" "globe" } var themeOnDemandImage: String { "wifi" } var themeDiagnosticsImage: String { "bandage.fill" } var themeFAQImage: String { "questionmark.diamond.fill" } var themeConceilImage: String { "eye.slash.fill" } var themeRevealImage: String { "eye.fill" } var themeShareImage: String { "square.and.arrow.up" } 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: Shortcuts extension View { func themeSaveButtonLabel() -> some View { // themeCheckmarkImage.asSystemImage Text(L10n.Global.Strings.save) } func themeDoneButtonLabel() -> some View { // themeCheckmarkImage.asSystemImage Text(L10n.Global.Strings.ok) } func themeTextPicker(_ title: String, selection: Binding, 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: { .insetGrouped } } func themeLongContentLinkDefault(_ title: String, content: Binding) -> some View { LongContentLink(title, content: content) { Text($0) .foregroundColor(themeSecondaryColor) } } func themeLongContentLink(_ title: String, content: Binding, withPreview preview: String? = nil) -> some View { LongContentLink(title, content: content, preview: preview) { Text(preview != nil ? $0 : "") .foregroundColor(themeSecondaryColor) } } @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: Validation extension View { func themeURL(_ urlString: String?) -> some View { themeValidating(urlString, validator: Validators.url) .keyboardType(.asciiCapable) .autocapitalization(.none) .disableAutocorrection(true) } func themeIPAddress(_ ipAddress: String?) -> some View { themeValidating(ipAddress, validator: Validators.ipAddress) .keyboardType(.numbersAndPunctuation) .autocapitalization(.none) .disableAutocorrection(true) } func themeSocketPort() -> some View { keyboardType(.numberPad) } func themeDomainName(_ domainName: String?) -> some View { themeValidating(domainName, validator: Validators.domainName) .keyboardType(.asciiCapable) .autocapitalization(.none) .disableAutocorrection(true) } func themeSSID(_ text: String?) -> some View { themeValidating(text, validator: Validators.notEmpty) .keyboardType(.asciiCapable) .autocapitalization(.none) .disableAutocorrection(true) } private func themeValidating(_ string: String?, validator: (String) throws -> Void) -> some View { foregroundColor(themeColor(string, validator: validator)) } } // MARK: Hacks extension View { // @available(*, deprecated, message: "mitigates multiline text truncation (1.0 does not work though)") func xxxThemeTruncation() -> some View { minimumScaleFactor(0.5) } }