2021-08-24 22:19:31 +00:00
|
|
|
//
|
|
|
|
// AppView.swift
|
|
|
|
// OTP
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 8/21/21.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
import OTPKit
|
|
|
|
import Combine
|
|
|
|
|
|
|
|
struct AppView: View {
|
|
|
|
@ObservedObject private var store: KeyStore
|
|
|
|
@ObservedObject private var entryHolder: CodeHolder
|
|
|
|
@State private var isPresentingScanner = false
|
|
|
|
@State private var isPresentingScanFailedAlert = false
|
|
|
|
@State private var isPresentingAddURLSheet = false
|
|
|
|
@State private var isPresentingManualAddFormSheet = false
|
|
|
|
@State private var isPresentingPreferences = false
|
|
|
|
|
|
|
|
init() {
|
|
|
|
self.store = .shared
|
|
|
|
self.entryHolder = CodeHolder(store: .shared) { (entry) in entry.folderID == nil }
|
|
|
|
}
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
NavigationView {
|
|
|
|
List {
|
|
|
|
KeysSection(codeHolder: entryHolder)
|
|
|
|
|
|
|
|
FoldersSection()
|
|
|
|
}
|
|
|
|
.listStyle(.insetGrouped)
|
|
|
|
.navigationTitle("OTP")
|
|
|
|
.toolbar {
|
|
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
|
|
Button {
|
|
|
|
isPresentingPreferences = true
|
|
|
|
} label: {
|
|
|
|
Label("Preferences", systemImage: "gear")
|
2021-08-25 04:12:06 +00:00
|
|
|
.tint(.accentColor)
|
2021-08-24 22:19:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Section {
|
|
|
|
Button {
|
|
|
|
store.addFolder()
|
|
|
|
} label: {
|
|
|
|
Label("New Folder", systemImage: "folder.badge.plus")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
Label("Add Key", systemImage: "plus.circle")
|
2021-08-25 04:12:06 +00:00
|
|
|
.tint(.accentColor)
|
2021-08-24 22:19:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.tint(.blue)
|
|
|
|
.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 {
|
|
|
|
NavigationView {
|
|
|
|
PreferencesView()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
let entry: KeyData.Entry
|
|
|
|
let code: TOTPCode
|
|
|
|
var key: TOTPKey { entry.key }
|
|
|
|
var id: UUID { entry.id }
|
|
|
|
|
|
|
|
init(_ entry: KeyData.Entry) {
|
|
|
|
self.entry = entry
|
|
|
|
self.code = OTPGenerator.generate(key: entry.key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class CodeHolder: ObservableObject {
|
|
|
|
private let store: KeyStore
|
|
|
|
private let entryFilter: ((KeyData.Entry) -> Bool)?
|
|
|
|
private var timer: Timer!
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
|
|
|
|
init(store: KeyStore, entryFilter: ((KeyData.Entry) -> Bool)? = nil) {
|
|
|
|
self.store = store
|
|
|
|
self.entryFilter = entryFilter
|
|
|
|
|
|
|
|
updateTimer(entries: filterEntries(from: store.data))
|
|
|
|
|
|
|
|
store.$data
|
|
|
|
.sink { [unowned self] (newData) in
|
|
|
|
self.objectWillChange.send()
|
|
|
|
self.updateTimer(entries: filterEntries(from: newData!))
|
|
|
|
}
|
|
|
|
.store(in: &cancellables)
|
|
|
|
}
|
|
|
|
|
|
|
|
var entries: [CodeEntry] {
|
|
|
|
return filterEntries(from: store.data).map { CodeEntry($0) }
|
|
|
|
}
|
|
|
|
|
|
|
|
var sortedEntries: [CodeEntry] {
|
|
|
|
return entries.sorted(by: { (a, b) in
|
|
|
|
if a.key.issuer == b.key.issuer,
|
|
|
|
let aLabel = a.key.label,
|
|
|
|
let bLabel = b.key.label {
|
|
|
|
return aLabel < bLabel
|
|
|
|
} else {
|
|
|
|
return a.key.issuer < b.key.issuer
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private func filterEntries(from data: KeyData) -> [KeyData.Entry] {
|
|
|
|
if let filter = entryFilter {
|
|
|
|
return data.entries.filter(filter)
|
|
|
|
} else {
|
|
|
|
return data.entries
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateTimer(entries: [KeyData.Entry]) {
|
|
|
|
if entries.isEmpty {
|
|
|
|
timer?.invalidate()
|
|
|
|
return
|
|
|
|
} else if timer == nil || !timer.isValid {
|
|
|
|
timer = .scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] (timer) in
|
|
|
|
guard let self = self else {
|
|
|
|
timer.invalidate()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self.objectWillChange.send()
|
|
|
|
}
|
|
|
|
timer.tolerance = 0.01
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
struct AppView_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
|
|
|
AppView()
|
|
|
|
}
|
|
|
|
}
|