diff --git a/.claude/context/server.md b/.claude/context/server.md
index 74f0e99..df64637 100644
--- a/.claude/context/server.md
+++ b/.claude/context/server.md
@@ -908,19 +908,32 @@ Reminders/Notes - the destination is picked *inside* the sheet:
`initState`) republishes - through the native `share_account` channel's
`setAccounts` - when an account becomes ready or when its change
signature moves: the account list/active account, the three lock
- settings, or the active account's hidden-files filter; sign-out calls
+ settings, or the active account's hidden-files filter / storage scope;
+ sign-out calls
`clearAccount`. It publishes every saved account that has a stored
password and isn't signed out, each with its **own Files hidden-files
filter** (`FilesController.savedHiddenFilter`).
- - **Hidden folders** follow that app setting - there is no share-sheet
- setting of their own. `hide` (default) drops dot-folders, `only` lists
- just them, `include` lists everything; a folder is hidden when it *or
- any ancestor* starts with a dot (`HiddenFilter`, same rule as the app).
- When the filter isn't `hide` and the app's "lock hidden files" is on,
- the sheet asks for Face ID/passcode first (`DeviceAuth`, the
+ - **Toggles**: the folder list has two menu chips, **Hidden** (hide / only
+ hidden / all) and **Storage** (cloud only / only external / all +
+ external), each starting at the chosen account's own app setting
+ (`hiddenFilter`, `storageScope` published per account) and changeable for
+ this share only - nothing is written back. The list is fetched once per
+ folder (`DavClient.listFolders` returns everything, hidden and external
+ included, with `nc:mount-type` requested) and filtered in memory
+ (`ShareModel.visibleFolders`), so flipping a toggle just returns to the
+ top. Hidden uses the app's rule (`HiddenFilter`): a folder is hidden when
+ it *or any ancestor* starts with a dot. External storage: only a mount's
+ *root* has `mount-type=external`, so `ShareModel` remembers the roots it
+ has seen and treats anything under one as external too (the app's own
+ filter doesn't, which empties its list inside a mount). "All" lists
+ external folders in their own "External storage" group.
+ - **Hidden unlock**: turning hidden folders *on* (from `hide`), or opening
+ the sheet with the account's filter already not `hide`, asks for
+ Face ID/passcode when "lock hidden files" is on (`DeviceAuth`, the
`.deviceOwnerAuthentication` policy - biometrics with passcode fallback,
- like `local_auth` with `biometricOnly: false`); cancelling falls back to
- hiding them, with a note.
+ like `local_auth` with `biometricOnly: false`); cancelling keeps them
+ hidden, with a note. Switching `only` <-> `all`, or back to `hide`, never
+ asks.
- **Account switching**: choosing any account other than the app's
active one asks for the same unlock when "lock account switching" is on. One successful unlock covers the rest of that sheet.
- Opening the sheet itself is not gated - only these two actions are
diff --git a/ios/Runner/Native/NativeServices.swift b/ios/Runner/Native/NativeServices.swift
index 954d235..8a770cd 100644
--- a/ios/Runner/Native/NativeServices.swift
+++ b/ios/Runner/Native/NativeServices.swift
@@ -95,6 +95,7 @@ enum NativeServices {
let accounts = try? JSONDecoder().decode(SharedAccounts.self, from: data)
else { throw TransferError(message: "Bad accounts payload.") }
try SharedAccountStore.save(accounts)
+ NSLog("[ShareAccounts] saved %d account(s), active=%@", accounts.accounts.count, accounts.activeId ?? "none")
}
case "clearAccount":
SharedAccountStore.clear()
diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift
index f390cdd..f324061 100644
--- a/ios/RunnerTests/RunnerTests.swift
+++ b/ios/RunnerTests/RunnerTests.swift
@@ -247,4 +247,44 @@ class RunnerTests: XCTestCase {
XCTAssertEqual(TransferNotifications.files(1), "1 file")
XCTAssertEqual(TransferNotifications.files(3), "3 files")
}
+
+ // MARK: - External storage + tolerant decoding
+
+ func testFoldersFlagExternalMountsAndStorageFilterAppliesToThem() {
+ let xml = Data(
+ """
+
+ /remote.php/dav/files/a/
+
+ /remote.php/dav/files/a/NAS/
+ external
+
+ /remote.php/dav/files/a/Docs/
+
+ /remote.php/dav/files/a/Shared/
+ shared
+
+
+ """.utf8)
+ let folders = DavFolderParser.folders(from: xml, excluding: "/")
+ XCTAssertEqual(folders.map(\.name), ["Docs", "NAS", "Shared"])
+ XCTAssertEqual(folders.map(\.isExternal), [false, true, false], "only mount-type=external counts")
+
+ XCTAssertTrue(StorageFilter.cloud.shows(isExternal: false))
+ XCTAssertFalse(StorageFilter.cloud.shows(isExternal: true))
+ XCTAssertTrue(StorageFilter.external.shows(isExternal: true))
+ XCTAssertFalse(StorageFilter.external.shows(isExternal: false))
+ XCTAssertTrue(StorageFilter.all.shows(isExternal: true) && StorageFilter.all.shows(isExternal: false))
+ XCTAssertEqual(StorageFilter(raw: "bogus"), .cloud)
+ }
+
+ func testSharedAccountDecodesDataWrittenBeforeNewFieldsExisted() throws {
+ let old = Data(
+ """
+ {"id":"a","serverUrl":"https://x","username":"u","authHeader":"Basic x","displayName":"u@x"}
+ """.utf8)
+ let decoded = try JSONDecoder().decode(SharedAccount.self, from: old)
+ XCTAssertEqual(decoded.hiddenFilter, "hide")
+ XCTAssertEqual(decoded.storageScope, "cloud")
+ }
}
diff --git a/ios/ShareExtension/SharePickerView.swift b/ios/ShareExtension/SharePickerView.swift
index 8ab7de6..9b963da 100644
--- a/ios/ShareExtension/SharePickerView.swift
+++ b/ios/ShareExtension/SharePickerView.swift
@@ -15,7 +15,6 @@ final class ShareModel: ObservableObject {
@Published var stage: Stage = .preparing
@Published var items: [SharedItem] = []
@Published var path = "/"
- @Published var folders: [DavFolder] = []
@Published var isLoadingFolders = false
@Published var folderError: String?
/// A short explanation shown under the list - e.g. hidden folders stayed
@@ -23,11 +22,21 @@ final class ShareModel: ObservableObject {
@Published var notice: String?
@Published private(set) var selected: SharedAccount?
+ /// The sheet's two toggles. They start at the app's own setting for the
+ /// chosen account and can be changed for this share only.
+ @Published private(set) var hiddenFilter: HiddenFilter = .hide
+ @Published private(set) var storageFilter: StorageFilter = .cloud
+
+ /// Every child folder of [path], unfiltered; the toggles filter it live.
+ @Published private var allFolders: [DavFolder] = []
+ /// Mount points of external storage seen so far. Only the mount's root is
+ /// flagged by the server, so anything under one of these is external too.
+ private var externalRoots: Set = []
+
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 = {}
@@ -50,6 +59,23 @@ final class ShareModel: ObservableObject {
items.count == 1 ? "1 file" : "\(items.count) files"
}
+ // MARK: - Folders, filtered by the toggles
+
+ private func isExternal(_ folder: DavFolder) -> Bool {
+ folder.isExternal
+ || externalRoots.contains { folder.path == $0 || folder.path.hasPrefix($0 + "/") }
+ }
+
+ private var visibleFolders: [DavFolder] {
+ allFolders.filter { hiddenFilter.shows(path: $0.path) && storageFilter.shows(isExternal: isExternal($0)) }
+ }
+
+ /// Internal folders - and, for "All", the external ones are listed apart.
+ var internalFolders: [DavFolder] { visibleFolders.filter { !isExternal($0) } }
+ var externalFolders: [DavFolder] { visibleFolders.filter { isExternal($0) } }
+
+ // MARK: - Accounts
+
/// With several accounts the list comes first; with one it goes straight
/// to its folders.
func start() async {
@@ -57,6 +83,7 @@ final class ShareModel: ObservableObject {
stage = .noAccount
return
}
+ NSLog("[ShareExtension] %d account(s), active=%@", shared.accounts.count, shared.active?.id ?? "none")
if shared.accounts.count > 1 {
stage = .chooseAccount
} else {
@@ -71,8 +98,8 @@ final class ShareModel: ObservableObject {
/// 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.
+ /// The toggles start at that account's own app settings; hidden folders
+ /// are behind the app's "lock hidden files" unlock.
func select(_ account: SharedAccount) async {
guard let shared else { return }
notice = nil
@@ -83,11 +110,14 @@ final class ShareModel: ObservableObject {
return
}
}
+ NSLog("[ShareExtension] selected %@ (hidden=%@ storage=%@)", account.id, account.hiddenFilter, account.storageScope)
selected = account
- hidden = HiddenFilter(raw: account.hiddenFilter)
- if hidden != .hide, shared.needsUnlockForHidden {
+ externalRoots = []
+ storageFilter = StorageFilter(raw: account.storageScope)
+ hiddenFilter = HiddenFilter(raw: account.hiddenFilter)
+ if hiddenFilter != .hide, shared.needsUnlockForHidden {
if await !unlock("Unlock to show hidden folders") {
- hidden = .hide
+ hiddenFilter = .hide
notice = "Hidden folders are locked, so they're not shown."
}
}
@@ -95,6 +125,31 @@ final class ShareModel: ObservableObject {
await open("/")
}
+ // MARK: - Toggles
+
+ /// Turning hidden folders *on* is what the app locks; going back to
+ /// hiding them never exposes anything, and neither does switching
+ /// between the two revealing modes.
+ func setHiddenFilter(_ filter: HiddenFilter) async {
+ guard filter != hiddenFilter else { return }
+ if hiddenFilter == .hide, shared?.needsUnlockForHidden == true {
+ guard await unlock("Unlock to show hidden folders") else {
+ notice = "Hidden folders are locked, so they're not shown."
+ return
+ }
+ }
+ notice = nil
+ hiddenFilter = filter
+ await open("/")
+ }
+
+ func setStorageFilter(_ filter: StorageFilter) async {
+ guard filter != storageFilter else { return }
+ notice = nil
+ storageFilter = filter
+ await open("/")
+ }
+
private func unlock(_ reason: String) async -> Bool {
if unlocked { return true }
let ok = await DeviceAuth.authenticate(reason: reason)
@@ -102,6 +157,10 @@ final class ShareModel: ObservableObject {
return ok
}
+ // MARK: - Browsing
+
+ /// A toggle can leave you inside a folder it now hides, so changing one
+ /// always returns to the top - same as [open] with "/".
func open(_ path: String) async {
guard let account = selected else { return }
self.path = path
@@ -109,9 +168,10 @@ final class ShareModel: ObservableObject {
folderError = nil
defer { isLoadingFolders = false }
do {
- folders = try await DavClient.listFolders(account: account, path: path, hidden: hidden)
+ allFolders = try await DavClient.listFolders(account: account, path: path)
+ externalRoots.formUnion(allFolders.filter(\.isExternal).map(\.path))
} catch {
- folders = []
+ allFolders = []
folderError = error.localizedDescription
}
}
@@ -124,6 +184,7 @@ final class ShareModel: ObservableObject {
struct SharePickerView: View {
@ObservedObject var model: ShareModel
+ @State private var externalExpanded = true
var body: some View {
VStack(spacing: 0) {
@@ -203,12 +264,12 @@ struct SharePickerView: View {
.listStyle(.insetGrouped)
}
+ // MARK: Folders
+
private var folderList: some View {
List {
- Section(
- header: accountHeader,
- footer: model.notice.map { Text($0) }
- ) {
+ Section(header: accountHeader, footer: model.notice.map { Text($0) }) {
+ filterRow
if model.path != "/" {
Button {
Task { await model.openParent() }
@@ -220,15 +281,19 @@ struct SharePickerView: View {
HStack { Spacer(); ProgressView(); Spacer() }
} else if let error = model.folderError {
Text(error).foregroundColor(.secondary)
- } else if model.folders.isEmpty {
+ } else if model.internalFolders.isEmpty && model.externalFolders.isEmpty {
Text("No folders here").foregroundColor(.secondary)
} else {
- ForEach(model.folders, id: \.path) { folder in
- Button {
- Task { await model.open(folder.path) }
- } label: {
- Label(folder.name, systemImage: "folder")
- }
+ ForEach(model.internalFolders, id: \.path) { folderRow($0) }
+ }
+ }
+ // "All" keeps external storage in its own group, like the app's list.
+ if !model.isLoadingFolders, model.folderError == nil, !model.externalFolders.isEmpty {
+ Section {
+ DisclosureGroup(isExpanded: $externalExpanded) {
+ ForEach(model.externalFolders, id: \.path) { folderRow($0) }
+ } label: {
+ Label("External storage (\(model.externalFolders.count))", systemImage: "externaldrive")
}
}
}
@@ -236,6 +301,73 @@ struct SharePickerView: View {
.listStyle(.insetGrouped)
}
+ private func folderRow(_ folder: DavFolder) -> some View {
+ Button {
+ Task { await model.open(folder.path) }
+ } label: {
+ Label(folder.name, systemImage: folder.isExternal ? "externaldrive" : "folder")
+ }
+ }
+
+ /// The two toggles, as menus - Hidden folders and External storage each
+ /// start at the app's setting and can be changed for this share.
+ private var filterRow: some View {
+ HStack(spacing: 8) {
+ Menu {
+ Picker("Hidden folders", selection: hiddenBinding) {
+ Text("Hide hidden folders").tag(HiddenFilter.hide)
+ Text("Only hidden folders").tag(HiddenFilter.only)
+ Text("Show all").tag(HiddenFilter.include)
+ }
+ } label: {
+ chip("eye", "Hidden", hiddenTitle)
+ }
+ Menu {
+ Picker("External storage", selection: storageBinding) {
+ Text("Cloud only").tag(StorageFilter.cloud)
+ Text("Only external storage").tag(StorageFilter.external)
+ Text("All + external storage").tag(StorageFilter.all)
+ }
+ } label: {
+ chip("externaldrive", "Storage", storageTitle)
+ }
+ Spacer()
+ }
+ .buttonStyle(.borderless)
+ }
+
+ private func chip(_ icon: String, _ name: String, _ value: String) -> some View {
+ Label("\(name): \(value)", systemImage: icon)
+ .font(.footnote.weight(.medium))
+ .padding(.horizontal, 10)
+ .padding(.vertical, 6)
+ .background(Capsule().fill(Color(.secondarySystemFill)))
+ }
+
+ private var hiddenBinding: Binding {
+ Binding(get: { model.hiddenFilter }, set: { value in Task { await model.setHiddenFilter(value) } })
+ }
+
+ private var storageBinding: Binding {
+ Binding(get: { model.storageFilter }, set: { value in Task { await model.setStorageFilter(value) } })
+ }
+
+ private var hiddenTitle: String {
+ switch model.hiddenFilter {
+ case .hide: return "Hide"
+ case .only: return "Only"
+ case .include: return "All"
+ }
+ }
+
+ private var storageTitle: String {
+ switch model.storageFilter {
+ case .cloud: return "Cloud"
+ case .external: return "External"
+ case .all: return "All"
+ }
+ }
+
private var accountHeader: some View {
HStack {
Text(model.selected?.displayName ?? "")
diff --git a/ios/Shared/DavFolders.swift b/ios/Shared/DavFolders.swift
index a855b6e..bb502c6 100644
--- a/ios/Shared/DavFolders.swift
+++ b/ios/Shared/DavFolders.swift
@@ -2,9 +2,31 @@ import Foundation
/// A folder in the account's WebDAV tree. [path] is relative to the user's
/// root with a leading slash ("/Documents/Tax Forms"); "/" is the root.
+/// [isExternal] is true for an external-storage mount point (Nextcloud's
+/// `nc:mount-type` = `external`) - only the mount's root carries it, its
+/// children don't, so callers track mount roots to know what's inside one.
struct DavFolder: Equatable {
let name: String
let path: String
+ var isExternal = false
+}
+
+/// The app's Files storage scope (`StorageScope` in Dart): internal ("cloud")
+/// folders only (the default), only external-storage ones, or both.
+enum StorageFilter: String {
+ case cloud, external, all
+
+ init(raw: String) {
+ self = StorageFilter(rawValue: raw) ?? .cloud
+ }
+
+ func shows(isExternal: Bool) -> Bool {
+ switch self {
+ case .cloud: return !isExternal
+ case .external: return isExternal
+ case .all: return true
+ }
+ }
}
/// The app's Files "hidden files" filter (`HiddenFilesFilter` in Dart):
@@ -37,6 +59,7 @@ enum DavFolderParser {
private struct Response {
var href = ""
var isCollection = false
+ var mountType = ""
}
private final class Delegate: NSObject, XMLParserDelegate {
@@ -54,7 +77,7 @@ enum DavFolderParser {
) {
switch local(elementName) {
case "response": current = Response()
- case "href": text = ""
+ case "href", "mount-type": text = ""
case "collection": current?.isCollection = true
default: break
}
@@ -70,6 +93,7 @@ enum DavFolderParser {
) {
switch local(elementName) {
case "href": current?.href = text.trimmingCharacters(in: .whitespacesAndNewlines)
+ case "mount-type": current?.mountType = text.trimmingCharacters(in: .whitespacesAndNewlines)
case "response":
if let current { responses.append(current) }
current = nil
@@ -109,7 +133,7 @@ enum DavFolderParser {
guard let path = relativePath(fromHref: response.href), path != current else { return nil }
let name = (path as NSString).lastPathComponent
guard !name.isEmpty, hidden.shows(path: path) else { return nil }
- return DavFolder(name: name, path: path)
+ return DavFolder(name: name, path: path, isExternal: response.mountType == "external")
}
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}
@@ -120,10 +144,12 @@ enum DavFolderParser {
enum DavClient {
private static let body = """
-
+
"""
- static func listFolders(account: SharedAccount, path: String, hidden: HiddenFilter) async throws -> [DavFolder] {
+ /// Every child folder, hidden and external ones included - the sheet's
+ /// toggles filter that list in memory, so changing one doesn't refetch.
+ static func listFolders(account: SharedAccount, path: String) async throws -> [DavFolder] {
guard let url = WebDAV.fileURL(serverUrl: account.serverUrl, username: account.username, remotePath: path)
else { throw TransferError(message: "Invalid server address.") }
var request = URLRequest(url: url)
@@ -139,6 +165,6 @@ enum DavClient {
guard status == 207 else {
throw TransferError(message: status == 401 ? "Signed out - open Noo to sign in again." : "Server returned \(status).")
}
- return DavFolderParser.folders(from: data, excluding: path, hidden: hidden)
+ return DavFolderParser.folders(from: data, excluding: path, hidden: .include)
}
}
diff --git a/ios/Shared/SharedAccount.swift b/ios/Shared/SharedAccount.swift
index 2423ff8..aa9ff97 100644
--- a/ios/Shared/SharedAccount.swift
+++ b/ios/Shared/SharedAccount.swift
@@ -15,6 +15,37 @@ struct SharedAccount: Codable, Equatable, Identifiable {
/// default), `only` or `include` (see `HiddenFilter`). The share sheet
/// follows it instead of having a setting of its own.
let hiddenFilter: String
+ /// The app's storage scope for this account - `cloud` (the default),
+ /// `external` or `all` (see `StorageFilter`).
+ let storageScope: String
+
+ init(
+ id: String, serverUrl: String, username: String, authHeader: String, displayName: String,
+ hiddenFilter: String = "hide", storageScope: String = "cloud"
+ ) {
+ self.id = id
+ self.serverUrl = serverUrl
+ self.username = username
+ self.authHeader = authHeader
+ self.displayName = displayName
+ self.hiddenFilter = hiddenFilter
+ self.storageScope = storageScope
+ }
+
+ /// Tolerates data written before a field existed instead of failing the
+ /// whole decode (which would look like "signed out" to the extension).
+ init(from decoder: Decoder) throws {
+ let c = try decoder.container(keyedBy: CodingKeys.self)
+ self.init(
+ id: try c.decode(String.self, forKey: .id),
+ serverUrl: try c.decode(String.self, forKey: .serverUrl),
+ username: try c.decode(String.self, forKey: .username),
+ authHeader: try c.decode(String.self, forKey: .authHeader),
+ displayName: try c.decode(String.self, forKey: .displayName),
+ hiddenFilter: try c.decodeIfPresent(String.self, forKey: .hiddenFilter) ?? "hide",
+ storageScope: try c.decodeIfPresent(String.self, forKey: .storageScope) ?? "cloud"
+ )
+ }
}
/// Everything the app publishes for the extension: every account that can
diff --git a/lib/providers/files_controller.dart b/lib/providers/files_controller.dart
index 552ae90..486a1e1 100644
--- a/lib/providers/files_controller.dart
+++ b/lib/providers/files_controller.dart
@@ -677,6 +677,19 @@ class FilesController extends ChangeNotifier
);
}
+ /// The storage scope saved for [accountId] (see [savedHiddenFilter]).
+ static StorageScope savedStorageScope(
+ SharedPreferences prefs,
+ String Function(String accountId, String baseKey) accountPrefKey,
+ String accountId,
+ ) {
+ final name = prefs.getString(accountPrefKey(accountId, _prefStorageScope));
+ return StorageScope.values.firstWhere(
+ (s) => s.name == name,
+ orElse: () => StorageScope.cloud,
+ );
+ }
+
/// The hidden-files filter saved for [accountId] - not just the active
/// account's, which is all [hiddenFilter] holds. For iOS's Share Extension,
/// which follows each account's own setting.
diff --git a/lib/services/share_account_service.dart b/lib/services/share_account_service.dart
index 6b39003..fccd277 100644
--- a/lib/services/share_account_service.dart
+++ b/lib/services/share_account_service.dart
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
+import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '../providers/files_controller.dart';
import '../providers/session_controller.dart';
@@ -16,12 +17,16 @@ class ShareAccountEntry {
/// The account's Files "hidden files" filter: `hide`, `only` or `include`.
final String hiddenFilter;
+ /// The account's Files storage scope: `cloud`, `external` or `all`.
+ final String storageScope;
+
const ShareAccountEntry({
required this.id,
required this.serverUrl,
required this.username,
required this.password,
required this.hiddenFilter,
+ required this.storageScope,
});
}
@@ -57,6 +62,7 @@ class ShareAccountService {
'displayName':
'${a.username}@${Uri.tryParse(a.serverUrl)?.host ?? a.serverUrl}',
'hiddenFilter': a.hiddenFilter,
+ 'storageScope': a.storageScope,
},
],
'activeId': activeId,
@@ -68,8 +74,8 @@ class ShareAccountService {
/// Publishes every saved account that can actually upload (has a stored
/// password and isn't signed out), with the active one's hidden-files
- /// filter taken live from [files] - its saved pref is written
- /// asynchronously, so it can lag a change that just happened.
+ /// filter and storage scope taken live from [files] - the saved prefs are
+ /// written asynchronously, so they can lag a change that just happened.
static Future publish(
SessionController session,
FilesController files,
@@ -82,16 +88,32 @@ class ShareAccountService {
final signedOut = store.loadSignedOut(prefs);
final entries = [];
for (final account in session.accounts) {
- if (signedOut.contains(account.id)) continue;
+ if (signedOut.contains(account.id)) {
+ debugPrint('[ShareAccounts] skipping ${account.id}: signed out');
+ continue;
+ }
final password = await store.readPassword(account.id);
- if (password == null) continue;
- final filter = account.id == activeId
+ if (password == null) {
+ debugPrint(
+ '[ShareAccounts] skipping ${account.id}: no stored password',
+ );
+ continue;
+ }
+ final isActive = account.id == activeId;
+ final filter = isActive
? files.hiddenFilter
: FilesController.savedHiddenFilter(
prefs,
store.accountPrefKey,
account.id,
);
+ final scope = isActive
+ ? files.storageScope
+ : FilesController.savedStorageScope(
+ prefs,
+ store.accountPrefKey,
+ account.id,
+ );
entries.add(
ShareAccountEntry(
id: account.id,
@@ -99,9 +121,15 @@ class ShareAccountService {
username: account.username,
password: password,
hiddenFilter: filter.name,
+ storageScope: scope.name,
),
);
}
+ debugPrint(
+ '[ShareAccounts] publishing ${entries.length} of '
+ '${session.accounts.length} saved accounts, active=$activeId '
+ '(${signedOut.length} signed out)',
+ );
if (entries.isEmpty) return clear();
await invokeIfAvailable(_channel, 'setAccounts', {
@@ -152,6 +180,7 @@ class ShareAccountSync {
session.lockAccountSwitching,
session.lockHiddenFiles,
files.hiddenFilter.name,
+ files.storageScope.name,
].join('|');
void _onChange() {
diff --git a/test/services/share_account_service_test.dart b/test/services/share_account_service_test.dart
index 813306e..1f3c70c 100644
--- a/test/services/share_account_service_test.dart
+++ b/test/services/share_account_service_test.dart
@@ -11,6 +11,7 @@ void main() {
username: 'alice',
password: 's3cret',
hiddenFilter: 'include',
+ storageScope: 'all',
);
const bob = ShareAccountEntry(
id: 'nc_home_lan__bob',
@@ -18,6 +19,7 @@ void main() {
username: 'bob',
password: 'pw',
hiddenFilter: 'hide',
+ storageScope: 'cloud',
);
Map build({
@@ -52,10 +54,12 @@ void main() {
);
expect(accounts[0]['displayName'], 'alice@cloud.example.com');
expect(accounts[0]['hiddenFilter'], 'include');
+ expect(accounts[0]['storageScope'], 'all');
// The display name uses the host only - not the port or sub-path.
expect(accounts[1]['displayName'], 'bob@nc.home.lan');
expect(accounts[1]['serverUrl'], 'https://nc.home.lan:8443/nextcloud');
expect(accounts[1]['hiddenFilter'], 'hide');
+ expect(accounts[1]['storageScope'], 'cloud');
});
test('the active account and the lock settings are passed through', () {