Share Extension: account picker first, hidden folders follow the app's filter + unlock
The app now publishes every uploadable account, the active one, the three lock settings and each account's own hidden-files filter (ShareAccountSync). The sheet lists accounts first when there are several, asks for unlock to use a non-active account (login lock + account-switching lock), and shows hidden folders per the account's app setting behind the hidden-files unlock. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
0cda4a1241
commit
a37524e959
@@ -1,10 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
/// State behind the share sheet's folder picker.
|
||||
/// State behind the share sheet's account and folder pickers.
|
||||
@MainActor
|
||||
final class ShareModel: ObservableObject {
|
||||
enum Stage: Equatable {
|
||||
case preparing
|
||||
case chooseAccount
|
||||
case picking
|
||||
case uploading
|
||||
case noAccount
|
||||
@@ -17,18 +18,30 @@ final class ShareModel: ObservableObject {
|
||||
@Published var folders: [DavFolder] = []
|
||||
@Published var isLoadingFolders = false
|
||||
@Published var folderError: String?
|
||||
/// A short explanation shown under the list - e.g. hidden folders stayed
|
||||
/// hidden because the unlock was cancelled.
|
||||
@Published var notice: String?
|
||||
@Published private(set) var selected: SharedAccount?
|
||||
|
||||
let account: SharedAccount?
|
||||
let shared: SharedAccounts?
|
||||
|
||||
/// One successful unlock covers every gate for the rest of this sheet.
|
||||
private var unlocked = false
|
||||
private var hidden: HiddenFilter = .hide
|
||||
|
||||
/// Set by the view controller: what each button actually does.
|
||||
var onUpload: () -> Void = {}
|
||||
var onSaveForLater: () -> Void = {}
|
||||
var onCancel: () -> Void = {}
|
||||
|
||||
init(account: SharedAccount?) {
|
||||
self.account = account
|
||||
init(shared: SharedAccounts?) {
|
||||
self.shared = shared
|
||||
}
|
||||
|
||||
var accounts: [SharedAccount] { shared?.accounts ?? [] }
|
||||
var canChangeAccount: Bool { accounts.count > 1 }
|
||||
var activeAccountId: String? { shared?.active?.id }
|
||||
|
||||
var currentFolderName: String {
|
||||
path == "/" ? "All files" : (path as NSString).lastPathComponent
|
||||
}
|
||||
@@ -37,14 +50,66 @@ final class ShareModel: ObservableObject {
|
||||
items.count == 1 ? "1 file" : "\(items.count) files"
|
||||
}
|
||||
|
||||
/// With several accounts the list comes first; with one it goes straight
|
||||
/// to its folders.
|
||||
func start() async {
|
||||
guard let shared, let first = shared.accounts.first else {
|
||||
stage = .noAccount
|
||||
return
|
||||
}
|
||||
if shared.accounts.count > 1 {
|
||||
stage = .chooseAccount
|
||||
} else {
|
||||
await select(first)
|
||||
}
|
||||
}
|
||||
|
||||
func showAccounts() {
|
||||
notice = nil
|
||||
stage = .chooseAccount
|
||||
}
|
||||
|
||||
/// Uploading to an account other than the one the app is on is "switching"
|
||||
/// in the app's terms, so it asks for the same unlock when the app does.
|
||||
/// Hidden folders follow the account's own app setting, behind the app's
|
||||
/// "lock hidden files" unlock.
|
||||
func select(_ account: SharedAccount) async {
|
||||
guard let shared else { return }
|
||||
notice = nil
|
||||
if account.id != shared.active?.id, shared.needsUnlockToSwitchAccount {
|
||||
guard await unlock("Unlock to upload to \(account.displayName)") else {
|
||||
notice = "Unlock to upload to a different account."
|
||||
stage = .chooseAccount
|
||||
return
|
||||
}
|
||||
}
|
||||
selected = account
|
||||
hidden = HiddenFilter(raw: account.hiddenFilter)
|
||||
if hidden != .hide, shared.needsUnlockForHidden {
|
||||
if await !unlock("Unlock to show hidden folders") {
|
||||
hidden = .hide
|
||||
notice = "Hidden folders are locked, so they're not shown."
|
||||
}
|
||||
}
|
||||
stage = .picking
|
||||
await open("/")
|
||||
}
|
||||
|
||||
private func unlock(_ reason: String) async -> Bool {
|
||||
if unlocked { return true }
|
||||
let ok = await DeviceAuth.authenticate(reason: reason)
|
||||
if ok { unlocked = true }
|
||||
return ok
|
||||
}
|
||||
|
||||
func open(_ path: String) async {
|
||||
guard let account else { return }
|
||||
guard let account = selected else { return }
|
||||
self.path = path
|
||||
isLoadingFolders = true
|
||||
folderError = nil
|
||||
defer { isLoadingFolders = false }
|
||||
do {
|
||||
folders = try await DavClient.listFolders(account: account, path: path)
|
||||
folders = try await DavClient.listFolders(account: account, path: path, hidden: hidden)
|
||||
} catch {
|
||||
folders = []
|
||||
folderError = error.localizedDescription
|
||||
@@ -107,19 +172,48 @@ struct SharePickerView: View {
|
||||
centered {
|
||||
Text(message).multilineTextAlignment(.center).foregroundColor(.secondary).padding()
|
||||
}
|
||||
case .chooseAccount:
|
||||
accountList
|
||||
case .picking:
|
||||
folderList
|
||||
}
|
||||
}
|
||||
|
||||
private var accountList: some View {
|
||||
List {
|
||||
Section(
|
||||
header: Text("Upload \(model.fileCountText) to which account?"),
|
||||
footer: model.notice.map { Text($0) }
|
||||
) {
|
||||
ForEach(model.accounts) { account in
|
||||
Button {
|
||||
Task { await model.select(account) }
|
||||
} label: {
|
||||
HStack {
|
||||
Label(account.displayName, systemImage: "person.crop.circle")
|
||||
Spacer()
|
||||
if account.id == model.activeAccountId {
|
||||
Text("Active").font(.caption).foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
|
||||
private var folderList: some View {
|
||||
List {
|
||||
Section(header: Text(model.account?.displayName ?? "")) {
|
||||
Section(
|
||||
header: accountHeader,
|
||||
footer: model.notice.map { Text($0) }
|
||||
) {
|
||||
if model.path != "/" {
|
||||
Button {
|
||||
Task { await model.openParent() }
|
||||
} label: {
|
||||
Label("Up to \((model.path as NSString).deletingLastPathComponent == "/" ? "All files" : ((model.path as NSString).deletingLastPathComponent as NSString).lastPathComponent)", systemImage: "arrow.turn.left.up")
|
||||
Label("Up to \(parentName)", systemImage: "arrow.turn.left.up")
|
||||
}
|
||||
}
|
||||
if model.isLoadingFolders {
|
||||
@@ -142,6 +236,22 @@ struct SharePickerView: View {
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
|
||||
private var accountHeader: some View {
|
||||
HStack {
|
||||
Text(model.selected?.displayName ?? "")
|
||||
Spacer()
|
||||
if model.canChangeAccount {
|
||||
Button("Change account") { model.showAccounts() }
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var parentName: String {
|
||||
let parent = (model.path as NSString).deletingLastPathComponent
|
||||
return parent.isEmpty || parent == "/" ? "All files" : (parent as NSString).lastPathComponent
|
||||
}
|
||||
|
||||
private var footer: some View {
|
||||
VStack(spacing: 8) {
|
||||
if model.stage == .picking {
|
||||
@@ -154,7 +264,7 @@ struct SharePickerView: View {
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(model.isLoadingFolders)
|
||||
}
|
||||
if model.stage == .picking || model.stage == .noAccount {
|
||||
if model.stage == .picking || model.stage == .chooseAccount || model.stage == .noAccount {
|
||||
Button("Choose a folder later in Noo", action: model.onSaveForLater)
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user