mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 23:33:36 -06:00
120 lines
4.6 KiB
Swift
120 lines
4.6 KiB
Swift
import SwiftUI
|
|
|
|
struct CipherDetailsView: View {
|
|
@ObservedObject var cipherDetailsViewModel: CipherDetailsViewModel
|
|
@Environment(\.scenePhase) var scenePhase
|
|
|
|
let iconSize: CGSize = .init(width: 30, height: 30)
|
|
|
|
init(cipher: CipherDTO) {
|
|
cipherDetailsViewModel = CipherDetailsViewModel(cipher: cipher)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading) {
|
|
HStack {
|
|
if cipherDetailsViewModel.iconImageUri == nil {
|
|
iconPlaceholderImage
|
|
} else {
|
|
if #available(watchOSApplicationExtension 8.0, *) {
|
|
AsyncImage(url: URL(string: cipherDetailsViewModel.iconImageUri!)) { phase in
|
|
switch phase {
|
|
case .empty:
|
|
iconPlaceholderImage
|
|
case let .success(image):
|
|
image.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(maxWidth: iconSize.width, maxHeight: iconSize.height)
|
|
case .failure:
|
|
iconPlaceholderImage
|
|
@unknown default:
|
|
EmptyView()
|
|
}
|
|
}
|
|
} else {
|
|
ImageView(
|
|
withURL: cipherDetailsViewModel.iconImageUri!,
|
|
maxWidth: iconSize.width,
|
|
maxHeight: iconSize.height,
|
|
) {
|
|
iconPlaceholderImage
|
|
}
|
|
}
|
|
}
|
|
Text(cipherDetailsViewModel.cipher.name!)
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
.padding(.leading, 5)
|
|
}
|
|
if cipherDetailsViewModel.cipher.login.username != nil {
|
|
Text(cipherDetailsViewModel.cipher.login.username!)
|
|
.font(.title3)
|
|
.fontWeight(.bold)
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
.privacySensitive()
|
|
}
|
|
if cipherDetailsViewModel.totpFormatted == "" {
|
|
ProgressView()
|
|
} else {
|
|
HStack {
|
|
let transition = AnyTransition.asymmetric(insertion: .slide, removal: .scale)
|
|
.combined(with: .opacity)
|
|
Text(cipherDetailsViewModel.totpFormatted)
|
|
.font(.largeTitle)
|
|
.scaledToFit()
|
|
.minimumScaleFactor(0.01)
|
|
.lineLimit(1)
|
|
.id(cipherDetailsViewModel.totpFormatted)
|
|
.privacySensitive()
|
|
.transition(transition)
|
|
.animation(.default.speed(0.7), value: cipherDetailsViewModel.totpFormatted)
|
|
Spacer()
|
|
ZStack {
|
|
CircularProgressView(
|
|
progress: cipherDetailsViewModel.progress,
|
|
strokeLineWidth: 3,
|
|
strokeColor: Color.blue,
|
|
endingStrokeColor: Color.red,
|
|
)
|
|
.frame(width: 40, height: 40)
|
|
Text("\(cipherDetailsViewModel.counter)")
|
|
.font(.title3)
|
|
.fontWeight(.semibold)
|
|
.privacySensitive()
|
|
}
|
|
}
|
|
.padding(.top, 20)
|
|
.padding(.leading, 5)
|
|
.padding(.trailing, 5)
|
|
}
|
|
}
|
|
.onAppear {
|
|
cipherDetailsViewModel.startGeneration()
|
|
}
|
|
.onDisappear {
|
|
cipherDetailsViewModel.stopGeneration()
|
|
}
|
|
.onChange(of: scenePhase) { newPhase in
|
|
if newPhase == .active {
|
|
try? cipherDetailsViewModel.regenerateTotp()
|
|
}
|
|
}
|
|
}
|
|
|
|
var iconPlaceholderImage: some View {
|
|
Image("DefaultCipherIcon")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(maxWidth: iconSize.width, maxHeight: iconSize.height)
|
|
}
|
|
}
|
|
|
|
struct CipherDetailsView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
CipherDetailsView(cipher: CipherMock.ciphers[0])
|
|
}
|
|
}
|