Navigate password folders on iPhone
This commit is contained in:
@@ -1523,6 +1523,138 @@ public func FfiConverterTypeMobilePage_lower(_ value: MobilePage) -> RustBuffer
|
||||
}
|
||||
|
||||
|
||||
public struct MobilePasswordPage: Equatable, Hashable {
|
||||
public var id: String
|
||||
public var path: String
|
||||
public var title: String
|
||||
public var rows: [MobilePasswordRow]
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(id: String, path: String, title: String, rows: [MobilePasswordRow]) {
|
||||
self.id = id
|
||||
self.path = path
|
||||
self.title = title
|
||||
self.rows = rows
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobilePasswordPage: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobilePasswordPage: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordPage {
|
||||
return
|
||||
try MobilePasswordPage(
|
||||
id: FfiConverterString.read(from: &buf),
|
||||
path: FfiConverterString.read(from: &buf),
|
||||
title: FfiConverterString.read(from: &buf),
|
||||
rows: FfiConverterSequenceTypeMobilePasswordRow.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobilePasswordPage, into buf: inout [UInt8]) {
|
||||
FfiConverterString.write(value.id, into: &buf)
|
||||
FfiConverterString.write(value.path, into: &buf)
|
||||
FfiConverterString.write(value.title, into: &buf)
|
||||
FfiConverterSequenceTypeMobilePasswordRow.write(value.rows, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordPage_lift(_ buf: RustBuffer) throws -> MobilePasswordPage {
|
||||
return try FfiConverterTypeMobilePasswordPage.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordPage_lower(_ value: MobilePasswordPage) -> RustBuffer {
|
||||
return FfiConverterTypeMobilePasswordPage.lower(value)
|
||||
}
|
||||
|
||||
|
||||
public struct MobilePasswordRow: Equatable, Hashable {
|
||||
public var id: String
|
||||
public var path: String
|
||||
public var title: String
|
||||
public var detail: String
|
||||
public var systemImage: String
|
||||
public var kind: MobilePasswordRowKind
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(id: String, path: String, title: String, detail: String, systemImage: String, kind: MobilePasswordRowKind) {
|
||||
self.id = id
|
||||
self.path = path
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
self.systemImage = systemImage
|
||||
self.kind = kind
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobilePasswordRow: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobilePasswordRow: FfiConverterRustBuffer {
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordRow {
|
||||
return
|
||||
try MobilePasswordRow(
|
||||
id: FfiConverterString.read(from: &buf),
|
||||
path: FfiConverterString.read(from: &buf),
|
||||
title: FfiConverterString.read(from: &buf),
|
||||
detail: FfiConverterString.read(from: &buf),
|
||||
systemImage: FfiConverterString.read(from: &buf),
|
||||
kind: FfiConverterTypeMobilePasswordRowKind.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
public static func write(_ value: MobilePasswordRow, into buf: inout [UInt8]) {
|
||||
FfiConverterString.write(value.id, into: &buf)
|
||||
FfiConverterString.write(value.path, into: &buf)
|
||||
FfiConverterString.write(value.title, into: &buf)
|
||||
FfiConverterString.write(value.detail, into: &buf)
|
||||
FfiConverterString.write(value.systemImage, into: &buf)
|
||||
FfiConverterTypeMobilePasswordRowKind.write(value.kind, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordRow_lift(_ buf: RustBuffer) throws -> MobilePasswordRow {
|
||||
return try FfiConverterTypeMobilePasswordRow.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordRow_lower(_ value: MobilePasswordRow) -> RustBuffer {
|
||||
return FfiConverterTypeMobilePasswordRow.lower(value)
|
||||
}
|
||||
|
||||
|
||||
public struct MobileShell: Equatable, Hashable {
|
||||
public var selectedTab: MobileTab
|
||||
public var pages: [MobilePage]
|
||||
@@ -2372,6 +2504,238 @@ public func FfiConverterTypeMobileOnboardingPhase_lower(_ value: MobileOnboardin
|
||||
|
||||
|
||||
|
||||
|
||||
public enum MobilePasswordErrorKind: Equatable, Hashable {
|
||||
|
||||
case missingConfiguration
|
||||
case configuration
|
||||
case invalidPath
|
||||
case directoryMissing
|
||||
case repository
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobilePasswordErrorKind: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobilePasswordErrorKind: FfiConverterRustBuffer {
|
||||
typealias SwiftType = MobilePasswordErrorKind
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordErrorKind {
|
||||
let variant: Int32 = try readInt(&buf)
|
||||
switch variant {
|
||||
|
||||
case 1: return .missingConfiguration
|
||||
|
||||
case 2: return .configuration
|
||||
|
||||
case 3: return .invalidPath
|
||||
|
||||
case 4: return .directoryMissing
|
||||
|
||||
case 5: return .repository
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
}
|
||||
|
||||
public static func write(_ value: MobilePasswordErrorKind, into buf: inout [UInt8]) {
|
||||
switch value {
|
||||
|
||||
|
||||
case .missingConfiguration:
|
||||
writeInt(&buf, Int32(1))
|
||||
|
||||
|
||||
case .configuration:
|
||||
writeInt(&buf, Int32(2))
|
||||
|
||||
|
||||
case .invalidPath:
|
||||
writeInt(&buf, Int32(3))
|
||||
|
||||
|
||||
case .directoryMissing:
|
||||
writeInt(&buf, Int32(4))
|
||||
|
||||
|
||||
case .repository:
|
||||
writeInt(&buf, Int32(5))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordErrorKind_lift(_ buf: RustBuffer) throws -> MobilePasswordErrorKind {
|
||||
return try FfiConverterTypeMobilePasswordErrorKind.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordErrorKind_lower(_ value: MobilePasswordErrorKind) -> RustBuffer {
|
||||
return FfiConverterTypeMobilePasswordErrorKind.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
public
|
||||
enum MobilePasswordFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||
|
||||
|
||||
|
||||
case Failed(kind: MobilePasswordErrorKind, title: String, detail: String
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public var errorDescription: String? {
|
||||
String(reflecting: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobilePasswordFfiError: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobilePasswordFfiError: FfiConverterRustBuffer {
|
||||
typealias SwiftType = MobilePasswordFfiError
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordFfiError {
|
||||
let variant: Int32 = try readInt(&buf)
|
||||
switch variant {
|
||||
|
||||
|
||||
|
||||
|
||||
case 1: return .Failed(
|
||||
kind: try FfiConverterTypeMobilePasswordErrorKind.read(from: &buf),
|
||||
title: try FfiConverterString.read(from: &buf),
|
||||
detail: try FfiConverterString.read(from: &buf)
|
||||
)
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
}
|
||||
|
||||
public static func write(_ value: MobilePasswordFfiError, into buf: inout [UInt8]) {
|
||||
switch value {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
case let .Failed(kind,title,detail):
|
||||
writeInt(&buf, Int32(1))
|
||||
FfiConverterTypeMobilePasswordErrorKind.write(kind, into: &buf)
|
||||
FfiConverterString.write(title, into: &buf)
|
||||
FfiConverterString.write(detail, into: &buf)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordFfiError_lift(_ buf: RustBuffer) throws -> MobilePasswordFfiError {
|
||||
return try FfiConverterTypeMobilePasswordFfiError.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordFfiError_lower(_ value: MobilePasswordFfiError) -> RustBuffer {
|
||||
return FfiConverterTypeMobilePasswordFfiError.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
public enum MobilePasswordRowKind: Equatable, Hashable {
|
||||
|
||||
case directory
|
||||
case entry
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if compiler(>=6)
|
||||
extension MobilePasswordRowKind: Sendable {}
|
||||
#endif
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public struct FfiConverterTypeMobilePasswordRowKind: FfiConverterRustBuffer {
|
||||
typealias SwiftType = MobilePasswordRowKind
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordRowKind {
|
||||
let variant: Int32 = try readInt(&buf)
|
||||
switch variant {
|
||||
|
||||
case 1: return .directory
|
||||
|
||||
case 2: return .entry
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
}
|
||||
|
||||
public static func write(_ value: MobilePasswordRowKind, into buf: inout [UInt8]) {
|
||||
switch value {
|
||||
|
||||
|
||||
case .directory:
|
||||
writeInt(&buf, Int32(1))
|
||||
|
||||
|
||||
case .entry:
|
||||
writeInt(&buf, Int32(2))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordRowKind_lift(_ buf: RustBuffer) throws -> MobilePasswordRowKind {
|
||||
return try FfiConverterTypeMobilePasswordRowKind.lift(buf)
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
public func FfiConverterTypeMobilePasswordRowKind_lower(_ value: MobilePasswordRowKind) -> RustBuffer {
|
||||
return FfiConverterTypeMobilePasswordRowKind.lower(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
public
|
||||
enum MobilePreferenceError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||
|
||||
@@ -2637,6 +3001,30 @@ fileprivate struct FfiConverterOptionInt64: FfiConverterRustBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer {
|
||||
typealias SwiftType = String?
|
||||
|
||||
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
|
||||
guard let value = value else {
|
||||
writeInt(&buf, Int8(0))
|
||||
return
|
||||
}
|
||||
writeInt(&buf, Int8(1))
|
||||
FfiConverterString.write(value, into: &buf)
|
||||
}
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
|
||||
switch try readInt(&buf) as Int8 {
|
||||
case 0: return nil
|
||||
case 1: return try FfiConverterString.read(from: &buf)
|
||||
default: throw UniffiInternalError.unexpectedOptionalTag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
@@ -2785,6 +3173,31 @@ fileprivate struct FfiConverterSequenceTypeMobilePage: FfiConverterRustBuffer {
|
||||
return seq
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.8)
|
||||
@_documentation(visibility: private)
|
||||
#endif
|
||||
fileprivate struct FfiConverterSequenceTypeMobilePasswordRow: FfiConverterRustBuffer {
|
||||
typealias SwiftType = [MobilePasswordRow]
|
||||
|
||||
public static func write(_ value: [MobilePasswordRow], into buf: inout [UInt8]) {
|
||||
let len = Int32(value.count)
|
||||
writeInt(&buf, len)
|
||||
for item in value {
|
||||
FfiConverterTypeMobilePasswordRow.write(item, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobilePasswordRow] {
|
||||
let len: Int32 = try readInt(&buf)
|
||||
var seq = [MobilePasswordRow]()
|
||||
seq.reserveCapacity(Int(len))
|
||||
for _ in 0 ..< len {
|
||||
seq.append(try FfiConverterTypeMobilePasswordRow.read(from: &buf))
|
||||
}
|
||||
return seq
|
||||
}
|
||||
}
|
||||
public func mobileHomeOperation() -> MobileHomeOperation {
|
||||
return try! FfiConverterTypeMobileHomeOperation_lift(try! rustCall() {
|
||||
uniffiCallStatus in
|
||||
@@ -2803,6 +3216,14 @@ public func mobileOnboardingOperation(serverUrl: String, account: String, reposi
|
||||
)
|
||||
})
|
||||
}
|
||||
public func mobilePasswordPage(path: String?)throws -> MobilePasswordPage {
|
||||
return try FfiConverterTypeMobilePasswordPage_lift(try rustCallWithError(FfiConverterTypeMobilePasswordFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_func_mobile_password_page(
|
||||
FfiConverterOptionString.lower(path),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
public func mobileShell() -> MobileShell {
|
||||
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
|
||||
uniffiCallStatus in
|
||||
@@ -2862,6 +3283,9 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation() != 8354) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_password_page() != 64312) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_shell() != 42687) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
|
||||
@@ -324,6 +324,11 @@ uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(RustCallStatus *
|
||||
uint64_t uniffi_ironstorage_apple_fn_func_mobile_onboarding_operation(RustBuffer server_url, RustBuffer account, RustBuffer repository_path, RustBuffer application_token, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_PASSWORD_PAGE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_PASSWORD_PAGE
|
||||
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_password_page(RustBuffer path, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
|
||||
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_shell(RustCallStatus *_Nonnull out_status
|
||||
@@ -621,6 +626,12 @@ uint16_t uniffi_ironstorage_apple_checksum_func_mobile_home_operation(void
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_ONBOARDING_OPERATION
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_PAGE
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_PAGE
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_password_page(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL
|
||||
|
||||
@@ -25,6 +25,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private protocol MobileTabRoot: AnyObject {
|
||||
var shellTab: MobileTab { get }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||
private let tabs = UITabBarController()
|
||||
@@ -34,7 +39,9 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||
func makeRootController() -> UIViewController {
|
||||
let shell = mobileShellFixture(state: .loading)
|
||||
navigationControllers = shell.pages.map { page in
|
||||
let root = ShellViewController(page: page)
|
||||
let root: UIViewController = page.tab == .passwords
|
||||
? PasswordDirectoryViewController(shellPage: page)
|
||||
: ShellViewController(page: page)
|
||||
let navigation = UINavigationController(rootViewController: root)
|
||||
navigation.navigationBar.prefersLargeTitles = true
|
||||
navigation.tabBarItem = UITabBarItem(
|
||||
@@ -56,7 +63,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||
) {
|
||||
guard
|
||||
let navigation = viewController as? UINavigationController,
|
||||
let root = navigation.viewControllers.first as? ShellViewController
|
||||
let root = navigation.viewControllers.first as? MobileTabRoot
|
||||
else {
|
||||
return
|
||||
}
|
||||
@@ -86,7 +93,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class ShellViewController: UITableViewController {
|
||||
private final class ShellViewController: UITableViewController, MobileTabRoot {
|
||||
fileprivate let shellTab: MobileTab
|
||||
private var page: MobilePage
|
||||
private var loadTask: Task<Void, Never>?
|
||||
@@ -607,6 +614,289 @@ private final class ShellViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class PasswordDirectoryViewController: UITableViewController, MobileTabRoot {
|
||||
fileprivate let shellTab = MobileTab.passwords
|
||||
private let path: String?
|
||||
private var shellPage: MobilePage?
|
||||
private var directoryPage: MobilePasswordPage?
|
||||
private var loadTask: Task<Void, Never>?
|
||||
private var loadGeneration = 0
|
||||
|
||||
init(shellPage: MobilePage, path: String? = nil) {
|
||||
self.shellPage = shellPage
|
||||
self.path = path
|
||||
super.init(style: .insetGrouped)
|
||||
title = shellPage.title
|
||||
navigationItem.largeTitleDisplayMode = path == nil ? .always : .never
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(localStoreDidChange),
|
||||
name: .ironStorageLocalStoreDidChange,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
private init(path: String, title: String) {
|
||||
self.path = path
|
||||
shellPage = nil
|
||||
super.init(style: .insetGrouped)
|
||||
self.title = title
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(localStoreDidChange),
|
||||
name: .ironStorageLocalStoreDidChange,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
deinit {
|
||||
loadTask?.cancel()
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
if let shellPage {
|
||||
apply(shellPage)
|
||||
} else {
|
||||
showLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
reload()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
loadGeneration += 1
|
||||
loadTask?.cancel()
|
||||
refreshControl?.endRefreshing()
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
directoryPage?.rows.isEmpty == false ? 1 : 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
directoryPage?.rows.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||
guard let row = directoryPage?.rows[indexPath.row] else { return cell }
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.image = UIImage(systemName: row.systemImage)
|
||||
content.text = row.title
|
||||
content.secondaryText = row.detail
|
||||
content.textProperties.numberOfLines = 2
|
||||
content.secondaryTextProperties.numberOfLines = 1
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.accessibilityLabel = "\(row.title), \(row.detail)"
|
||||
cell.accessibilityHint = row.kind == .directory
|
||||
? "Opens this password folder."
|
||||
: "Opens the locked password viewer."
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let row = directoryPage?.rows[indexPath.row] else { return }
|
||||
let controller: UIViewController = switch row.kind {
|
||||
case .directory:
|
||||
PasswordDirectoryViewController(path: row.path, title: row.title)
|
||||
case .entry:
|
||||
LockedPasswordViewController(entry: row)
|
||||
}
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
|
||||
@objc private func refreshRequested() {
|
||||
reload()
|
||||
}
|
||||
|
||||
@objc private func localStoreDidChange() {
|
||||
guard viewIfLoaded?.window != nil else { return }
|
||||
reload()
|
||||
}
|
||||
|
||||
private func reload() {
|
||||
loadGeneration += 1
|
||||
let generation = loadGeneration
|
||||
let selectedID = tableView.indexPathForSelectedRow.flatMap {
|
||||
directoryPage?.rows[$0.row].id
|
||||
}
|
||||
loadTask?.cancel()
|
||||
loadTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
if path == nil {
|
||||
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
|
||||
guard
|
||||
!Task.isCancelled,
|
||||
generation == loadGeneration,
|
||||
let page = shell.pages.first(where: { $0.tab == .passwords })
|
||||
else { return }
|
||||
apply(page)
|
||||
guard page.state == .ready else { return }
|
||||
} else {
|
||||
showLoading()
|
||||
}
|
||||
let path = self.path
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
return Result<MobilePasswordPage, PasswordFailure>.success(
|
||||
try mobilePasswordPage(path: path)
|
||||
)
|
||||
} catch let error as MobilePasswordFfiError {
|
||||
return .failure(PasswordFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
guard !Task.isCancelled, generation == loadGeneration else { return }
|
||||
finish(result, selectedID: selectedID)
|
||||
}
|
||||
}
|
||||
|
||||
private func apply(_ page: MobilePage) {
|
||||
shellPage = page
|
||||
title = page.title
|
||||
guard page.state == .ready else {
|
||||
directoryPage = nil
|
||||
tableView.reloadData()
|
||||
refreshControl?.endRefreshing()
|
||||
var configuration = page.state == .loading
|
||||
? UIContentUnavailableConfiguration.loading()
|
||||
: UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = page.state == .loading
|
||||
? nil
|
||||
: UIImage(systemName: page.state == .error ? "exclamationmark.triangle" : "lock.shield")
|
||||
configuration.text = page.stateTitle
|
||||
configuration.secondaryText = page.stateDetail
|
||||
contentUnavailableConfiguration = configuration
|
||||
return
|
||||
}
|
||||
showLoading()
|
||||
}
|
||||
|
||||
private func finish(
|
||||
_ result: Result<MobilePasswordPage, PasswordFailure>,
|
||||
selectedID: String?
|
||||
) {
|
||||
refreshControl?.endRefreshing()
|
||||
switch result {
|
||||
case let .success(page):
|
||||
directoryPage = page
|
||||
title = page.title
|
||||
contentUnavailableConfiguration = nil
|
||||
tableView.reloadData()
|
||||
if page.rows.isEmpty {
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = UIImage(systemName: "folder")
|
||||
configuration.text = "Empty Folder"
|
||||
configuration.secondaryText = "This password folder contains no entries."
|
||||
contentUnavailableConfiguration = configuration
|
||||
} else if let selectedID,
|
||||
let row = page.rows.firstIndex(where: { $0.id == selectedID }) {
|
||||
tableView.selectRow(at: IndexPath(row: row, section: 0), animated: false, scrollPosition: .none)
|
||||
}
|
||||
case let .failure(failure):
|
||||
if failure.kind == .directoryMissing, path != nil {
|
||||
UIAccessibility.post(
|
||||
notification: .announcement,
|
||||
argument: "The password folder no longer exists."
|
||||
)
|
||||
navigationController?.popViewController(animated: true)
|
||||
return
|
||||
}
|
||||
directoryPage = nil
|
||||
tableView.reloadData()
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = UIImage(systemName: "exclamationmark.triangle")
|
||||
configuration.text = failure.title
|
||||
configuration.secondaryText = failure.detail
|
||||
contentUnavailableConfiguration = configuration
|
||||
}
|
||||
}
|
||||
|
||||
private func showLoading() {
|
||||
var configuration = UIContentUnavailableConfiguration.loading()
|
||||
configuration.text = "Loading Passwords"
|
||||
configuration.secondaryText = "Reading the locked folder model from storage."
|
||||
contentUnavailableConfiguration = configuration
|
||||
}
|
||||
}
|
||||
|
||||
private struct PasswordFailure: Error, Sendable {
|
||||
let kind: MobilePasswordErrorKind
|
||||
let title: String
|
||||
let detail: String
|
||||
|
||||
init(_ error: MobilePasswordFfiError) {
|
||||
switch error {
|
||||
case let .Failed(kind, title, detail):
|
||||
self.kind = kind
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
}
|
||||
}
|
||||
|
||||
static let unexpected = PasswordFailure(
|
||||
kind: .repository,
|
||||
title: "Passwords Are Unavailable",
|
||||
detail: "IronStorage could not load the storage-provided folder."
|
||||
)
|
||||
|
||||
private init(kind: MobilePasswordErrorKind, title: String, detail: String) {
|
||||
self.kind = kind
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class LockedPasswordViewController: UIViewController {
|
||||
private let entry: MobilePasswordRow
|
||||
|
||||
init(entry: MobilePasswordRow) {
|
||||
self.entry = entry
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
title = entry.title
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemGroupedBackground
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = UIImage(systemName: "lock.fill")
|
||||
configuration.text = "Locked Password"
|
||||
configuration.secondaryText = "Authenticate to reveal this password entry."
|
||||
contentUnavailableConfiguration = configuration
|
||||
view.accessibilityIdentifier = entry.id
|
||||
}
|
||||
}
|
||||
|
||||
private struct HomeFailure: Error, Sendable {
|
||||
let kind: MobileHomeErrorKind
|
||||
let title: String
|
||||
|
||||
Reference in New Issue
Block a user