PWD-1: add custom UI style #5

Merged
MarkusThielker merged 14 commits from 1-implement-custom-ui into development 2025-01-24 12:45:09 +00:00
9 changed files with 258 additions and 30 deletions

3
.gitignore vendored
View file

@ -14,3 +14,6 @@ DerivedData
# macOS # macOS
build/ build/
*.app *.app
# intelliJ
.idea

View file

@ -7,6 +7,6 @@ Gamification like success rates, typing time tracking and daily prompts to enter
## Security ## Security
An app handling secrets like these has to be implemented securly. That's why: An app handling secrets like these has to be implemented securly. That's why:
- Passwords are securly stored in your devices keychain - Passwords are securely stored in your devices keychain
- Authentication is required to enter the app - Authentication is required to enter the app
- No cloud sync (unless you are using your iCloud Keychain) - No cloud sync (unless you are using your iCloud Keychain)

View file

@ -413,7 +413,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.thielker.password; PRODUCT_BUNDLE_IDENTIFIER = dev.thielker.password;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -441,7 +441,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.thielker.password; PRODUCT_BUNDLE_IDENTIFIER = dev.thielker.password;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -51,13 +51,17 @@ struct passwordApp: App {
if isAuthenticated { if isAuthenticated {
ContextWrapper() ContextWrapper()
} else { } else {
Button("Authenticate") { PwdButton(
authenticate() label: Text("Authenticate"),
} variant: .primary,
action: authenticate
)
} }
} }
.onAppear { .onAppear {
authenticate() #if !DEBUG
authenticate()
#endif
} }
} }
.modelContainer(for: [Password.self, PasswordAttempt.self]) .modelContainer(for: [Password.self, PasswordAttempt.self])

View file

@ -0,0 +1,140 @@
//
// Button.swift
// password
//
// Created by Markus Thielker on 19.01.25.
//
import SwiftUI
struct PwdButton<Label: View>: View {
@Environment(\.colorScheme) private var colorScheme
var label: Label
var variant: ButtonVariant = .default
var size: ButtonSize = .medium
var action: () -> Void
var body: some View {
HStack{
label
}
.buttonStyle(PlainButtonStyle())
.padding(size.padding)
.background(variant.backgroundColor(colorScheme))
.foregroundColor(variant.foregroundColor(colorScheme))
.font(size.font)
.cornerRadius(size.cornerRadius)
.overlay(
RoundedRectangle(cornerRadius: size.cornerRadius)
.stroke(variant.borderColor(colorScheme), lineWidth: variant.borderWidth)
)
.onTapGesture {
action()
}
}
}
enum ButtonVariant {
case `default`, primary, secondary, outline, ghost
func backgroundColor(_ colorScheme: ColorScheme) -> Color {
switch self {
case .primary: return .blue
case .secondary: return .gray
case .outline, .ghost: return .clear
default: return .blue
}
}
func foregroundColor(_ colorScheme: ColorScheme) -> Color {
switch self {
case .primary, .secondary, .default: return .white
case .outline, .ghost: return colorScheme == .dark ? .white : .black
}
}
func borderColor(_ colorScheme: ColorScheme) -> Color {
switch self {
case .outline: return .gray
default: return .clear
}
}
var borderWidth: CGFloat {
switch self {
case .outline: return 1
default: return 0
}
}
}
enum ButtonSize {
case small, medium, large, icon
var padding: EdgeInsets {
switch self {
case .small: return EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)
case .medium: return EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16)
case .large: return EdgeInsets(top: 12, leading: 20, bottom: 12, trailing: 20)
case .icon: return EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
}
}
var font: Font {
switch self {
case .small: return .caption
case .medium: return .body
case .large: return .title3
case .icon: return .body
}
}
var cornerRadius: CGFloat {
switch self {
case .small: return 6
case .medium: return 8
case .large: return 10
case .icon: return 8
}
}
}
#Preview {
VStack {
PwdButton(
label: Text("Click me!"),
variant: .primary,
action: {}
)
PwdButton(
label: Text("Click me!"),
variant: .secondary,
action: {}
)
PwdButton(
label: Text("Click me!"),
variant: .outline,
action: {}
)
PwdButton(
label: Text("Click me!"),
variant: .ghost,
action: {}
)
PwdButton(
label: Image(systemName: "plus"),
variant: .primary,
size: .icon,
action: {}
)
}.padding()
}

View file

