mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-10 08:18:08 -04:00
129 lines
5.3 KiB
Swift
129 lines
5.3 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
|
|
/// A reusable branded loading/placeholder view with the AliasVault logo.
|
|
/// The logo is positioned in the upper portion of the screen to avoid overlap with Face ID prompts.
|
|
/// Supports both light and dark mode automatically.
|
|
public struct BrandedLoadingView: View {
|
|
/// Optional loading message to display below the logo
|
|
let message: String?
|
|
|
|
/// Whether to show the animated loading dots
|
|
let showLoadingAnimation: Bool
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var animatingDots: [Bool] = [false, false, false, false]
|
|
@State private var textDots = ""
|
|
@State private var timer: Timer?
|
|
|
|
/// Creates a branded loading view with optional message and loading animation
|
|
/// - Parameters:
|
|
/// - message: Optional message to display below the logo
|
|
/// - showLoadingAnimation: Whether to show the animated loading dots (default: true)
|
|
public init(message: String? = nil, showLoadingAnimation: Bool = true) {
|
|
self.message = message
|
|
self.showLoadingAnimation = showLoadingAnimation
|
|
}
|
|
|
|
/// Determine the effective color scheme, falling back to UIKit if SwiftUI environment is unavailable
|
|
private var effectiveColorScheme: ColorScheme {
|
|
// Use UIKit's trait collection as fallback since extension contexts may not
|
|
// properly propagate the SwiftUI colorScheme environment
|
|
let uiStyle = UITraitCollection.current.userInterfaceStyle
|
|
return uiStyle == .dark ? .dark : .light
|
|
}
|
|
|
|
private var colors: ColorConstants.Colors.Type {
|
|
ColorConstants.colors(for: effectiveColorScheme)
|
|
}
|
|
|
|
public var body: some View {
|
|
GeometryReader { geometry in
|
|
ZStack {
|
|
colors.background
|
|
.ignoresSafeArea()
|
|
|
|
VStack(spacing: 0) {
|
|
// Position content at 15% from top to avoid Face ID prompt obstruction
|
|
Spacer()
|
|
.frame(height: geometry.size.height * 0.15)
|
|
|
|
VStack(spacing: 16) {
|
|
// AliasVault logo
|
|
Image("Logo", bundle: .vaultUI)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 100, height: 100)
|
|
|
|
// Loading animation - four pulsing dots
|
|
if showLoadingAnimation {
|
|
HStack(spacing: 10) {
|
|
ForEach(0..<4) { index in
|
|
Circle()
|
|
.fill(colors.tertiary)
|
|
.frame(width: 8, height: 8)
|
|
.opacity(animatingDots[index] ? 1.0 : 0.3)
|
|
.animation(
|
|
Animation.easeInOut(duration: 0.7)
|
|
.repeatForever(autoreverses: true)
|
|
.delay(Double(index) * 0.2),
|
|
value: animatingDots[index]
|
|
)
|
|
}
|
|
}
|
|
.padding(12)
|
|
.padding(.horizontal, 12)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.fill(effectiveColorScheme == .dark ? Color.clear : Color.white)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.stroke(colors.tertiary, lineWidth: 5)
|
|
)
|
|
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
|
|
)
|
|
}
|
|
|
|
// Loading message with animated dots
|
|
if let message = message, !message.isEmpty {
|
|
Text(message + textDots)
|
|
.font(.body)
|
|
.foregroundColor(colors.text)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
.padding(.top, showLoadingAnimation ? 0 : 8)
|
|
}
|
|
}
|
|
.padding(20)
|
|
.frame(maxWidth: .infinity)
|
|
|
|
Spacer()
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
.onAppear {
|
|
if showLoadingAnimation {
|
|
// Start dot animations
|
|
for index in 0..<4 {
|
|
animatingDots[index] = true
|
|
}
|
|
|
|
// Start text dots animation
|
|
let textTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { _ in
|
|
if textDots.count >= 3 {
|
|
textDots = ""
|
|
} else {
|
|
textDots += "."
|
|
}
|
|
}
|
|
timer = textTimer
|
|
}
|
|
}
|
|
.onDisappear {
|
|
timer?.invalidate()
|
|
timer = nil
|
|
}
|
|
}
|
|
}
|