PWD-1: add custom UI style (#5)
This commit is contained in:
commit
b8baca3461
9 changed files with 258 additions and 30 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -14,3 +14,6 @@ DerivedData
|
|||
# macOS
|
||||
build/
|
||||
*.app
|
||||
|
||||
# intelliJ
|
||||
.idea
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)";
|
||||
|
|
|
@ -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])
|
||||
|
|
140
password/ui/components/PwdButton.swift
Normal file
140
password/ui/components/PwdButton.swift
Normal 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()
|
||||
}
|
28
password/ui/styles/PwdTextFieldStyle.swift
Normal file
28
password/ui/styles/PwdTextFieldStyle.swift
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue