Add more view modifiers (#838)
1. ThemeProgressViewModifier to replace content with a progress view while a condition is active 2. ThemeEmptyContentModifier to replace content with a message if an empty condition is met 3. Replace .opacity(bool ? 1.0 : 0.0) with .opaque(bool) Reuse: - 1 in PaywallView and DonateView - 2 in ProfileContainerView
This commit is contained in:
parent
e07833b2a4
commit
3a5e3889d3
|
@ -51,27 +51,19 @@ struct DonateView: View {
|
|||
private var errorHandler: ErrorHandler = .default()
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if isFetchingProducts {
|
||||
ProgressView()
|
||||
.id(UUID())
|
||||
} else if !availableProducts.isEmpty {
|
||||
donationsView
|
||||
.disabled(purchasingIdentifier != nil)
|
||||
donationsView
|
||||
.themeProgress(if: isFetchingProducts)
|
||||
.navigationTitle(title)
|
||||
.alert(
|
||||
title,
|
||||
isPresented: $isThankYouPresented,
|
||||
actions: thankYouActions,
|
||||
message: thankYouMessage
|
||||
)
|
||||
.task {
|
||||
await fetchAvailableProducts()
|
||||
}
|
||||
}
|
||||
.themeForm()
|
||||
.navigationTitle(title)
|
||||
.alert(
|
||||
title,
|
||||
isPresented: $isThankYouPresented,
|
||||
actions: thankYouActions,
|
||||
message: thankYouMessage
|
||||
)
|
||||
.task {
|
||||
await fetchAvailableProducts()
|
||||
}
|
||||
.withErrorHandler(errorHandler)
|
||||
.withErrorHandler(errorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,24 +72,27 @@ private extension DonateView {
|
|||
Strings.Views.Donate.title
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var donationsView: some View {
|
||||
Form {
|
||||
#if os(macOS)
|
||||
Section {
|
||||
Text(Strings.Views.Donate.Sections.Main.footer)
|
||||
}
|
||||
Section {
|
||||
Text(Strings.Views.Donate.Sections.Main.footer)
|
||||
}
|
||||
#endif
|
||||
ForEach(availableProducts, id: \.productIdentifier) {
|
||||
PaywallProductView(
|
||||
iapManager: iapManager,
|
||||
style: .donation,
|
||||
product: $0,
|
||||
purchasingIdentifier: $purchasingIdentifier,
|
||||
onComplete: onComplete,
|
||||
onError: onError
|
||||
)
|
||||
ForEach(availableProducts, id: \.productIdentifier) {
|
||||
PaywallProductView(
|
||||
iapManager: iapManager,
|
||||
style: .donation,
|
||||
product: $0,
|
||||
purchasingIdentifier: $purchasingIdentifier,
|
||||
onComplete: onComplete,
|
||||
onError: onError
|
||||
)
|
||||
}
|
||||
.themeSection(footer: Strings.Views.Donate.Sections.Main.footer)
|
||||
}
|
||||
.themeSection(footer: Strings.Views.Donate.Sections.Main.footer)
|
||||
.themeForm()
|
||||
.disabled(purchasingIdentifier != nil)
|
||||
}
|
||||
|
||||
func thankYouActions() -> some View {
|
||||
|
|
|
@ -62,8 +62,8 @@ struct InstalledProfileView: View, Routable {
|
|||
}
|
||||
|
||||
private extension InstalledProfileView {
|
||||
var installedOpacity: CGFloat {
|
||||
profile != nil ? 1.0 : 0.0
|
||||
var isOpaque: Bool {
|
||||
profile != nil
|
||||
}
|
||||
|
||||
var cardView: some View {
|
||||
|
@ -102,7 +102,7 @@ private extension InstalledProfileView {
|
|||
StatusText(
|
||||
theme: theme,
|
||||
tunnel: tunnel,
|
||||
opacity: installedOpacity
|
||||
isOpaque: isOpaque
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ private extension InstalledProfileView {
|
|||
nextProfileId: $nextProfileId,
|
||||
interactiveManager: interactiveManager,
|
||||
errorHandler: errorHandler,
|
||||
opacity: installedOpacity,
|
||||
isOpaque: isOpaque,
|
||||
flow: flow
|
||||
)
|
||||
}
|
||||
|
@ -160,13 +160,13 @@ private struct StatusText: View {
|
|||
@ObservedObject
|
||||
var tunnel: ExtendedTunnel
|
||||
|
||||
let opacity: Double
|
||||
let isOpaque: Bool
|
||||
|
||||
var body: some View {
|
||||
ConnectionStatusText(tunnel: tunnel)
|
||||
.font(.body)
|
||||
.foregroundStyle(tunnel.statusColor(theme))
|
||||
.opacity(opacity)
|
||||
.opaque(isOpaque)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,7 +189,7 @@ private struct ToggleButton: View {
|
|||
@ObservedObject
|
||||
var errorHandler: ErrorHandler
|
||||
|
||||
let opacity: Double
|
||||
let isOpaque: Bool
|
||||
|
||||
let flow: ProfileFlow?
|
||||
|
||||
|
@ -209,7 +209,7 @@ private struct ToggleButton: View {
|
|||
// TODO: #584, necessary to avoid cell selection
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(tunnel.statusColor(theme))
|
||||
.opacity(opacity)
|
||||
.opaque(isOpaque)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -113,20 +113,13 @@ private struct ContainerModifier: ViewModifier {
|
|||
|
||||
func body(content: Content) -> some View {
|
||||
debugChanges()
|
||||
return ZStack {
|
||||
content
|
||||
.opacity(profileManager.hasProfiles ? 1.0 : 0.0)
|
||||
|
||||
if !profileManager.hasProfiles {
|
||||
Text(Strings.Views.Profiles.Folders.noProfiles)
|
||||
.themeEmptyMessage()
|
||||
return content
|
||||
.themeEmptyContent(if: !profileManager.hasProfiles, message: Strings.Views.Profiles.Folders.noProfiles)
|
||||
.searchable(text: $search)
|
||||
.onChange(of: search) {
|
||||
profileManager.search(byName: $0)
|
||||
}
|
||||
}
|
||||
.searchable(text: $search)
|
||||
.onChange(of: search) {
|
||||
profileManager.search(byName: $0)
|
||||
}
|
||||
.themeAnimation(on: profileManager.headers, category: .profiles)
|
||||
.themeAnimation(on: profileManager.headers, category: .profiles)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ struct ProfileRowView: View, Routable {
|
|||
private extension ProfileRowView {
|
||||
var markerView: some View {
|
||||
ThemeImage(header.id == nextProfileId ? .pending : statusImage)
|
||||
.opacity(header.id == nextProfileId || header.id == tunnel.currentProfile?.id ? 1.0 : 0.0)
|
||||
.opaque(header.id == nextProfileId || header.id == tunnel.currentProfile?.id)
|
||||
.frame(width: 24.0)
|
||||
}
|
||||
|
||||
|
|
|
@ -178,7 +178,7 @@ private extension VPNProviderServerView.ServersSubview {
|
|||
} label: {
|
||||
HStack {
|
||||
ThemeImage(.marked)
|
||||
.opacity(server.id == selectedServerId ? 1.0 : 0.0)
|
||||
.opaque(server.id == selectedServerId)
|
||||
VStack(alignment: .leading) {
|
||||
if let area = server.provider.area {
|
||||
Text(area)
|
||||
|
|
|
@ -68,7 +68,7 @@ extension VPNProviderServerView {
|
|||
return Table(servers) {
|
||||
TableColumn("") { server in
|
||||
ThemeImage(.marked)
|
||||
.opacity(server.id == selectedServerId ? 1.0 : 0.0)
|
||||
.opaque(server.id == selectedServerId)
|
||||
}
|
||||
.width(10.0)
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ struct ProfileView: View, TunnelInstallationProviding {
|
|||
ZStack {
|
||||
listView
|
||||
.padding(.horizontal)
|
||||
.opacity(interactiveManager.isPresented ? 0.0 : 1.0)
|
||||
.opaque(!interactiveManager.isPresented)
|
||||
|
||||
if interactiveManager.isPresented {
|
||||
interactiveView
|
||||
|
|
|
@ -38,6 +38,10 @@ extension View {
|
|||
self
|
||||
}
|
||||
}
|
||||
|
||||
public func opaque(_ condition: Bool) -> some View {
|
||||
opacity(condition ? 1.0 : 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewModifier {
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
//
|
||||
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
#if canImport(LocalAuthentication)
|
||||
import LocalAuthentication
|
||||
#endif
|
||||
import CommonUtils
|
||||
import SwiftUI
|
||||
|
||||
// MARK: Shortcuts
|
||||
|
@ -143,6 +143,14 @@ extension View {
|
|||
modifier(ThemeAnimationModifier(value: value, category: category))
|
||||
}
|
||||
|
||||
public func themeProgress(if isProgressing: Bool) -> some View {
|
||||
modifier(ThemeProgressViewModifier(isProgressing: isProgressing))
|
||||
}
|
||||
|
||||
public func themeEmptyContent(if isEmpty: Bool, message: String) -> some View {
|
||||
modifier(ThemeEmptyContentModifier(isEmpty: isEmpty, message: message))
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
public func themeWindow(width: CGFloat, height: CGFloat) -> some View {
|
||||
modifier(ThemeWindowModifier(size: .init(width: width, height: height)))
|
||||
|
@ -349,6 +357,39 @@ struct ThemeAnimationModifier<T>: ViewModifier where T: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
struct ThemeProgressViewModifier: ViewModifier {
|
||||
let isProgressing: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
ZStack {
|
||||
if isProgressing {
|
||||
ThemeProgressView()
|
||||
}
|
||||
content
|
||||
.opaque(!isProgressing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ThemeEmptyContentModifier: ViewModifier {
|
||||
let isEmpty: Bool
|
||||
|
||||
let message: String
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
ZStack {
|
||||
content
|
||||
.opaque(!isEmpty)
|
||||
|
||||
if isEmpty {
|
||||
Text(message)
|
||||
.themeEmptyMessage()
|
||||
.opaque(isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
|
||||
struct ThemeWindowModifier: ViewModifier {
|
||||
|
|
|
@ -225,6 +225,17 @@ public struct ThemeSecureField: View {
|
|||
}
|
||||
}
|
||||
|
||||
public struct ThemeProgressView: View {
|
||||
public init() {
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.id(UUID())
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
|
||||
public struct ThemeMenuImage: View {
|
||||
|
|
|
@ -59,31 +59,19 @@ struct PaywallView: View {
|
|||
private var errorHandler: ErrorHandler = .default()
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if isFetchingProducts {
|
||||
ProgressView()
|
||||
.id(UUID())
|
||||
} else if !recurringProducts.isEmpty {
|
||||
Group {
|
||||
productsView
|
||||
subscriptionFeaturesView
|
||||
restoreView
|
||||
}
|
||||
.disabled(purchasingIdentifier != nil)
|
||||
paywallView
|
||||
.themeProgress(if: isFetchingProducts)
|
||||
.toolbar(content: toolbarContent)
|
||||
.alert(
|
||||
Strings.Global.purchase,
|
||||
isPresented: $isPurchasePendingConfirmation,
|
||||
actions: pendingActions,
|
||||
message: pendingMessage
|
||||
)
|
||||
.task(id: feature) {
|
||||
await fetchAvailableProducts()
|
||||
}
|
||||
}
|
||||
.themeForm()
|
||||
.toolbar(content: toolbarContent)
|
||||
.alert(
|
||||
Strings.Global.purchase,
|
||||
isPresented: $isPurchasePendingConfirmation,
|
||||
actions: pendingActions,
|
||||
message: pendingMessage
|
||||
)
|
||||
.task(id: feature) {
|
||||
await fetchAvailableProducts()
|
||||
}
|
||||
.withErrorHandler(errorHandler)
|
||||
.withErrorHandler(errorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,6 +80,16 @@ private extension PaywallView {
|
|||
Strings.Global.purchase
|
||||
}
|
||||
|
||||
var paywallView: some View {
|
||||
Form {
|
||||
productsView
|
||||
subscriptionFeaturesView
|
||||
restoreView
|
||||
}
|
||||
.themeForm()
|
||||
.disabled(purchasingIdentifier != nil)
|
||||
}
|
||||
|
||||
var subscriptionFeatures: [AppFeature] {
|
||||
AppFeature.allCases.sorted {
|
||||
$0.localizedDescription.lowercased() < $1.localizedDescription.lowercased()
|
||||
|
|
|
@ -48,7 +48,7 @@ public struct FavoriteToggle<ID>: View where ID: Hashable {
|
|||
}
|
||||
} label: {
|
||||
ThemeImage(selection.contains(value) ? .favoriteOn : .favoriteOff)
|
||||
.opacity(opacity)
|
||||
.opaque(opaque)
|
||||
}
|
||||
#if os(macOS)
|
||||
.onHover {
|
||||
|
@ -59,11 +59,11 @@ public struct FavoriteToggle<ID>: View where ID: Hashable {
|
|||
}
|
||||
|
||||
private extension FavoriteToggle {
|
||||
var opacity: Double {
|
||||
var opaque: Bool {
|
||||
#if os(macOS)
|
||||
selection.contains(value) || value == hover ? 1.0 : 0.0
|
||||
selection.contains(value) || value == hover
|
||||
#else
|
||||
1.0
|
||||
true
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue