Compare commits
4 Commits
02e229042a
...
94c39fb5c5
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 94c39fb5c5 | |
Shadowfacts | 13d4ce0ab7 | |
Shadowfacts | bd54e3bca3 | |
Shadowfacts | 07f9619217 |
|
@ -89,7 +89,6 @@
|
|||
D60265472744012A00C77599 /* AddKeyButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddKeyButton.swift; sourceTree = "<group>"; };
|
||||
D60265492744014500C77599 /* AllKeysView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllKeysView.swift; sourceTree = "<group>"; };
|
||||
D602654B274401F800C77599 /* OTP.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OTP.entitlements; sourceTree = "<group>"; };
|
||||
D604B0F82744036F00960D31 /* CodeScanner */ = {isa = PBXFileReference; lastKnownFileType = folder; name = CodeScanner; path = ../CodeScanner; sourceTree = "<group>"; };
|
||||
D60E9D6D26D1998B009A4537 /* OTPGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPGeneratorTests.swift; sourceTree = "<group>"; };
|
||||
D60E9D7126D1A863009A4537 /* KeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyView.swift; sourceTree = "<group>"; };
|
||||
D60E9D7326D1ABF9009A4537 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
|
||||
|
@ -159,7 +158,6 @@
|
|||
D604B0F72744036F00960D31 /* Packages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D604B0F82744036F00960D31 /* CodeScanner */,
|
||||
);
|
||||
name = Packages;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
<key>OTP.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>OTPKit.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
|
|
@ -24,11 +24,13 @@ struct AddKeyButton: View {
|
|||
var body: some View {
|
||||
Menu {
|
||||
Section {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
Button {
|
||||
isPresentingScanner = true
|
||||
} label: {
|
||||
Label("Scan QR", systemImage: "qrcode.viewfinder")
|
||||
}
|
||||
#endif
|
||||
Button {
|
||||
isPresentingAddURLSheet = true
|
||||
} label: {
|
||||
|
@ -55,11 +57,14 @@ struct AddKeyButton: View {
|
|||
}
|
||||
.menuStyle(.borderlessButton)
|
||||
.accessibilityLabel("Add Key")
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
.sheet(isPresented: $isPresentingScanner, content: self.scannerSheet)
|
||||
#endif
|
||||
.sheet(isPresented: $isPresentingManualAddFormSheet, content: self.manualAddFormSheet)
|
||||
.sheet(isPresented: $isPresentingAddURLSheet, content: self.addURLSheet)
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
private func scannerSheet() -> some View {
|
||||
AddQRView() { (action) in
|
||||
self.isPresentingScanner = false
|
||||
|
@ -71,6 +76,7 @@ struct AddKeyButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func addURLSheet() -> some View {
|
||||
NavigationView {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// Created by Shadowfacts on 8/22/21.
|
||||
//
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
import SwiftUI
|
||||
import CodeScanner
|
||||
import OTPKit
|
||||
|
@ -93,3 +94,4 @@ struct AddQRView_Previews: PreviewProvider {
|
|||
AddQRView() { (_) in }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -12,6 +12,7 @@ struct KeyView: View {
|
|||
let key: TOTPKey
|
||||
let currentCode: TOTPCode
|
||||
@State private var copying = false
|
||||
@State private var copiedLabelWidth: CGFloat = 0
|
||||
|
||||
private var formattedCode: String {
|
||||
let code = currentCode.code
|
||||
|
@ -27,36 +28,48 @@ struct KeyView: View {
|
|||
var body: some View {
|
||||
Button(action: self.copy) {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(key.issuer)
|
||||
.font(.title3)
|
||||
|
||||
if let label = key.label, !label.isEmpty {
|
||||
Text(label)
|
||||
.font(.footnote)
|
||||
ZStack {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(key.issuer)
|
||||
.font(.title3)
|
||||
|
||||
if let label = key.label, !label.isEmpty {
|
||||
Text(label)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Text(formattedCode)
|
||||
.font(.system(.title2, design: .monospaced))
|
||||
.opacity(copying ? 0 : 1)
|
||||
|
||||
Text("Copied!")
|
||||
.font(.title2)
|
||||
.opacity(copying ? 1 : 0)
|
||||
// this nonsense shouldn't be necessary, but the transition only works the first time a code is copied, for some indiscernible reason
|
||||
.background(GeometryReader { proxy in
|
||||
Color.clear
|
||||
.preference(key: CopiedLabelWidth.self, value: proxy.size.width)
|
||||
.onPreferenceChange(CopiedLabelWidth.self) { newValue in
|
||||
copiedLabelWidth = newValue
|
||||
}
|
||||
})
|
||||
}
|
||||
.offset(x: copying ? 0 : copiedLabelWidth)
|
||||
.clipped()
|
||||
}
|
||||
|
||||
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())
|
||||
.padding(.trailing, 8)
|
||||
|
||||
// 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))
|
||||
CircularProgressView(progress: progress(at: ctx.date), colorChangeThreshold: 5.0 / Double(key.period))
|
||||
|
||||
Text(Int(round(currentCode.validUntil.timeIntervalSinceNow)).description)
|
||||
.font(.caption.monospacedDigit())
|
||||
|
@ -79,12 +92,22 @@ struct KeyView: View {
|
|||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
copying = true
|
||||
}
|
||||
withAnimation(.easeInOut(duration: 0.5).delay(0.65)) {
|
||||
copying = false
|
||||
// .easeInOut(duration: 0.5).delay(0.65) does not work any more
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(650)) {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
copying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CopiedLabelWidth: PreferenceKey {
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyView_Previews: PreviewProvider {
|
||||
static var key: TOTPKey {
|
||||
TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!)!
|
||||
|
|
|
@ -46,14 +46,22 @@ struct PreferencesView: View {
|
|||
}
|
||||
.fileExporter(isPresented: $isPresentingExport, document: BackupDocument(data: store.data), contentType: .propertyList, defaultFilename: "OTPBackup") { (_) in
|
||||
}
|
||||
.fileImporter(isPresented: $isPresentingImport, allowedContentTypes: [.propertyList], allowsMultipleSelection: false) { (result) in
|
||||
.fileImporter(isPresented: $isPresentingImport, allowedContentTypes: [.propertyList]) { (result) in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
self.importFailedError = error
|
||||
self.isPresentingImportFailedAlert = true
|
||||
case let .success(urls):
|
||||
case let .success(url):
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
self.importFailedError = ImportError.accessingSecurityScopedResource
|
||||
self.isPresentingImportFailedAlert = true
|
||||
return
|
||||
}
|
||||
defer {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
do {
|
||||
let backup = try BackupDocument(url: urls.first!)
|
||||
let backup = try BackupDocument(url: url)
|
||||
store.updateFromStore(backup.data, replaceExisting: clearBeforeImport)
|
||||
dismiss()
|
||||
} catch {
|
||||
|
@ -71,6 +79,10 @@ struct PreferencesView: View {
|
|||
}
|
||||
}
|
||||
|
||||
enum ImportError: LocalizedError {
|
||||
case accessingSecurityScopedResource
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct PreferencesView_Previews: PreviewProvider {
|
||||
|
|
Loading…
Reference in New Issue