diff --git a/.gitignore b/.gitignore index 315f335..e5f8427 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ DerivedData # macOS build/ *.app + +# intelliJ +.idea diff --git a/README.md b/README.md index 700cbac..b245b5d 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/password.xcodeproj/project.pbxproj b/password.xcodeproj/project.pbxproj index 56e343b..270ea8f 100644 --- a/password.xcodeproj/project.pbxproj +++ b/password.xcodeproj/project.pbxproj @@ -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)"; diff --git a/password/passwordApp.swift b/password/passwordApp.swift index 6357c27..3b32951 100644 --- a/password/passwordApp.swift +++ b/password/passwordApp.swift @@ -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]) diff --git a/password/ui/components/PwdButton.swift b/password/ui/components/PwdButton.swift new file mode 100644 index 0000000..b4ade36 --- /dev/null +++ b/password/ui/components/PwdButton.swift @@ -0,0 +1,140 @@ +// +// Button.swift +// password +// +// Created by Markus Thielker on 19.01.25. +// + +import SwiftUI + +struct PwdButton: 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() +} diff --git a/password/ui/styles/PwdTextFieldStyle.swift b/password/ui/styles/PwdTextFieldStyle.swift new file mode 100644 index 0000000..16adef3 --- /dev/null +++ b/password/ui/styles/PwdTextFieldStyle.swift @@ -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) -> 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) + ) + } +} diff --git a/password/view/add/AddView.swift b/password/view/add/AddView.swift index 0fff1ad..4e58d9d 100644 --- a/password/view/add/AddView.swift +++ b/password/view/add/AddView.swift @@ -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 { diff --git a/password/view/detail/DetailView.swift b/password/view/detail/DetailView.swift index 0c7bb93..a894cda 100644 --- a/password/view/detail/DetailView.swift +++ b/password/view/detail/DetailView.swift @@ -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 diff --git a/password/view/list/ListView.swift b/password/view/list/ListView.swift index a6d3ae6..06e2eda 100644 --- a/password/view/list/ListView.swift +++ b/password/view/list/ListView.swift @@ -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) }