mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
2.3.2 - Part 1
This commit is contained in:
parent
9fd2f51871
commit
3fb8a8e956
3 changed files with 310 additions and 94 deletions
|
|
@ -733,7 +733,7 @@
|
|||
"$(PROJECT_DIR)/StikJIT/idevice",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MARKETING_VERSION = 2.3.1;
|
||||
MARKETING_VERSION = 2.3.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
|
|
@ -786,7 +786,7 @@
|
|||
"$(PROJECT_DIR)/StikJIT/idevice",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MARKETING_VERSION = 2.3.1;
|
||||
MARKETING_VERSION = 2.3.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,14 @@ struct CustomTheme: Identifiable, Codable, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Distribution detection (receipt-based)
|
||||
|
||||
enum DistributorType: String {
|
||||
case appStore
|
||||
case testFlight
|
||||
case other
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ThemeExpansionManager: ObservableObject {
|
||||
static let productIdentifier = "SD_Theme_Expansion"
|
||||
|
|
@ -73,12 +81,17 @@ final class ThemeExpansionManager: ObservableObject {
|
|||
@Published var lastError: String?
|
||||
@Published private(set) var customThemes: [CustomTheme] = []
|
||||
|
||||
// New: distribution awareness
|
||||
@Published private(set) var distributor: DistributorType
|
||||
var isAppStoreBuild: Bool { distributor == .appStore }
|
||||
|
||||
private var updatesTask: Task<Void, Never>?
|
||||
private let isPreviewInstance: Bool
|
||||
private let customThemesKey = "ThemeExpansion.CustomThemes"
|
||||
|
||||
init(previewUnlocked: Bool = false) {
|
||||
self.isPreviewInstance = previewUnlocked
|
||||
self.distributor = ThemeExpansionManager.detectDistributor()
|
||||
self.hasThemeExpansion = previewUnlocked
|
||||
loadCustomThemes()
|
||||
|
||||
|
|
@ -93,23 +106,34 @@ final class ThemeExpansionManager: ObservableObject {
|
|||
|
||||
guard !previewUnlocked else { return }
|
||||
|
||||
// Listen for transaction updates and apply verified ones
|
||||
updatesTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
for await result in StoreKit.Transaction.updates {
|
||||
await self.handle(transactionResult: result)
|
||||
// Only wire StoreKit listeners if this is an App Store build
|
||||
if isAppStoreBuild {
|
||||
updatesTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
for await result in StoreKit.Transaction.updates {
|
||||
await self.handle(transactionResult: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task { await refreshEntitlements() }
|
||||
Task { await refreshEntitlements() }
|
||||
} else {
|
||||
// Non–App Store builds cannot purchase yet; keep everything locked and quiet
|
||||
self.hasThemeExpansion = false
|
||||
self.themeExpansionProduct = nil
|
||||
self.lastError = nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
updatesTask?.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
func refreshEntitlements() async {
|
||||
guard !isPreviewInstance else { return }
|
||||
guard isAppStoreBuild else { return } // No-op outside App Store
|
||||
|
||||
isProcessing = true
|
||||
defer { isProcessing = false }
|
||||
do {
|
||||
|
|
@ -138,6 +162,10 @@ final class ThemeExpansionManager: ObservableObject {
|
|||
|
||||
func restorePurchases() async {
|
||||
guard !isPreviewInstance else { return }
|
||||
guard isAppStoreBuild else {
|
||||
lastError = "Theme Expansion is coming soon on this store."
|
||||
return
|
||||
}
|
||||
isProcessing = true
|
||||
defer { isProcessing = false }
|
||||
do {
|
||||
|
|
@ -151,6 +179,10 @@ final class ThemeExpansionManager: ObservableObject {
|
|||
|
||||
func purchaseThemeExpansion() async {
|
||||
guard !isPreviewInstance else { return }
|
||||
guard isAppStoreBuild else {
|
||||
lastError = "Theme Expansion is coming soon on this store."
|
||||
return
|
||||
}
|
||||
|
||||
let product: Product
|
||||
if let cached = themeExpansionProduct {
|
||||
|
|
@ -277,6 +309,8 @@ final class ThemeExpansionManager: ObservableObject {
|
|||
saveCustomThemes()
|
||||
}
|
||||
|
||||
// MARK: - Persistence
|
||||
|
||||
private func loadCustomThemes() {
|
||||
guard let data = UserDefaults.standard.data(forKey: customThemesKey),
|
||||
let decoded = try? JSONDecoder().decode([CustomTheme].self, from: data) else {
|
||||
|
|
@ -306,6 +340,8 @@ final class ThemeExpansionManager: ObservableObject {
|
|||
return [Color.blue, Color.purple]
|
||||
}
|
||||
|
||||
// MARK: - StoreKit plumbing
|
||||
|
||||
private func handle(transactionResult: VerificationResult<StoreKit.Transaction>, finishTransaction: Bool = true) async {
|
||||
switch transactionResult {
|
||||
case .verified(let transaction):
|
||||
|
|
@ -329,6 +365,21 @@ final class ThemeExpansionManager: ObservableObject {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Distributor detection helper
|
||||
|
||||
private static func detectDistributor() -> DistributorType {
|
||||
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
|
||||
return .other
|
||||
}
|
||||
let path = receiptURL.path
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
// TestFlight builds use "sandboxReceipt"
|
||||
return receiptURL.lastPathComponent == "sandboxReceipt" ? .testFlight : .appStore
|
||||
} else {
|
||||
return .other
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
|
|
|||
|
|
@ -122,7 +122,21 @@ struct DisplayView: View {
|
|||
themeCard
|
||||
customThemesSection
|
||||
} else {
|
||||
themeExpansionUpsellCard
|
||||
// Accent preview remains above
|
||||
accentPreview
|
||||
|
||||
// Built-in + Custom themes previews with paywall centered on top
|
||||
ZStack(alignment: .center) {
|
||||
VStack(spacing: 20) {
|
||||
themePreview
|
||||
customThemesPreview
|
||||
}
|
||||
.zIndex(0)
|
||||
|
||||
themeExpansionUpsellCard
|
||||
.frame(maxWidth: .infinity) // match other cards’ width
|
||||
.zIndex(1)
|
||||
}
|
||||
}
|
||||
jitOptionsCard
|
||||
}
|
||||
|
|
@ -313,6 +327,7 @@ struct DisplayView: View {
|
|||
}
|
||||
|
||||
private var themeExpansionUpsellCard: some View {
|
||||
let isAppStore = themeExpansion?.isAppStoreBuild ?? true
|
||||
let productLoaded = themeExpansion?.themeExpansionProduct != nil
|
||||
return VStack(alignment: .leading, spacing: 14) {
|
||||
Text("StikDebug Theme Expansion")
|
||||
|
|
@ -320,92 +335,103 @@ struct DisplayView: View {
|
|||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("Unlock custom accent colors and dynamic backgrounds with the Theme Expansion.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let price = themeExpansion?.themeExpansionProduct?.displayPrice {
|
||||
Text("One-time purchase • \(price)")
|
||||
.font(.subheadline)
|
||||
if !isAppStore {
|
||||
Text("Theme Expansion is coming soon on this store.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
Text("For now, you can continue using the default theme.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Text("Unlock custom accent colors and dynamic backgrounds with the Theme Expansion.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if productLoaded, let manager = themeExpansion {
|
||||
Button {
|
||||
Task { await manager.purchaseThemeExpansion() }
|
||||
} label: {
|
||||
HStack {
|
||||
if manager.isProcessing {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
Text(manager.isProcessing ? "Purchasing…" : "Unlock Theme Expansion")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color.blue)
|
||||
)
|
||||
.foregroundColor(Color.blue.contrastText())
|
||||
}
|
||||
.disabled(manager.isProcessing)
|
||||
} else if let manager = themeExpansion {
|
||||
Button {
|
||||
Task { await manager.refreshEntitlements() }
|
||||
} label: {
|
||||
HStack {
|
||||
if manager.isProcessing {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
Text(manager.isProcessing ? "Contacting App Store…" : "Try Again")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.stroke(Color.blue.opacity(0.4), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.disabled(manager.isProcessing)
|
||||
}
|
||||
|
||||
if let manager = themeExpansion {
|
||||
Button {
|
||||
Task { await manager.restorePurchases() }
|
||||
} label: {
|
||||
Text("Restore Purchase")
|
||||
if let price = themeExpansion?.themeExpansionProduct?.displayPrice {
|
||||
Text("One-time purchase • \(price)")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if productLoaded, let manager = themeExpansion {
|
||||
Button {
|
||||
Task { await manager.purchaseThemeExpansion() }
|
||||
} label: {
|
||||
HStack {
|
||||
if manager.isProcessing {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
Text(manager.isProcessing ? "Purchasing…" : "Unlock Theme Expansion")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color.blue)
|
||||
)
|
||||
.foregroundColor(Color.blue.contrastText())
|
||||
}
|
||||
.disabled(manager.isProcessing)
|
||||
} else if let manager = themeExpansion {
|
||||
Button {
|
||||
Task { await manager.refreshEntitlements() }
|
||||
} label: {
|
||||
HStack {
|
||||
if manager.isProcessing {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
Text(manager.isProcessing ? "Contacting App Store…" : "Try Again")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.stroke(Color.blue.opacity(0.4), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.disabled(manager.isProcessing)
|
||||
}
|
||||
.disabled(manager.isProcessing)
|
||||
}
|
||||
|
||||
if let manager = themeExpansion, !productLoaded, manager.lastError == nil {
|
||||
Text(manager.isProcessing ? "Contacting the App Store…" : "Waiting for App Store information.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
if let manager = themeExpansion {
|
||||
Button {
|
||||
Task { await manager.restorePurchases() }
|
||||
} label: {
|
||||
Text("Restore Purchase")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.stroke(Color.blue.opacity(0.4), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.disabled(manager.isProcessing)
|
||||
}
|
||||
|
||||
if let error = themeExpansion?.lastError {
|
||||
Text(error)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.red)
|
||||
if let manager = themeExpansion, !productLoaded, manager.lastError == nil {
|
||||
Text(manager.isProcessing ? "Contacting the App Store…" : "Waiting for App Store information.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if let error = themeExpansion?.lastError {
|
||||
Text(error)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.frame(maxWidth: .infinity) // ensure background fills available width
|
||||
.background(
|
||||
// Slightly thicker material so the upsell stands out over the previews
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
.fill(.regularMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
|
||||
|
|
@ -414,6 +440,7 @@ struct DisplayView: View {
|
|||
.shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 4)
|
||||
.task {
|
||||
if let manager = themeExpansion,
|
||||
manager.isAppStoreBuild,
|
||||
!manager.isProcessing,
|
||||
manager.themeExpansionProduct == nil,
|
||||
manager.lastError == nil {
|
||||
|
|
@ -461,13 +488,15 @@ struct DisplayView: View {
|
|||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
|
||||
ForEach(AppTheme.allCases, id: \.self) { theme in
|
||||
ThemePreviewCard(style: theme.backgroundStyle,
|
||||
title: theme.displayName,
|
||||
selected: selectedBuiltInTheme == theme && selectedCustomTheme == nil) {
|
||||
guard hasThemeExpansion else { return }
|
||||
appThemeRaw = theme.rawValue
|
||||
applyThemePreferences()
|
||||
showSavedToast()
|
||||
}
|
||||
title: theme.displayName,
|
||||
selected: selectedBuiltInTheme == theme && selectedCustomTheme == nil,
|
||||
action: {
|
||||
guard hasThemeExpansion else { return }
|
||||
appThemeRaw = theme.rawValue
|
||||
applyThemePreferences()
|
||||
showSavedToast()
|
||||
},
|
||||
staticPreview: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -480,7 +509,37 @@ struct DisplayView: View {
|
|||
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 4)
|
||||
.shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 4)
|
||||
}
|
||||
|
||||
// Static version used in locked preview to avoid background animation cost
|
||||
private var themeCardStatic: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Theme")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
|
||||
ForEach(AppTheme.allCases, id: \.self) { theme in
|
||||
ThemePreviewCard(style: theme.backgroundStyle,
|
||||
title: theme.displayName,
|
||||
selected: selectedBuiltInTheme == theme && selectedCustomTheme == nil,
|
||||
action: {},
|
||||
staticPreview: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 4)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -525,11 +584,13 @@ struct DisplayView: View {
|
|||
let identifier = manager.customThemeIdentifier(for: theme)
|
||||
ThemePreviewCard(style: manager.backgroundStyle(for: identifier),
|
||||
title: theme.name,
|
||||
selected: selectedCustomTheme?.id == theme.id) {
|
||||
appThemeRaw = identifier
|
||||
applyThemePreferences()
|
||||
showSavedToast()
|
||||
}
|
||||
selected: selectedCustomTheme?.id == theme.id,
|
||||
action: {
|
||||
appThemeRaw = identifier
|
||||
applyThemePreferences()
|
||||
showSavedToast()
|
||||
},
|
||||
staticPreview: false)
|
||||
.contextMenu {
|
||||
Button("Edit") { editingCustomTheme = theme }
|
||||
Button("Delete", role: .destructive) {
|
||||
|
|
@ -584,6 +645,91 @@ struct DisplayView: View {
|
|||
withAnimation { justSaved = false }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Paywall previews (optimized look, non-interactive)
|
||||
|
||||
private func lockedPreview<Content: View>(_ content: Content) -> some View {
|
||||
content
|
||||
// Hardware-optimized blur via system material over the content
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
.opacity(0.4) // more transparent blur for a stronger preview
|
||||
)
|
||||
// Slight dim to improve contrast and preserve the “locked” feel
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(Color.black.opacity(0.03)) // lighter dim for more visibility
|
||||
)
|
||||
.overlay(alignment: .topLeading) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "lock.fill")
|
||||
Text("Preview")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.font(.caption2)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(.ultraThinMaterial, in: Capsule())
|
||||
.overlay(Capsule().stroke(Color.white.opacity(0.15), lineWidth: 1))
|
||||
.padding(12)
|
||||
}
|
||||
.compositingGroup() // flatten for better GPU compositing
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
// Use static theme grid inside the locked preview to avoid animation cost
|
||||
private var accentPreview: some View { lockedPreview(accentCard) }
|
||||
private var themePreview: some View { lockedPreview(themeCardStatic) }
|
||||
|
||||
private var customThemesPreview: some View {
|
||||
lockedPreview(customThemesPreviewCard)
|
||||
}
|
||||
|
||||
private var customThemesPreviewCard: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack {
|
||||
Text("Custom Themes")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Label("New", systemImage: "plus.circle.fill")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.opacity(0.6)
|
||||
}
|
||||
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
|
||||
ThemePreviewCard(
|
||||
style: .customGradient(colors: [Color(hex: "#3E4C7C") ?? .indigo,
|
||||
Color(hex: "#1C1F3A") ?? .blue]),
|
||||
title: "Midnight Fade",
|
||||
selected: false,
|
||||
action: {},
|
||||
staticPreview: true
|
||||
)
|
||||
ThemePreviewCard(
|
||||
style: .customGradient(colors: [Color(hex: "#00F5A0") ?? .green,
|
||||
Color(hex: "#00D9F5") ?? .cyan,
|
||||
Color(hex: "#C96BFF") ?? .purple]),
|
||||
title: "Neon Drift",
|
||||
selected: false,
|
||||
action: {},
|
||||
staticPreview: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 4)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Theme Preview Card
|
||||
|
|
@ -593,11 +739,29 @@ private struct ThemePreviewCard: View {
|
|||
let title: String
|
||||
let selected: Bool
|
||||
let action: () -> Void
|
||||
var staticPreview: Bool = false
|
||||
|
||||
private func staticized(_ style: BackgroundStyle) -> BackgroundStyle {
|
||||
switch style {
|
||||
case .staticGradient(let colors):
|
||||
return .staticGradient(colors: colors)
|
||||
case .animatedGradient(let colors, _):
|
||||
return .staticGradient(colors: colors)
|
||||
case .blobs(_, let background):
|
||||
// Use the background gradient for a static look
|
||||
return .staticGradient(colors: background)
|
||||
case .particles(_, let background):
|
||||
// Use the background gradient for a static look
|
||||
return .staticGradient(colors: background)
|
||||
case .customGradient(let colors):
|
||||
return .customGradient(colors: colors)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
ZStack {
|
||||
ThemedBackground(style: style)
|
||||
ThemedBackground(style: staticPreview ? staticized(style) : style)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
|
|
@ -786,3 +950,4 @@ private struct CustomThemeEditorView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue