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:
Davide 2024-11-10 12:00:07 +01:00 committed by GitHub
parent e07833b2a4
commit 3a5e3889d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 130 additions and 88 deletions

View File

@ -51,16 +51,8 @@ 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)
}
}
.themeForm()
.themeProgress(if: isFetchingProducts)
.navigationTitle(title)
.alert(
title,
@ -80,8 +72,8 @@ private extension DonateView {
Strings.Views.Donate.title
}
@ViewBuilder
var donationsView: some View {
Form {
#if os(macOS)
Section {
Text(Strings.Views.Donate.Sections.Main.footer)
@ -99,6 +91,9 @@ private extension DonateView {
}
.themeSection(footer: Strings.Views.Donate.Sections.Main.footer)
}
.themeForm()
.disabled(purchasingIdentifier != nil)
}
func thankYouActions() -> some View {
Button(Strings.Global.ok) {

View File

@ -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)
}
}

View File

@ -113,15 +113,8 @@ 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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -38,6 +38,10 @@ extension View {
self
}
}
public func opaque(_ condition: Bool) -> some View {
opacity(condition ? 1.0 : 0.0)
}
}
extension ViewModifier {

View File

@ -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 {

View File

@ -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 {

View File

@ -59,20 +59,8 @@ 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)
}
}
.themeForm()
paywallView
.themeProgress(if: isFetchingProducts)
.toolbar(content: toolbarContent)
.alert(
Strings.Global.purchase,
@ -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()

View File

@ -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
}
}