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
|
# macOS
|
||||||
build/
|
build/
|
||||||
*.app
|
*.app
|
||||||
|
|
||||||
|
# intelliJ
|
||||||
|
.idea
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)";
|
||||||
|
|
|
@ -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 {
|
||||||
|
#if !DEBUG
|
||||||
authenticate()
|
authenticate()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modelContainer(for: [Password.self, PasswordAttempt.self])
|
.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))
|
.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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
HStack {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.passwords) { password in
|
HStack {
|
||||||
NavigationLink(destination: DetailView(viewModel: DetailViewModel(context: context, passwordID: password.id))) {
|
Text("Your passwords")
|
||||||
Text(password.name)
|
.fontWeight(.bold)
|
||||||
}
|
|
||||||
}
|
Spacer()
|
||||||
}
|
|
||||||
.scrollContentBackground(.hidden)
|
PwdButton(
|
||||||
.toolbar {
|
label: Image(systemName: "plus"),
|
||||||
Button(action: { isAddingPassword = true }) {
|
variant: .primary,
|
||||||
Image(systemName: "plus")
|
size: .icon,
|
||||||
.frame(width: 20, height: 20)
|
action: { isAddingPassword = true }
|
||||||
}
|
)
|
||||||
Button(action: {
|
PwdButton(
|
||||||
|
label: Image(systemName: "arrow.trianglehead.clockwise")
|
||||||
|
.imageScale(.small),
|
||||||
|
variant: .primary,
|
||||||
|
size: .icon,
|
||||||
|
action: {
|
||||||
viewModel.passwords = viewModel.getAllPasswords()
|
viewModel.passwords = viewModel.getAllPasswords()
|
||||||
}) {
|
isUpdateTextVisible = true
|
||||||
Image(systemName: "arrow.trianglehead.clockwise")
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
.imageScale(.medium)
|
isUpdateTextVisible = false
|
||||||
.frame(width: 20, height: 20)
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Password Trainer")
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.windowToolbarFullScreenVisibility(.onHover)
|
||||||
.sheet(isPresented: $isAddingPassword) {
|
.sheet(isPresented: $isAddingPassword) {
|
||||||
AddPasswordView(viewModel: viewModel)
|
AddPasswordView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue