Compare commits

...

5 Commits

Author SHA1 Message Date
Shadowfacts 8a4196db3a Add search 2021-08-25 13:04:52 -04:00
Shadowfacts 9e16f9a693 Remove unneeded tint modifiers 2021-08-25 11:40:39 -04:00
Shadowfacts 85f60c7261 Add all keys view 2021-08-25 11:38:37 -04:00
Shadowfacts ffd9b8434e Add new keys directly to folders 2021-08-25 11:35:22 -04:00
Shadowfacts 643452459b Add copying 2021-08-25 11:26:21 -04:00
10 changed files with 240 additions and 122 deletions

View File

@ -29,8 +29,8 @@ struct KeyData: Codable {
try container.encode(folders, forKey: .folders) try container.encode(folders, forKey: .folders)
} }
mutating func addKey(_ key: TOTPKey) { mutating func addKey(_ key: TOTPKey, folderID: UUID? = nil) {
entries.append(Entry(key: key)) entries.append(Entry(key: key, folderID: folderID))
} }
mutating func addOrUpdateEntries(_ entries: [Entry]) { mutating func addOrUpdateEntries(_ entries: [Entry]) {

View File

@ -54,8 +54,8 @@ class KeyStore: ObservableObject {
} }
} }
func addKey(_ key: TOTPKey) { func addKey(_ key: TOTPKey, folderID: UUID? = nil) {
data.addKey(key) data.addKey(key, folderID: folderID)
} }
func updateKey(entryID id: UUID, newKey: TOTPKey) { func updateKey(entryID id: UUID, newKey: TOTPKey) {

View File

@ -0,0 +1,109 @@
//
// AddKeyButton.swift
// OTP
//
// Created by Shadowfacts on 8/25/21.
//
import SwiftUI
struct AddKeyButton: View {
let folderID: UUID?
let canAddFolder: Bool
@ObservedObject private var store: KeyStore = .shared
@State private var isPresentingScanner = false
@State private var isPresentingScanFailedAlert = false
@State private var isPresentingAddURLSheet = false
@State private var isPresentingManualAddFormSheet = false
init(folderID: UUID?, canAddFolder: Bool) {
self.folderID = folderID
self.canAddFolder = canAddFolder
}
var body: some View {
Menu {
Section {
Button {
isPresentingScanner = true
} label: {
Label("Scan QR", systemImage: "qrcode.viewfinder")
}
Button {
isPresentingAddURLSheet = true
} label: {
Label("From URL", systemImage: "link")
}
Button {
isPresentingManualAddFormSheet = true
} label: {
Label("Enter Manually", systemImage: "textbox")
}
}
if canAddFolder {
Section {
Button {
store.addFolder()
} label: {
Label("New Folder", systemImage: "folder.badge.plus")
}
}
}
} label: {
Label("Add Key", systemImage: "plus.circle")
}
.accessibilityLabel("Add Key")
.sheet(isPresented: $isPresentingScanner, content: self.scannerSheet)
.sheet(isPresented: $isPresentingManualAddFormSheet, content: self.manualAddFormSheet)
.sheet(isPresented: $isPresentingAddURLSheet, content: self.addURLSheet)
}
private func scannerSheet() -> some View {
AddQRView() { (action) in
self.isPresentingScanner = false
switch action {
case .cancel:
break
case .save(let key):
store.addKey(key, folderID: folderID)
}
}
}
private func addURLSheet() -> some View {
NavigationView {
AddURLForm { (action) in
self.isPresentingAddURLSheet = false
switch action {
case .cancel:
break
case .save(let key):
store.addKey(key, folderID: folderID)
}
}
}
}
private func manualAddFormSheet() -> some View {
NavigationView {
EditKeyForm(editingKey: nil, focusOnAppear: true) { (action) in
self.isPresentingManualAddFormSheet = false
switch action {
case .cancel:
break
case .save(let key):
store.addKey(key, folderID: folderID)
}
}
.navigationTitle("Add Key")
}
}
}
struct AddKeyButton_Previews: PreviewProvider {
static var previews: some View {
AddKeyButton(folderID: nil, canAddFolder: true)
}
}

View File

@ -0,0 +1,40 @@
//
// AllKeysView.swift
// OTP
//
// Created by Shadowfacts on 8/25/21.
//
import SwiftUI
struct AllKeysView: View {
@ObservedObject private var store: KeyStore
@ObservedObject private var codeHolder: AppView.CodeHolder
@State private var searchQuery = ""
init() {
let store = KeyStore.shared
self.store = store
self.codeHolder = AppView.CodeHolder(store: store, entryFilter: nil)
}
var body: some View {
List {
KeysSection(codeHolder: codeHolder, searchQuery: searchQuery)
}
.listStyle(.insetGrouped)
.searchable(text: $searchQuery)
.navigationTitle("All Keys")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
AddKeyButton(folderID: nil, canAddFolder: false)
}
}
}
}
struct AllKeysView_Previews: PreviewProvider {
static var previews: some View {
AllKeysView()
}
}

