Compare commits

...

4 Commits

6 changed files with 74 additions and 33 deletions

View File

@ -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>";

View File

@ -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>

View File

@ -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 {

View File

@ -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

View File

@ -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")!)!

View File

@ -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 {