@ -0,0 +1,28 @@
//
// ModernInputStyle.swift
// password
//
// Created by Markus Thielker on 19.01.25.
//
import SwiftUI
struct PwdTextFieldStyle: TextFieldStyle {
@Environment(\.colorScheme) var colorScheme
private let radius: CGFloat = 12
func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
.background(colorScheme == .dark ? .black : .white)
.foregroundColor(colorScheme == .dark ? .white : .black)
.textFieldStyle(.plain)
.cornerRadius(radius)
.overlay(
RoundedRectangle(cornerRadius: radius)
.stroke(.gray, lineWidth: 1)
)
}
}

View file

@ -26,7 +26,9 @@ struct AddPasswordView: View {
.padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0))
Form { Form {
TextField("Name", text: $name) TextField("Name", text: $name)
.textFieldStyle(PwdTextFieldStyle())
TextField("Value", text: $value) TextField("Value", text: $value)
.textFieldStyle(PwdTextFieldStyle())
Text("The password will not be visible again later. Make sure to save it somewhere else too!") Text("The password will not be visible again later. Make sure to save it somewhere else too!")
.font(.footnote) .font(.footnote)
HStack { HStack {

View file

@ -25,10 +25,16 @@ struct DetailView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("Enter the password for \(viewModel.password.name) and submit with \"Enter\"") VStack {
Text(viewModel.password.name)
.font(.title)
.foregroundColor(.primary)
Text("Enter the password and submit with \"Enter\"")
.font(.title3)
}
Form { Form {
SecureField("", text: $value) SecureField("", text: $value)
.textFieldStyle(RoundedBorderTextFieldStyle()) .textFieldStyle(PwdTextFieldStyle())
.onChange(of: value) { _, _ in .onChange(of: value) { _, _ in
if (value.isEmpty){ if (value.isEmpty){
startTime = nil startTime = nil

View file

@ -11,35 +11,80 @@ import _SwiftData_SwiftUI
struct ListView: View { struct ListView: View {
@Environment(\.modelContext) var context @Environment(\.modelContext) var context
@Environment(\.colorScheme) var colorScheme
@ObservedObject var viewModel: ListViewModel @ObservedObject var viewModel: ListViewModel
@State var isAddingPassword: Bool = false
@State private var isAddingPassword: Bool = false
@State private var isUpdateTextVisible: Bool = false
@State private var selectedItem: UUID?
var body: some View { var body: some View {
NavigationView { NavigationStack {
List { HStack {
ForEach(viewModel.passwords) { password in List {
NavigationLink(destination: DetailView(viewModel: DetailViewModel(context: context, passwordID: password.id))) { HStack {
Text(password.name) Text("Your passwords")
.fontWeight(.bold)
Spacer()
PwdButton(
label: Image(systemName: "plus"),
variant: .primary,
size: .icon,
action: { isAddingPassword = true }
)
PwdButton(
label: Image(systemName: "arrow.trianglehead.clockwise")
.imageScale(.small),
variant: .primary,
size: .icon,
action: {
viewModel.passwords = viewModel.getAllPasswords()
isUpdateTextVisible = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
isUpdateTextVisible = false
}
}
)
}
.background(.clear)
.frame(maxWidth: .infinity)
ForEach(viewModel.passwords) { password in
Button("\(password.name)") {
selectedItem = password.id
}
.frame(maxWidth: .infinity, alignment: .leading)
.buttonStyle(PlainButtonStyle())
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.background(selectedItem == password.id ? .blue : .clear)
.foregroundColor(selectedItem == password.id ? .white : (colorScheme == .dark ? .white : .black))
.cornerRadius(8)
}
if isUpdateTextVisible {
Text("List updated")
.animation(.easeInOut(duration: 1), value: isUpdateTextVisible)
}
}
.frame(width: 250)
.listStyle(SidebarListStyle())
if let selectedItem = selectedItem {
DetailView(viewModel: DetailViewModel(context: context, passwordID: selectedItem))
} else {
HStack {
Spacer()
Text("Select a password")
Spacer()
} }
} }
} }
.scrollContentBackground(.hidden) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.toolbar {
Button(action: { isAddingPassword = true }) {
Image(systemName: "plus")
.frame(width: 20, height: 20)
}
Button(action: {
viewModel.passwords = viewModel.getAllPasswords()
}) {
Image(systemName: "arrow.trianglehead.clockwise")
.imageScale(.medium)
.frame(width: 20, height: 20)
}
}
} }
.navigationTitle("Password Trainer") .windowToolbarFullScreenVisibility(.onHover)
.sheet(isPresented: $isAddingPassword) { .sheet(isPresented: $isAddingPassword) {
AddPasswordView(viewModel: viewModel) AddPasswordView(viewModel: viewModel)
} }