View File

@ -12,23 +12,32 @@ import Combine
struct AppView: View { struct AppView: View {
@ObservedObject private var store: KeyStore @ObservedObject private var store: KeyStore
@ObservedObject private var entryHolder: CodeHolder @ObservedObject private var entryHolder: CodeHolder
@State private var isPresentingScanner = false @ObservedObject private var allEntriesHolder: CodeHolder
@State private var isPresentingScanFailedAlert = false @State private var searchQuery = ""
@State private var isPresentingAddURLSheet = false
@State private var isPresentingManualAddFormSheet = false
@State private var isPresentingPreferences = false @State private var isPresentingPreferences = false
init() { init() {
self.store = .shared self.store = .shared
self.entryHolder = CodeHolder(store: .shared) { (entry) in entry.folderID == nil } self.entryHolder = CodeHolder(store: .shared) { (entry) in entry.folderID == nil }
self.allEntriesHolder = CodeHolder(store: .shared, entryFilter: nil)
} }
var body: some View { var body: some View {
NavigationView { NavigationView {
List { List {
KeysSection(codeHolder: entryHolder) if searchQuery.isEmpty {
KeysSection(codeHolder: entryHolder)
FoldersSection()
Section {
NavigationLink("All Keys") {
AllKeysView()
}
}
FoldersSection()
} else {
KeysSection(codeHolder: allEntriesHolder, searchQuery: searchQuery)
}
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.navigationTitle("OTP") .navigationTitle("OTP")
@ -38,49 +47,16 @@ struct AppView: View {
isPresentingPreferences = true isPresentingPreferences = true
} label: { } label: {
Label("Preferences", systemImage: "gear") Label("Preferences", systemImage: "gear")
.tint(.accentColor)
} }
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Menu { AddKeyButton(folderID: nil, canAddFolder: true)
Section {
Button {
isPresentingScanner = true
} label: {
Label("Scan QR", systemImage: "qrcode.viewfinder")
}
Button {
isPresentingAddURLSheet = true
} label: {
Label("From URL", systemImage: "link")
}
Button {
isPresentingManualAddFormSheet = true
} label: {
Label("Enter Manually", systemImage: "textbox")
}
}
Section {
Button {
store.addFolder()
} label: {
Label("New Folder", systemImage: "folder.badge.plus")
}
}
} label: {
Label("Add Key", systemImage: "plus.circle")
.tint(.accentColor)
}
} }
} }
} }
.tint(.blue) .searchable(text: $searchQuery)
.sheet(isPresented: $isPresentingPreferences, content: self.preferencesSheet) .sheet(isPresented: $isPresentingPreferences, content: self.preferencesSheet)
.sheet(isPresented: $isPresentingScanner, content: self.scannerSheet)
.sheet(isPresented: $isPresentingManualAddFormSheet, content: self.manualAddFormSheet)
.sheet(isPresented: $isPresentingAddURLSheet, content: self.addURLSheet)
} }
private func preferencesSheet() -> some View { private func preferencesSheet() -> some View {
@ -89,47 +65,6 @@ struct AppView: View {
} }
} }
private func scannerSheet() -> some View {
AddQRView() { (action) in
self.isPresentingScanner = false
switch action {
case .cancel:
break
case .save(let key):
store.addKey(key)
}
}
}
private func addURLSheet() -> some View {
NavigationView {
AddURLForm { (action) in
self.isPresentingAddURLSheet = false
switch action {
case .cancel:
break
case .save(let key):
store.addKey(key)
}
}
}
}
private func manualAddFormSheet() -> some View {
NavigationView {
EditKeyForm(editingKey: nil, focusOnAppear: true) { (action) in
self.isPresentingManualAddFormSheet = false
switch action {
case .cancel:
break
case .save(let key):
store.addKey(key)
}
}
.navigationTitle("Add Key")
}
}
struct CodeEntry: Identifiable, Equatable, Hashable { struct CodeEntry: Identifiable, Equatable, Hashable {
let entry: KeyData.Entry let entry: KeyData.Entry
let code: TOTPCode let code: TOTPCode

View File

@ -94,14 +94,12 @@ struct EditKeyForm: View {
Button("Cancel") { Button("Cancel") {
dismiss(.cancel) dismiss(.cancel)
} }
.tint(.accentColor)
} }
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") { Button("Save") {
dismiss(.save(editedKey.toTOTPKey()!)) dismiss(.save(editedKey.toTOTPKey()!))
} }
.tint(.accentColor)
.disabled(!isValid) .disabled(!isValid)
} }
} }

