PWD-1: add custom UI style (#5)

This commit is contained in:
Markus Thielker 2025-01-24 13:45:08 +01:00 committed by GitHub
commit b8baca3461
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 258 additions and 30 deletions

3
.gitignore vendored
View file

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

View file

@ -7,6 +7,6 @@ Gamification like success rates, typing time tracking and daily prompts to enter
## Security
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
- No cloud sync (unless you are using your iCloud Keychain)

View file

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

View file

@ -51,13 +51,17 @@ struct passwordApp: App {
if isAuthenticated {
ContextWrapper()
} else {
Button("Authenticate") {
authenticate()
}
PwdButton(
label: Text("Authenticate"),
variant: .primary,
action: authenticate
)
}
}
.onAppear {
authenticate()
#if !DEBUG
authenticate()
#endif
}
}
.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))
Form {
TextField("Name", text: $name)
.textFieldStyle(PwdTextFieldStyle())
TextField("Value", text: $value)
.textFieldStyle(PwdTextFieldStyle())
Text("The password will not be visible again later. Make sure to save it somewhere else too!")
.font(.footnote)
HStack {

View file

@ -25,10 +25,16 @@ struct DetailView: View {
var body: some View {
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 {
SecureField("", text: $value)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textFieldStyle(PwdTextFieldStyle())
.onChange(of: value) { _, _ in
if (value.isEmpty){
startTime = nil

View file

@ -11,35 +11,80 @@ import _SwiftData_SwiftUI
struct ListView: View {
@Environment(\.modelContext) var context
@Environment(\.colorScheme) var colorScheme
@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 {
NavigationView {
List {
ForEach(viewModel.passwords) { password in
NavigationLink(destination: DetailView(viewModel: DetailViewModel(context: context, passwordID: password.id))) {
Text(password.name)
NavigationStack {
HStack {
List {
HStack {
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)
.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)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
.navigationTitle("Password Trainer")
.windowToolbarFullScreenVisibility(.onHover)
.sheet(isPresented: $isAddingPassword) {
AddPasswordView(viewModel: viewModel)
}