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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D60E9D7326D1ABF9009A4537 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -159,7 +158,6 @@
|
||||||
D604B0F72744036F00960D31 /* Packages */ = {
|
D604B0F72744036F00960D31 /* Packages */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D604B0F82744036F00960D31 /* CodeScanner */,
|
|
||||||
);
|
);
|
||||||
name = Packages;
|
name = Packages;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
|
@ -7,12 +7,12 @@
|
||||||
<key>OTP.xcscheme_^#shared#^_</key>
|
<key>OTP.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>OTPKit.xcscheme_^#shared#^_</key>
|
<key>OTPKit.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
@ -24,11 +24,13 @@ struct AddKeyButton: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Menu {
|
Menu {
|
||||||
Section {
|
Section {
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
Button {
|
Button {
|
||||||
isPresentingScanner = true
|
isPresentingScanner = true
|
||||||
} label: {
|
} label: {
|
||||||
Label("Scan QR", systemImage: "qrcode.viewfinder")
|
Label("Scan QR", systemImage: "qrcode.viewfinder")
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
Button {
|
Button {
|
||||||
isPresentingAddURLSheet = true
|
isPresentingAddURLSheet = true
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -55,11 +57,14 @@ struct AddKeyButton: View {
|
||||||
}
|
}
|
||||||
.menuStyle(.borderlessButton)
|
.menuStyle(.borderlessButton)
|
||||||
.accessibilityLabel("Add Key")
|
.accessibilityLabel("Add Key")
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
.sheet(isPresented: $isPresentingScanner, content: self.scannerSheet)
|
.sheet(isPresented: $isPresentingScanner, content: self.scannerSheet)
|
||||||
|
#endif
|
||||||
.sheet(isPresented: $isPresentingManualAddFormSheet, content: self.manualAddFormSheet)
|
.sheet(isPresented: $isPresentingManualAddFormSheet, content: self.manualAddFormSheet)
|
||||||
.sheet(isPresented: $isPresentingAddURLSheet, content: self.addURLSheet)
|
.sheet(isPresented: $isPresentingAddURLSheet, content: self.addURLSheet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
private func scannerSheet() -> some View {
|
private func scannerSheet() -> some View {
|
||||||
AddQRView() { (action) in
|
AddQRView() { (action) in
|
||||||
self.isPresentingScanner = false
|
self.isPresentingScanner = false
|
||||||
|
@ -71,6 +76,7 @@ struct AddKeyButton: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private func addURLSheet() -> some View {
|
private func addURLSheet() -> some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
// Created by Shadowfacts on 8/22/21.
|
// Created by Shadowfacts on 8/22/21.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CodeScanner
|
import CodeScanner
|
||||||
import OTPKit
|
import OTPKit
|
||||||
|
@ -93,3 +94,4 @@ struct AddQRView_Previews: PreviewProvider {
|
||||||
AddQRView() { (_) in }
|
AddQRView() { (_) in }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
|
@ -12,6 +12,7 @@ struct KeyView: View {
|
||||||
let key: TOTPKey
|
let key: TOTPKey
|
||||||
let currentCode: TOTPCode
|
let currentCode: TOTPCode
|
||||||
@State private var copying = false
|
@State private var copying = false
|
||||||
|
@State private var copiedLabelWidth: CGFloat = 0
|
||||||
|
|
||||||
private var formattedCode: String {
|
private var formattedCode: String {
|
||||||
let code = currentCode.code
|
let code = currentCode.code
|
||||||
|
@ -27,36 +28,48 @@ struct KeyView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: self.copy) {
|
Button(action: self.copy) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
ZStack {
|
||||||
Text(key.issuer)
|
HStack {
|
||||||
.font(.title3)
|
VStack(alignment: .leading) {
|
||||||
|
Text(key.issuer)
|
||||||
if let label = key.label, !label.isEmpty {
|
.font(.title3)
|
||||||
Text(label)
|
|
||||||
.font(.footnote)
|
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()
|
||||||
}
|
}
|
||||||
|
.padding(.trailing, 8)
|
||||||
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())
|
|
||||||
|
|
||||||
// I don't think this TimelineView should be necessary since the CodeHolder timer fires every .5 seconds
|
// I don't think this TimelineView should be necessary since the CodeHolder timer fires every .5 seconds
|
||||||
TimelineView(.animation) { (ctx) in
|
TimelineView(.animation) { (ctx) in
|
||||||
ZStack {
|
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)
|
Text(Int(round(currentCode.validUntil.timeIntervalSinceNow)).description)
|
||||||
.font(.caption.monospacedDigit())
|
.font(.caption.monospacedDigit())
|
||||||
|
@ -79,12 +92,22 @@ struct KeyView: View {
|
||||||
withAnimation(.easeInOut(duration: 0.5)) {
|
withAnimation(.easeInOut(duration: 0.5)) {
|
||||||
copying = true
|
copying = true
|
||||||
}
|
}
|
||||||
withAnimation(.easeInOut(duration: 0.5).delay(0.65)) {
|
// .easeInOut(duration: 0.5).delay(0.65) does not work any more
|
||||||
copying = false
|
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 {
|
struct KeyView_Previews: PreviewProvider {
|
||||||
static var key: TOTPKey {
|
static var key: TOTPKey {
|
||||||
TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!)!
|
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
|
.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 {
|
switch result {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
self.importFailedError = error
|
self.importFailedError = error
|
||||||
self.isPresentingImportFailedAlert = true
|
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 {
|
do {
|
||||||
let backup = try BackupDocument(url: urls.first!)
|
let backup = try BackupDocument(url: url)
|
||||||
store.updateFromStore(backup.data, replaceExisting: clearBeforeImport)
|
store.updateFromStore(backup.data, replaceExisting: clearBeforeImport)
|
||||||
dismiss()
|
dismiss()
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -71,6 +79,10 @@ struct PreferencesView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ImportError: LocalizedError {
|
||||||
|
case accessingSecurityScopedResource
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PreferencesView_Previews: PreviewProvider {
|
struct PreferencesView_Previews: PreviewProvider {
|
||||||
|
|
Loading…
Reference in New Issue