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