View File

@ -27,6 +27,11 @@ struct FolderView: View {
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.navigationTitle(folder.name) .navigationTitle(folder.name)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
AddKeyButton(folderID: folder.id, canAddFolder: false)
}
}
} }
} }

View File

@ -11,8 +11,9 @@ import OTPKit
struct KeyView: View { struct KeyView: View {
let key: TOTPKey let key: TOTPKey
let currentCode: TOTPCode let currentCode: TOTPCode
@State private var copying = false
var formattedCode: String { private var formattedCode: String {
let code = currentCode.code let code = currentCode.code
let mid = code.index(code.startIndex, offsetBy: code.count / 2) let mid = code.index(code.startIndex, offsetBy: code.count / 2)
return "\(code[code.startIndex..<mid]) \(code[mid...])" return "\(code[code.startIndex..<mid]) \(code[mid...])"
@ -24,37 +25,47 @@ struct KeyView: View {
} }
var body: some View { var body: some View {
HStack { Button(action: self.copy) {
VStack(alignment: .leading) { HStack {
Text(key.issuer) VStack(alignment: .leading) {
.font(.title3) Text(key.issuer)
.font(.title3)
if let label = key.label, !label.isEmpty {
Text(label)
.font(.footnote)
}
}
Spacer()
Text(formattedCode)
.font(.system(.title2, design: .monospaced))
// Text("\(currentCode.validUntil, style: .relative)")
// .font(.body.monospacedDigit())
// I don't think this TimelineView should be necessary since the CodeHolder timer fires every .5 seconds
TimelineView(.animation) { (ctx) in
ZStack {
CircularProgressView(progress: progress(at: Date()), colorChangeThreshold: 5.0 / Double(key.period))
Text(Int(round(currentCode.validUntil.timeIntervalSinceNow)).description) if let label = key.label, !label.isEmpty {
.font(.caption.monospacedDigit()) Text(label)
.font(.footnote)
}
}
Spacer()
if copying {
Text("Copied!")
.font(.title2)
.transition(.move(edge: .trailing).combined(with: .opacity))
} else {
Text(formattedCode)
.font(.system(.title2, design: .monospaced))
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)).combined(with: .opacity))
}
// Text("\(currentCode.validUntil, style: .relative)")
// .font(.body.monospacedDigit())
// I don't think this TimelineView should be necessary since the CodeHolder timer fires every .5 seconds
TimelineView(.animation) { (ctx) in
ZStack {
CircularProgressView(progress: progress(at: Date()), colorChangeThreshold: 5.0 / Double(key.period))
Text(Int(round(currentCode.validUntil.timeIntervalSinceNow)).description)
.font(.caption.monospacedDigit())
}
.frame(width: 30)
} }
.frame(width: 30)
} }
} }
.tint(.black)
} }
private func progress(at date: Date) -> Double { private func progress(at date: Date) -> Double {
@ -62,6 +73,16 @@ struct KeyView: View {
let progress = 1 - seconds / Double(key.period) let progress = 1 - seconds / Double(key.period)
return progress return progress
} }
private func copy() {
UIPasteboard.general.string = currentCode.code
withAnimation(.easeInOut(duration: 0.5)) {
copying = true
}
withAnimation(.easeInOut(duration: 0.5).delay(0.65)) {
copying = false
}
}
} }
struct KeyView_Previews: PreviewProvider { struct KeyView_Previews: PreviewProvider {

View File

@ -9,18 +9,30 @@ import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
struct KeysSection: View { struct KeysSection: View {
private let searchQuery: String?
@ObservedObject private var store: KeyStore = .shared @ObservedObject private var store: KeyStore = .shared
@ObservedObject private var entryHolder: AppView.CodeHolder @ObservedObject private var entryHolder: AppView.CodeHolder
@State private var editedEntry: AppView.CodeEntry? = nil @State private var editedEntry: AppView.CodeEntry? = nil
@State private var presentedQRCode: AppView.CodeEntry? = nil @State private var presentedQRCode: AppView.CodeEntry? = nil
init(codeHolder: AppView.CodeHolder) { init(codeHolder: AppView.CodeHolder, searchQuery: String? = nil) {
self.entryHolder = codeHolder self.entryHolder = codeHolder
self.searchQuery = searchQuery
}
var filteredEntries: [AppView.CodeEntry] {
if let query = searchQuery?.lowercased(), !query.isEmpty {
return entryHolder.sortedEntries.filter { (e) in
e.key.issuer.lowercased().contains(query) || (e.key.label?.lowercased().contains(query) ?? false)
}
} else {
return entryHolder.sortedEntries
}
} }
var body: some View { var body: some View {
Section { Section {
ForEach(entryHolder.sortedEntries) { (entry) in ForEach(filteredEntries) { (entry) in
KeyView(key: entry.key, currentCode: entry.code) KeyView(key: entry.key, currentCode: entry.code)
// disabled because dropping onto list rows does not work :/ // disabled because dropping onto list rows does not work :/
// .onDrag { // .onDrag {
@ -33,7 +45,7 @@ struct KeysSection: View {
.onDelete { (indices) in .onDelete { (indices) in
withAnimation(.default) { withAnimation(.default) {
for index in indices { for index in indices {
store.removeKey(entryID: entryHolder.sortedEntries[index].id) store.removeKey(entryID: filteredEntries[index].id)
} }
} }
} }

View File

@ -76,7 +76,6 @@ struct QRCodeView: View {
Button("Done") { Button("Done") {
dismiss() dismiss()
} }
.tint(.accentColor)
} }
} }
.background { .background {
@ -106,7 +105,6 @@ struct QRCodeView: View {
Label("Save", systemImage: "square.and.arrow.down") Label("Save", systemImage: "square.and.arrow.down")
} }
} }
.tint(.accentColor)
} }
// fix the size so that the HStack doesn't grow beyond with width of the image // fix the size so that the HStack doesn't grow beyond with width of the image
.fixedSize() .fixedSize()