OTP/OTP/Views/AppView.swift

211 lines
6.8 KiB
Swift

//
// 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")
}
}
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")
}
}
}
}
.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()
}
}