Compare commits

..

No commits in common. "glass-motion-test" and "main" have entirely different histories.

10 changed files with 107 additions and 438 deletions

View File

@ -292,12 +292,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 16; CURRENT_PROJECT_VERSION = 10;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Kasztelan/Info.plist; INFOPLIST_FILE = Kasztelan/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink";
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0.9; MARKETING_VERSION = 1.0.8;
PRODUCT_BUNDLE_IDENTIFIER = uk.co.kasztelan.app.ios; PRODUCT_BUNDLE_IDENTIFIER = uk.co.kasztelan.app.ios;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -316,12 +316,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 16; CURRENT_PROJECT_VERSION = 10;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Kasztelan/Info.plist; INFOPLIST_FILE = Kasztelan/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink";
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0.9; MARKETING_VERSION = 1.0.8;
PRODUCT_BUNDLE_IDENTIFIER = uk.co.kasztelan.app.ios; PRODUCT_BUNDLE_IDENTIFIER = uk.co.kasztelan.app.ios;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;

View File

@ -20,19 +20,13 @@ struct AuthView: View {
} }
.padding(.bottom, 18) .padding(.bottom, 18)
if registering { if registering { registrationForm }
registrationForm else { loginForm }
.transition(.move(edge: .trailing).combined(with: .opacity))
} else {
loginForm
.transition(.move(edge: .leading).combined(with: .opacity))
}
} }
.padding(22) .padding(22)
.animation(.spring(response: 0.42, dampingFraction: 0.86), value: registering)
} }
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboard(.interactively)
.background(AmbientBackground()) .background(KTheme.background)
} }
private var loginForm: some View { private var loginForm: some View {
@ -61,7 +55,9 @@ struct AuthView: View {
} }
.accessibilityLabel(model.text("Otwórz resetowanie hasła", "Open password reset")) .accessibilityLabel(model.text("Otwórz resetowanie hasła", "Open password reset"))
Button(model.text("Załóż konto", "Create account")) { registering = true } Button(model.text("Załóż konto", "Create account")) { registering = true }
.buttonStyle(GlassButtonStyle()) .frame(maxWidth: .infinity, minHeight: 50)
.background(KTheme.panel)
.clipShape(RoundedRectangle(cornerRadius: 16))
} }
} }
@ -89,7 +85,9 @@ struct AuthView: View {
} }
.buttonStyle(PrimaryButtonStyle()) .buttonStyle(PrimaryButtonStyle())
Button(model.text("Mam już konto", "I already have an account")) { registering = false } Button(model.text("Mam już konto", "I already have an account")) { registering = false }
.buttonStyle(GlassButtonStyle()) .frame(maxWidth: .infinity, minHeight: 50)
.background(KTheme.panel)
.clipShape(RoundedRectangle(cornerRadius: 16))
} }
} }
@ -122,9 +120,7 @@ struct LanguageButton: View {
.font(.subheadline.bold()) .font(.subheadline.bold())
.foregroundStyle(KTheme.orange) .foregroundStyle(KTheme.orange)
.frame(width: 44, height: 44, alignment: .center) .frame(width: 44, height: 44, alignment: .center)
.background(.ultraThinMaterial, in: Circle()) .background(KTheme.panel, in: Circle())
.overlay { Circle().stroke(.white.opacity(0.20), lineWidth: 1) }
.shadow(color: KTheme.orange.opacity(0.18), radius: 12)
.contentShape(Circle()) .contentShape(Circle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)

View File

@ -23,7 +23,7 @@ struct CardView: View {
GeometryReader { geometry in GeometryReader { geometry in
let compact = geometry.size.height < 680 let compact = geometry.size.height < 680
VStack(alignment: .leading, spacing: compact ? 8 : 12) { VStack(alignment: .leading, spacing: compact ? 10 : 18) {
Text(model.text("Witaj, ", "Hello, ") + firstName) Text(model.text("Witaj, ", "Hello, ") + firstName)
.font(compact ? .title.bold() : .largeTitle.bold()) .font(compact ? .title.bold() : .largeTitle.bold())
.lineLimit(1) .lineLimit(1)
@ -36,10 +36,10 @@ struct CardView: View {
summary(compact: compact) summary(compact: compact)
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.top, compact ? 0 : 2) .padding(.vertical, compact ? 8 : 14)
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top)
} }
.background(.clear) .background(KTheme.background)
.fullScreenCover(isPresented: $cardScannerPresented) { .fullScreenCover(isPresented: $cardScannerPresented) {
CardScannerScreen( CardScannerScreen(
onScan: acceptScannedCard, onScan: acceptScannedCard,
@ -89,17 +89,11 @@ struct CardView: View {
} }
private func loyaltyCard(compact: Bool) -> some View { private func loyaltyCard(compact: Bool) -> some View {
GlassDepthDeck { return VStack(spacing: compact ? 10 : 20) {
loyaltyCardFace(compact: compact)
}
}
private func loyaltyCardFace(compact: Bool) -> some View {
VStack(spacing: compact ? 10 : 20) {
Image("Logo") Image("Logo")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(maxHeight: compact ? 66 : 88) .frame(maxHeight: compact ? 54 : 76)
if model.cardNumber.count == 13 { if model.cardNumber.count == 13 {
VStack(spacing: compact ? 4 : 8) { VStack(spacing: compact ? 4 : 8) {
CardBarcodeView(digits: model.cardNumber) CardBarcodeView(digits: model.cardNumber)
@ -126,7 +120,8 @@ struct CardView: View {
.tint(.white) .tint(.white)
.padding(.horizontal, 14) .padding(.horizontal, 14)
.frame(minHeight: 50) .frame(minHeight: 50)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .background(KTheme.panel.opacity(0.92))
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay { .overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous) RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.35), lineWidth: 1) .stroke(Color.white.opacity(0.35), lineWidth: 1)
@ -149,7 +144,8 @@ struct CardView: View {
Label(model.text("Skanuj kartę", "Scan card"), systemImage: "camera.viewfinder") Label(model.text("Skanuj kartę", "Scan card"), systemImage: "camera.viewfinder")
.frame(maxWidth: .infinity, minHeight: 22) .frame(maxWidth: .infinity, minHeight: 22)
} }
.buttonStyle(GlassButtonStyle()) .buttonStyle(.bordered)
.tint(.white)
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
Button { Button {
cardNumberFocused = false cardNumberFocused = false
@ -158,50 +154,17 @@ struct CardView: View {
Text(model.text("Nie mam karty — wygeneruj cyfrową", "I don't have a card — create a digital one")) Text(model.text("Nie mam karty — wygeneruj cyfrową", "I don't have a card — create a digital one"))
.frame(maxWidth: .infinity, minHeight: 22) .frame(maxWidth: .infinity, minHeight: 22)
} }
.buttonStyle(GlassButtonStyle()) .buttonStyle(.bordered)
.tint(.white)
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
} }
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(compact ? 14 : 22) .padding(compact ? 14 : 22)
.background { .background(LinearGradient(colors: [KTheme.orange, KTheme.pink, KTheme.purple], startPoint: .topLeading, endPoint: .bottomTrailing))
ZStack {
LinearGradient(
colors: [KTheme.orange.opacity(0.94), KTheme.pink.opacity(0.90), KTheme.purple.opacity(0.94)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
Circle()
.fill(.white.opacity(0.16))
.frame(width: 210, height: 210)
.blur(radius: 10)
.offset(x: 150, y: -130)
Circle()
.fill(KTheme.cyan.opacity(0.14))
.frame(width: 140, height: 140)
.blur(radius: 16)
.offset(x: -145, y: 150)
LinearGradient(
colors: [.white.opacity(0.22), .clear, .black.opacity(0.12)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
}
.clipShape(RoundedRectangle(cornerRadius: 26, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 26, style: .continuous))
.overlay { .shadow(color: KTheme.pink.opacity(0.2), radius: 18, y: 8)
RoundedRectangle(cornerRadius: 26, style: .continuous)
.stroke(
LinearGradient(
colors: [.white.opacity(0.72), .white.opacity(0.18), KTheme.pink.opacity(0.38)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1.5
)
}
.shadow(color: KTheme.pink.opacity(0.28), radius: 28, y: 16)
} }
private func acceptScannedCard(_ scan: ScannedCard) { private func acceptScannedCard(_ scan: ScannedCard) {

View File

@ -22,8 +22,6 @@
<string>$(KASZTELAN_APP_KEY)</string> <string>$(KASZTELAN_APP_KEY)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Aparat służy do skanowania kodów z kart rabatowych.</string> <string>Aparat służy do skanowania kodów z kart rabatowych.</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>

View File

@ -1,5 +1,4 @@
import SwiftUI import SwiftUI
import UIKit
import UserNotifications import UserNotifications
import BackgroundTasks import BackgroundTasks
import CoreSpotlight import CoreSpotlight
@ -120,21 +119,6 @@ struct KasztelanApp: App {
@StateObject private var model = AppModel() @StateObject private var model = AppModel()
@Environment(\.scenePhase) private var scenePhase @Environment(\.scenePhase) private var scenePhase
init() {
let navigationAppearance = UINavigationBarAppearance()
navigationAppearance.configureWithTransparentBackground()
navigationAppearance.backgroundColor = .clear
navigationAppearance.backgroundEffect = nil
navigationAppearance.shadowColor = .clear
let navigationBar = UINavigationBar.appearance()
navigationBar.standardAppearance = navigationAppearance
navigationBar.scrollEdgeAppearance = navigationAppearance
navigationBar.compactAppearance = navigationAppearance
navigationBar.isTranslucent = true
navigationBar.backgroundColor = .clear
}
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
RootView() RootView()
@ -160,7 +144,7 @@ struct RootView: View {
var body: some View { var body: some View {
ZStack { ZStack {
AmbientBackground() KTheme.background.ignoresSafeArea()
if model.isLoading { if model.isLoading {
ProgressView(model.text("Sprawdzam konto…", "Checking your account…")) ProgressView(model.text("Sprawdzam konto…", "Checking your account…"))
.tint(KTheme.orange) .tint(KTheme.orange)

View File

@ -4,44 +4,19 @@ struct MainTabView: View {
@EnvironmentObject private var model: AppModel @EnvironmentObject private var model: AppModel
var body: some View { var body: some View {
ZStack {
AmbientBackground()
TabView(selection: $model.selectedTab) { TabView(selection: $model.selectedTab) {
NavigationStack { NavigationStack { WalletView() }
ZStack {
AmbientBackground()
WalletView()
}
}
.toolbarBackground(.hidden, for: .navigationBar)
.tabItem { Label(model.text("Karta", "Card"), systemImage: "creditcard.fill") } .tabItem { Label(model.text("Karta", "Card"), systemImage: "creditcard.fill") }
.tag(0) .tag(0)
NavigationStack { NavigationStack { MessagesView() }
ZStack {
AmbientBackground()
MessagesView()
}
}
.toolbarBackground(.hidden, for: .navigationBar)
.tabItem { Label(model.text("Wiadomości", "Messages"), systemImage: "envelope.fill") } .tabItem { Label(model.text("Wiadomości", "Messages"), systemImage: "envelope.fill") }
.badge(model.messages.count) .badge(model.messages.count)
.tag(1) .tag(1)
NavigationStack { NavigationStack { ProfileView() }
ZStack {
AmbientBackground()
ProfileView()
}
}
.toolbarBackground(.hidden, for: .navigationBar)
.tabItem { Label(model.text("Profil", "Profile"), systemImage: "person.crop.circle.fill") } .tabItem { Label(model.text("Profil", "Profile"), systemImage: "person.crop.circle.fill") }
.tag(2) .tag(2)
} }
.background(.clear)
}
.tint(KTheme.orange) .tint(KTheme.orange)
.toolbarBackground(.ultraThinMaterial, for: .tabBar)
.toolbarBackground(.visible, for: .tabBar)
.task { await NotificationService.requestPermission() } .task { await NotificationService.requestPermission() }
} }
} }
@ -54,8 +29,7 @@ struct AppToolbar: ToolbarContent {
Image("Logo") Image("Logo")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 190, height: 46) .frame(width: 170, height: 44)
.shadow(color: KTheme.pink.opacity(0.28), radius: 12)
} }
ToolbarItem(placement: .topBarTrailing) { LanguageButton() } ToolbarItem(placement: .topBarTrailing) { LanguageButton() }
} }

View File

@ -27,9 +27,8 @@ struct MessagesView: View {
} }
.padding(20) .padding(20)
} }
.background(.clear) .background(KTheme.background)
.toolbar { AppToolbar() } .toolbar { AppToolbar() }
.toolbarBackground(.hidden, for: .navigationBar)
.refreshable { await model.refresh() } .refreshable { await model.refresh() }
.task { await model.refresh() } .task { await model.refresh() }
} }

View File

@ -34,7 +34,9 @@ struct ProfileView: View {
if model.cardNumber.isEmpty { cardAssignment } if model.cardNumber.isEmpty { cardAssignment }
Button(model.text("Zmień hasło", "Change password")) { changingPassword = true } Button(model.text("Zmień hasło", "Change password")) { changingPassword = true }
.buttonStyle(GlassButtonStyle()) .frame(maxWidth: .infinity, minHeight: 52)
.background(KTheme.panel)
.clipShape(RoundedRectangle(cornerRadius: 16))
Button(model.text("Wyloguj", "Sign out")) { Task { await model.logout() } } Button(model.text("Wyloguj", "Sign out")) { Task { await model.logout() } }
.buttonStyle(PrimaryButtonStyle()) .buttonStyle(PrimaryButtonStyle())
Button(model.text("Usuń konto", "Delete account"), role: .destructive) { deletingAccount = true } Button(model.text("Usuń konto", "Delete account"), role: .destructive) { deletingAccount = true }
@ -51,9 +53,8 @@ struct ProfileView: View {
} }
.padding(20) .padding(20)
} }
.background(.clear) .background(KTheme.background)
.toolbar { AppToolbar() } .toolbar { AppToolbar() }
.toolbarBackground(.hidden, for: .navigationBar)
.fullScreenCover(isPresented: $editingProfile) { EditProfileView() } .fullScreenCover(isPresented: $editingProfile) { EditProfileView() }
.sheet(isPresented: $changingPassword) { ChangePasswordView() } .sheet(isPresented: $changingPassword) { ChangePasswordView() }
.sheet(isPresented: $deletingAccount) { .sheet(isPresented: $deletingAccount) {
@ -68,7 +69,7 @@ struct ProfileView: View {
Spacer() Spacer()
} }
.padding(20) .padding(20)
.background(AmbientBackground()) .background(KTheme.background)
.navigationTitle(model.text("Usuń konto", "Delete account")) .navigationTitle(model.text("Usuń konto", "Delete account"))
.toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { deletingAccount = false } } } .toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { deletingAccount = false } } }
} }
@ -94,7 +95,9 @@ struct ProfileView: View {
Button(model.text("Nie mam karty — wygeneruj cyfrową", "I don't have a card — create a digital one")) { Button(model.text("Nie mam karty — wygeneruj cyfrową", "I don't have a card — create a digital one")) {
Task { _ = await model.generateCard() } Task { _ = await model.generateCard() }
} }
.buttonStyle(GlassButtonStyle()) .frame(maxWidth: .infinity, minHeight: 50)
.background(KTheme.panel)
.clipShape(RoundedRectangle(cornerRadius: 16))
} }
.kasztelanPanel() .kasztelanPanel()
} }
@ -135,7 +138,7 @@ struct EditProfileView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(20) .padding(20)
} }
.background(AmbientBackground()) .background(KTheme.background)
.navigationTitle(model.text("Edytuj profil", "Edit profile")) .navigationTitle(model.text("Edytuj profil", "Edit profile"))
.toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() } } } .toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() } } }
.onAppear { form = model.profileForm() } .onAppear { form = model.profileForm() }
@ -171,7 +174,7 @@ struct ChangePasswordView: View {
Spacer() Spacer()
} }
.padding(20) .padding(20)
.background(AmbientBackground()) .background(KTheme.background)
.navigationTitle(model.text("Zmień hasło", "Change password")) .navigationTitle(model.text("Zmień hasło", "Change password"))
.toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() } } } .toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() } } }
} }

