Replace receive_sharing_intent; add background upload service; fix add-account login page

Sharing a large file to Noo used to black-screen and silently kick back
to the home screen: receive_sharing_intent copies the entire shared file
into the cache dir synchronously on the main thread before Flutter even
renders, which Android's watchdog eventually kills. Replaced with
hand-rolled handling in MainActivity.kt that only ever reads cheap Uri
metadata up front (ShareIntentService), so the destination picker always
appears instantly regardless of file size.

The destination picker (ShareUploadView) now mirrors the Files tab's own
controls/filters/listing instead of a bare folder list, and the actual
prepare+upload is handed off to ShareUploadService.kt, a real Android
foreground service with a single cancellable progress notification for
the whole batch - this survives the app being closed right after the
user confirms a destination, the same guarantee a file-manager app's own
upload notification gives.

Also fixes the "add another account" login page: it used to open in
url_launcher's bare inAppWebView, which has no close button and can hide
the status bar. LoginWebViewView is a real screen this app owns instead
(package:webview_flutter), with a normal AppBar/close button/safe area,
and can now auto-close itself on successful login.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 23:33:55 -04:00
co-authored by Claude Sonnet 5
parent 453aff983a
commit bcc18a7060
13 changed files with 1164 additions and 181 deletions
+92
View File
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../providers/server_provider.dart';
/// The "add another account" Login Flow v2 page, rendered in Flutter's own
/// WebView rather than url_launcher's `LaunchMode.inAppWebView` - that mode
/// hosts a bare native WebView Activity with no chrome of its own: no close
/// button, and on at least some devices it draws edge-to-edge and hides the
/// status bar with no way back short of the OS back gesture. A normal
/// Scaffold/AppBar here gets the status bar/safe-area handling and a close
/// button for free, exactly like every other screen in the app.
///
/// See ServerProvider.startLoginFlow's doc comment for why the add-account
/// flow uses an embedded WebView (isolated cookies) instead of a Chrome
/// Custom Tab at all.
class LoginWebViewView extends StatefulWidget {
final Uri url;
const LoginWebViewView({super.key, required this.url});
@override
State<LoginWebViewView> createState() => _LoginWebViewViewState();
}
class _LoginWebViewViewState extends State<LoginWebViewView> {
late final WebViewController _controller;
bool _pageLoading = true;
bool _popped = false;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (_) {
if (mounted) setState(() => _pageLoading = true);
},
onPageFinished: (_) {
if (mounted) setState(() => _pageLoading = false);
},
),
)
..loadRequest(widget.url);
}
void _cancel() {
context.read<ServerProvider>().cancelLoginFlow();
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
final provider = context.watch<ServerProvider>();
// The login flow finished (success or error) or was cancelled
// elsewhere - close this screen automatically rather than leaving it
// sitting open over a flow that's no longer awaiting the browser.
if (!_popped &&
provider.loginFlowStatus != LoginFlowStatus.awaitingBrowser) {
_popped = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) Navigator.of(context).pop();
});
}
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) _cancel();
},
child: Scaffold(
appBar: AppBar(
title: const Text('Add Account'),
leading: IconButton(
icon: const Icon(Icons.close_rounded),
tooltip: 'Cancel',
onPressed: _cancel,
),
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_pageLoading) const LinearProgressIndicator(),
],
),
),
);
}
}