148 lines
4.5 KiB
Swift
148 lines
4.5 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
|
|
@ObservedObject private var allEntriesHolder: CodeHolder
|
|
@State private var searchQuery = ""
|
|
@State private var isPresentingPreferences = false
|
|
|
|
init() {
|
|
self.store = .shared
|
|
self.entryHolder = CodeHolder(store: .shared) { (entry) in entry.folderID == nil }
|
|
self.allEntriesHolder = CodeHolder(store: .shared, entryFilter: nil)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
List {
|
|
if searchQuery.isEmpty {
|
|
KeysSection(codeHolder: entryHolder)
|
|
|
|
Section {
|
|
NavigationLink("All Keys") {
|
|
AllKeysView()
|
|
}
|
|
}
|
|
|
|
FoldersSection()
|
|
} else {
|
|
KeysSection(codeHolder: allEntriesHolder, searchQuery: searchQuery)
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.navigationTitle("OTP")
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button {
|
|
isPresentingPreferences = true
|
|
} label: {
|
|
Label("Preferences", systemImage: "gear")
|
|
}
|
|
}
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
AddKeyButton(folderID: nil, canAddFolder: true)
|
|
}
|
|
}
|
|
}
|
|
.searchable(text: $searchQuery)
|
|
.sheet(isPresented: $isPresentingPreferences, content: self.preferencesSheet)
|
|
}
|
|
|
|
private func preferencesSheet() -> some View {
|
|
NavigationView {
|
|
PreferencesView()
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|