View File

@ -1,168 +1,24 @@
import SwiftUI import SwiftUI
enum KTheme { enum KTheme {
static let background = Color(red: 5/255, green: 6/255, blue: 12/255) static let background = Color(red: 11/255, green: 11/255, blue: 14/255)
static let panel = Color(red: 28/255, green: 28/255, blue: 38/255) static let panel = Color(red: 28/255, green: 28/255, blue: 34/255)
static let orange = Color(red: 1, green: 106/255, blue: 0) static let orange = Color(red: 1, green: 106/255, blue: 0)
static let pink = Color(red: 242/255, green: 29/255, blue: 167/255) static let pink = Color(red: 242/255, green: 29/255, blue: 167/255)
static let purple = Color(red: 111/255, green: 60/255, blue: 1) static let purple = Color(red: 111/255, green: 60/255, blue: 1)
static let cyan = Color(red: 41/255, green: 209/255, blue: 1)
}
struct AmbientBackground: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var isMoving = false
var body: some View {
ZStack {
KTheme.background
RadialGradient(
colors: [KTheme.purple.opacity(0.32), .clear],
center: .topLeading,
startRadius: 10,
endRadius: 420
)
.scaleEffect(isMoving ? 1.18 : 0.92)
.offset(x: isMoving ? 36 : -28, y: isMoving ? 70 : -50)
Circle()
.fill(KTheme.pink.opacity(0.22))
.frame(width: 260, height: 260)
.blur(radius: 42)
.offset(x: isMoving ? 150 : 95, y: isMoving ? -250 : -190)
Circle()
.fill(KTheme.orange.opacity(0.20))
.frame(width: 220, height: 220)
.blur(radius: 48)
.offset(x: isMoving ? -150 : -90, y: isMoving ? 320 : 250)
Circle()
.fill(KTheme.cyan.opacity(0.12))
.frame(width: 180, height: 180)
.blur(radius: 40)
.offset(x: isMoving ? -120 : -165, y: isMoving ? -40 : 80)
LinearGradient(
colors: [.white.opacity(0.045), .clear, .black.opacity(0.25)],
startPoint: .top,
endPoint: .bottom
)
}
.ignoresSafeArea()
.onAppear {
guard !reduceMotion else { return }
withAnimation(.easeInOut(duration: 9).repeatForever(autoreverses: true)) {
isMoving = true
}
}
}
}
struct GlassDepthDeck<Content: View>: View {
private let cornerRadius: CGFloat
private let content: Content
init(cornerRadius: CGFloat = 26, @ViewBuilder content: () -> Content) {
self.cornerRadius = cornerRadius
self.content = content()
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(.black.opacity(0.50))
.scaleEffect(x: 0.94, y: 0.97)
.offset(y: 18)
.blur(radius: 18)
content
.shadow(color: .black.opacity(0.66), radius: 30, y: 20)
.shadow(color: KTheme.purple.opacity(0.30), radius: 24, x: -8, y: 8)
.shadow(color: KTheme.pink.opacity(0.30), radius: 20, x: 10, y: 4)
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(
LinearGradient(
colors: [.white.opacity(0.20), .clear, .clear, .black.opacity(0.12)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.allowsHitTesting(false)
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.stroke(
LinearGradient(
colors: [.white.opacity(0.92), .white.opacity(0.18), KTheme.purple.opacity(0.52)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 2.2
)
.allowsHitTesting(false)
RoundedRectangle(cornerRadius: cornerRadius - 3, style: .continuous)
.inset(by: 3)
.stroke(
LinearGradient(
colors: [.white.opacity(0.34), .clear, .black.opacity(0.28)],
startPoint: .top,
endPoint: .bottom
),
lineWidth: 1
)
.allowsHitTesting(false)
VStack {
HStack {
Capsule()
.fill(.white.opacity(0.58))
.frame(width: 112, height: 4)
.blur(radius: 0.6)
.padding(.top, 8)
.padding(.leading, 22)
Spacer()
}
Spacer()
}
.allowsHitTesting(false)
}
.compositingGroup()
.padding(.bottom, 20)
}
} }
struct PanelModifier: ViewModifier { struct PanelModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.padding(18) .padding(18)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 22, style: .continuous)) .background(KTheme.panel)
.overlay { .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
RoundedRectangle(cornerRadius: 22, style: .continuous)
.stroke(
LinearGradient(
colors: [.white.opacity(0.30), .white.opacity(0.06), KTheme.pink.opacity(0.16)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
}
.shadow(color: .black.opacity(0.22), radius: 24, y: 14)
} }
} }
extension View { extension View {
func kasztelanPanel() -> some View { modifier(PanelModifier()) } func kasztelanPanel() -> some View { modifier(PanelModifier()) }
func glassSurface(cornerRadius: CGFloat = 18) -> some View {
background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.stroke(.white.opacity(0.20), lineWidth: 1)
}
}
} }
struct PrimaryButtonStyle: ButtonStyle { struct PrimaryButtonStyle: ButtonStyle {
@ -171,37 +27,8 @@ struct PrimaryButtonStyle: ButtonStyle {
.font(.headline) .font(.headline)
.foregroundStyle(.white) .foregroundStyle(.white)
.frame(maxWidth: .infinity, minHeight: 54) .frame(maxWidth: .infinity, minHeight: 54)
.background( .background(KTheme.pink.opacity(configuration.isPressed ? 0.72 : 1))
LinearGradient(
colors: [KTheme.pink, KTheme.purple, KTheme.orange],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.opacity(configuration.isPressed ? 0.72 : 1)
)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(.white.opacity(configuration.isPressed ? 0.12 : 0.30), lineWidth: 1)
}
.shadow(color: KTheme.pink.opacity(configuration.isPressed ? 0.10 : 0.30), radius: 18, y: 9)
.scaleEffect(configuration.isPressed ? 0.975 : 1)
.animation(.spring(response: 0.24, dampingFraction: 0.72), value: configuration.isPressed)
}
}
struct GlassButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(.white)
.frame(maxWidth: .infinity, minHeight: 52)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(.white.opacity(configuration.isPressed ? 0.10 : 0.24), lineWidth: 1)
}
.scaleEffect(configuration.isPressed ? 0.975 : 1)
.animation(.spring(response: 0.24, dampingFraction: 0.72), value: configuration.isPressed)
} }
} }
@ -229,12 +56,8 @@ struct KasztelanField: View {
.autocorrectionDisabled(keyboard == .emailAddress) .autocorrectionDisabled(keyboard == .emailAddress)
.padding(.horizontal, 16) .padding(.horizontal, 16)
.frame(maxWidth: .infinity, minHeight: 54) .frame(maxWidth: .infinity, minHeight: 54)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) .background(KTheme.panel)
.overlay { .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(isFocused ? KTheme.orange.opacity(0.75) : .white.opacity(0.18), lineWidth: 1)
}
.shadow(color: isFocused ? KTheme.orange.opacity(0.14) : .clear, radius: 12)
.foregroundStyle(.white) .foregroundStyle(.white)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {

View File

@ -15,9 +15,8 @@ struct WalletView: View {
var body: some View { var body: some View {
walletCarousel walletCarousel
.background(.clear) .background(KTheme.background)
.toolbar { walletToolbar } .toolbar { walletToolbar }
.toolbarBackground(.hidden, for: .navigationBar)
.onAppear { selectedPage = 0 } .onAppear { selectedPage = 0 }
.task(id: model.user?.id) { await store.sync(using: model) } .task(id: model.user?.id) { await store.sync(using: model) }
.fullScreenCover(isPresented: $scannerPresented) { scannerView } .fullScreenCover(isPresented: $scannerPresented) { scannerView }
@ -32,26 +31,17 @@ struct WalletView: View {
private var walletCarousel: some View { private var walletCarousel: some View {
GeometryReader { geometry in GeometryReader { geometry in
ZStack {
WalletMotionBubbles(progress: carouselProgress(width: geometry.size.width))
AnyView(currentPage) AnyView(currentPage)
.id(pageIdentity) .id(pageIdentity)
.offset(x: dragOffset) .offset(x: dragOffset)
.offset(y: carouselArc(width: geometry.size.width)) .offset(y: carouselArc(width: geometry.size.width))
.scaleEffect(carouselScale(width: geometry.size.width)) .scaleEffect(carouselScale(width: geometry.size.width))
.rotation3DEffect( .rotation3DEffect(carouselAngle(width: geometry.size.width), axis: (x: 0, y: 1, z: 0), perspective: 0.9)
carouselAngle(width: geometry.size.width),
axis: (x: 0.08, y: 1, z: 0),
perspective: 0.62
)
.rotationEffect(.degrees(Double(carouselProgress(width: geometry.size.width)) * -2.4))
.opacity(carouselOpacity(width: geometry.size.width)) .opacity(carouselOpacity(width: geometry.size.width))
.shadow(color: KTheme.purple.opacity(carouselShadow), radius: 32, x: dragOffset < 0 ? 16 : -16, y: 12) .shadow(color: .black.opacity(carouselShadow), radius: 24, x: dragOffset < 0 ? 12 : -12)
.contentShape(Rectangle()) .contentShape(Rectangle())
.simultaneousGesture(carouselGesture(width: geometry.size.width)) .simultaneousGesture(carouselGesture(width: geometry.size.width))
} .overlay(alignment: .bottom) { pageIndicator }
.safeAreaInset(edge: .bottom, spacing: 0) { pageIndicator }
.clipped() .clipped()
} }
} }
@ -103,27 +93,23 @@ struct WalletView: View {
} }
private func carouselScale(width: CGFloat) -> CGFloat { private func carouselScale(width: CGFloat) -> CGFloat {
1 - min(abs(carouselProgress(width: width)), 1) * 0.10 1 - min(abs(dragOffset) / max(width, 1), 1) * 0.14
} }
private func carouselAngle(width: CGFloat) -> Angle { private func carouselAngle(width: CGFloat) -> Angle {
.degrees(Double(carouselProgress(width: width)) * 54) .degrees(Double(dragOffset / max(width, 1)) * 68)
} }
private func carouselArc(width: CGFloat) -> CGFloat { private func carouselArc(width: CGFloat) -> CGFloat {
let progress = min(abs(carouselProgress(width: width)), 1) let progress = min(abs(dragOffset) / max(width, 1), 1)
return progress * progress * 24 return progress * progress * 34
} }
private func carouselOpacity(width: CGFloat) -> Double { private func carouselOpacity(width: CGFloat) -> Double {
1 - min(Double(abs(carouselProgress(width: width))), 1) * 0.22 1 - min(Double(abs(dragOffset) / max(width, 1)), 1) * 0.28
} }
private func carouselProgress(width: CGFloat) -> CGFloat { private var carouselShadow: Double { min(Double(abs(dragOffset) / 520), 0.42) }
dragOffset / max(width, 1)
}
private var carouselShadow: Double { 0.12 + min(Double(abs(dragOffset) / 520), 0.34) }
private func carouselGesture(width: CGFloat) -> some Gesture { private func carouselGesture(width: CGFloat) -> some Gesture {
DragGesture(minimumDistance: 18) DragGesture(minimumDistance: 18)
@ -166,11 +152,7 @@ struct WalletView: View {
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.vertical, 8) .padding(.vertical, 8)
.background(.ultraThinMaterial, in: Capsule()) .background(.black.opacity(0.35), in: Capsule())
.overlay {
Capsule()
.stroke(.white.opacity(0.18), lineWidth: 1)
}
.padding(.bottom, 2) .padding(.bottom, 2)
.allowsHitTesting(false) .allowsHitTesting(false)
} }
@ -181,8 +163,8 @@ struct WalletView: View {
let predicted = value.predictedEndTranslation.width let predicted = value.predictedEndTranslation.width
guard horizontal, max(abs(value.translation.width), abs(predicted)) > max(52, width * 0.15) else { guard horizontal, max(abs(value.translation.width), abs(predicted)) > max(52, width * 0.15) else {
changingPage = true changingPage = true
withAnimation(.spring(response: 0.34, dampingFraction: 0.82)) { dragOffset = 0 } withAnimation(.easeOut(duration: 0.14)) { dragOffset = 0 }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { changingPage = false } DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { changingPage = false }
return return
} }
changingPage = true changingPage = true
@ -192,53 +174,15 @@ struct WalletView: View {
instant.disablesAnimations = true instant.disablesAnimations = true
withTransaction(instant) { withTransaction(instant) {
selectedPage = (selectedPage + direction + pageCount) % pageCount selectedPage = (selectedPage + direction + pageCount) % pageCount
dragOffset = direction > 0 ? width * 0.82 : -width * 0.82 dragOffset = direction > 0 ? width * 0.72 : -width * 0.72
} }
DispatchQueue.main.async { DispatchQueue.main.async {
withAnimation(.spring(response: 0.48, dampingFraction: 0.84, blendDuration: 0.08)) { dragOffset = 0 } withAnimation(.easeOut(duration: 0.26)) { dragOffset = 0 }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.50) { changingPage = false } DispatchQueue.main.asyncAfter(deadline: .now() + 0.27) { changingPage = false }
} }
} }
} }
private struct WalletMotionBubbles: View {
let progress: CGFloat
var body: some View {
ZStack {
bubble(size: 112, tint: KTheme.pink.opacity(0.20))
.offset(x: -145 + progress * 90, y: -230 - abs(progress) * 24)
bubble(size: 74, tint: KTheme.orange.opacity(0.22))
.offset(x: 145 + progress * 55, y: -80 + progress * 42)
bubble(size: 148, tint: KTheme.purple.opacity(0.17))
.offset(x: 125 - progress * 100, y: 255)
bubble(size: 48, tint: KTheme.cyan.opacity(0.18))
.offset(x: -120 - progress * 50, y: 150 + progress * 30)
}
.blur(radius: abs(progress) * 1.5)
.allowsHitTesting(false)
}
private func bubble(size: CGFloat, tint: Color) -> some View {
Circle()
.fill(.ultraThinMaterial)
.frame(width: size, height: size)
.overlay {
Circle()
.fill(
RadialGradient(
colors: [.white.opacity(0.28), tint, .clear],
center: .topLeading,
startRadius: 1,
endRadius: size
)
)
}
.overlay { Circle().stroke(.white.opacity(0.14), lineWidth: 1) }
.shadow(color: tint, radius: 24)
}
}
private struct AddDiscountCardView: View { private struct AddDiscountCardView: View {
@EnvironmentObject private var model: AppModel @EnvironmentObject private var model: AppModel
let scan: () -> Void let scan: () -> Void
@ -265,7 +209,7 @@ private struct AddDiscountCardView: View {
} }
.padding(.bottom, 34) .padding(.bottom, 34)
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.clear) .background(KTheme.background)
} }
} }
@ -289,7 +233,6 @@ private struct DiscountCardView: View {
.accessibilityLabel(model.text("Usuń kartę", "Delete card")) .accessibilityLabel(model.text("Usuń kartę", "Delete card"))
} }
GlassDepthDeck {
VStack(spacing: 18) { VStack(spacing: 18) {
StoredBarcodeView(value: card.payload, symbology: card.symbology) StoredBarcodeView(value: card.payload, symbology: card.symbology)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -306,26 +249,12 @@ private struct DiscountCardView: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(22) .padding(22)
.background( .background(LinearGradient(
ZStack { colors: [KTheme.purple, KTheme.pink, KTheme.orange],
LinearGradient(
colors: [KTheme.purple.opacity(0.92), KTheme.pink.opacity(0.88), KTheme.orange.opacity(0.86)],
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
) ))
Circle()
.fill(.white.opacity(0.15))
.frame(width: 170, height: 170)
.blur(radius: 8)
.offset(x: 145, y: -100)
}
)
.clipShape(RoundedRectangle(cornerRadius: 26, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 26, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 26, style: .continuous)
.stroke(.white.opacity(0.28), lineWidth: 1)
}
}
Text(model.text("Dostępna offline • zaszyfrowana synchronizacja", "Available offline • encrypted sync")) Text(model.text("Dostępna offline • zaszyfrowana synchronizacja", "Available offline • encrypted sync"))
.font(.footnote) .font(.footnote)
@ -335,7 +264,7 @@ private struct DiscountCardView: View {
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.vertical, 14) .padding(.vertical, 14)
.background(.clear) .background(KTheme.background)
.alert(model.text("Usunąć kartę?", "Delete this card?"), isPresented: $confirmingDelete) { .alert(model.text("Usunąć kartę?", "Delete this card?"), isPresented: $confirmingDelete) {
Button(model.text("Anuluj", "Cancel"), role: .cancel) {} Button(model.text("Anuluj", "Cancel"), role: .cancel) {}
Button(model.text("Usuń", "Delete"), role: .destructive, action: delete) Button(model.text("Usuń", "Delete"), role: .destructive, action: delete)
@ -365,7 +294,7 @@ private struct CardNameSheet: View {
Spacer() Spacer()
} }
.padding(20) .padding(20)
.background(AmbientBackground()) .background(KTheme.background)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button(model.text("Anuluj", "Cancel")) { dismiss() } Button(model.text("Anuluj", "Cancel")) { dismiss() }