Add GET_CONTENT picker support, fix account/login bugs, restyle selection UI
Build APK / build (push) Successful in 7m40s
Build APK / build (push) Successful in 7m40s
- Let Noo be picked as a source (Files + Photos) by other apps' native file/photo pickers via Android's GET_CONTENT intent, mirroring the existing "Share to Noo" plumbing: MainActivity.kt/PickIntentService/ PickRequest, ServerProvider.pickRequest/confirmPick/cancelPick, and pick-mode tap handling in FilesView/PhotosView. While picking, the bottom nav and MoreTabsButton are restricted to Files/Photos only. - Fix two account bugs: testConnection() failures no longer delete a still-valid stored app password (only an actual 401 does), and switchAccount() now retries a nominally-active-but-logged-out account instead of no-op'ing - both were silently locking users out of "Continue as" with no way back in except removing and re-adding the account (now exposed via login_view.dart's inline remove button). - Fix the login WebView: clear cookies before every load so "Add Account" can't silently reuse an existing session, and push it as a non-animated route to avoid a known WebView-as-PlatformView black-screen- on-pop issue under Impeller. - Redesign the Files/Photos selection toolbar to take over the top app bar entirely (SyncedHeaderScaffold.selectionBar) instead of sitting as a second bar under the controls row, with a horizontally-scrollable, uniformly-colored actions row and the same one-shot scroll-hint nudge the controls row uses. - Redesign ShareUploadView to share the same SyncedHeaderScaffold chrome as every tab, with the uploading-file summary (marqueed via the new shared MarqueeTitle widget) and upload button grouped into one rounded/elevated bottom sheet. - Restyle ShareSheet to open on the same DetailsHeader every other per-item sheet uses, and fix a couple of small design-language drifts (hardcoded text style, a non-rounded icon). - Settings' account card: icon buttons with tooltips instead of text buttons for refresh/logout/remove, divider removed.
This commit is contained in:
@@ -47,6 +47,60 @@ class _LoginViewState extends State<LoginView> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Resumes a saved account with one tap (see [ServerProvider.switchAccount]).
|
||||
/// That can fail silently from the account's own perspective - most often
|
||||
/// a stored app password that no longer works (revoked server-side, or
|
||||
/// left over from before a since-fixed bug that deleted it too eagerly on
|
||||
/// a plain network hiccup) - so this surfaces that as a SnackBar with a
|
||||
/// silently failing every time - a delete button on the row itself (see
|
||||
/// [_SavedAccountRow]) is the way out.
|
||||
Future<void> _continueAsAccount(SavedAccount account) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final success = await provider.switchAccount(account.id);
|
||||
if (success || !mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Couldn't sign in as ${account.username}"),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Mirrors AccountView's `_AccountsCard._confirmRemove` - same
|
||||
/// destructive-action gate, just reached from the login screen instead of
|
||||
/// Settings.
|
||||
Future<void> _confirmRemoveAccount(SavedAccount account) async {
|
||||
final host = Uri.tryParse(account.serverUrl)?.host ?? account.serverUrl;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Remove Account'),
|
||||
content: Text(
|
||||
'Remove ${account.username} ($host)? You can add it again later.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(dialogContext).colorScheme.error,
|
||||
),
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: const Text('Remove'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
await context.read<ServerProvider>().removeAccount(account.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -81,9 +135,21 @@ class _LoginViewState extends State<LoginView> {
|
||||
final url = provider.pendingLoginUrl!;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!mounted) return;
|
||||
await Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => LoginWebViewView(url: url)));
|
||||
// A plain MaterialPageRoute's animated slide/fade transition can
|
||||
// leave this screen fully black after popping back off of it - a
|
||||
// known Android WebView-as-PlatformView compositing issue with the
|
||||
// Impeller renderer (the platform view's surface isn't always torn
|
||||
// down in sync with an animated route transition). An instant,
|
||||
// non-animated route sidesteps it entirely; the close (X) button
|
||||
// already reads like a immediate action, not something that needs
|
||||
// a slide, so no UX is lost.
|
||||
await Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (_, _, _) => LoginWebViewView(url: url),
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
),
|
||||
);
|
||||
_webViewPushed = false;
|
||||
});
|
||||
}
|
||||
@@ -110,7 +176,8 @@ class _LoginViewState extends State<LoginView> {
|
||||
provider.accounts.isNotEmpty) ...[
|
||||
_SavedAccountsSection(
|
||||
accounts: provider.accounts,
|
||||
onSelect: (account) => provider.switchAccount(account.id),
|
||||
onSelect: (account) => _continueAsAccount(account),
|
||||
onRemove: (account) => _confirmRemoveAccount(account),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
Row(
|
||||
@@ -329,8 +396,13 @@ class _ServerForm extends StatelessWidget {
|
||||
class _SavedAccountsSection extends StatelessWidget {
|
||||
final List<SavedAccount> accounts;
|
||||
final ValueChanged<SavedAccount> onSelect;
|
||||
final ValueChanged<SavedAccount> onRemove;
|
||||
|
||||
const _SavedAccountsSection({required this.accounts, required this.onSelect});
|
||||
const _SavedAccountsSection({
|
||||
required this.accounts,
|
||||
required this.onSelect,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -358,6 +430,7 @@ class _SavedAccountsSection extends StatelessWidget {
|
||||
_SavedAccountRow(
|
||||
account: account,
|
||||
onTap: () => onSelect(account),
|
||||
onRemove: () => onRemove(account),
|
||||
),
|
||||
if (account != accounts.last)
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
@@ -373,8 +446,13 @@ class _SavedAccountsSection extends StatelessWidget {
|
||||
class _SavedAccountRow extends StatelessWidget {
|
||||
final SavedAccount account;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const _SavedAccountRow({required this.account, required this.onTap});
|
||||
const _SavedAccountRow({
|
||||
required this.account,
|
||||
required this.onTap,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -397,7 +475,12 @@ class _SavedAccountRow extends StatelessWidget {
|
||||
),
|
||||
title: Text(account.username),
|
||||
subtitle: Text(host),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
tooltip: 'Remove account',
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: onRemove,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user