This commit is contained in:
2026-04-09 15:47:59 +01:00
parent 2033ad79e2
commit 558a28d12b
22 changed files with 1275 additions and 271 deletions
+19
View File
@@ -0,0 +1,19 @@
import 'dart:io';
import 'package:http/io_client.dart';
import 'package:nextcloud/nextcloud.dart';
extension NextcloudClientExtension on NextcloudClient {
//
static final userAgent = 'NextCloud App'
'(${Platform.operatingSystem}) ';
// 'Dart/${Platform.version.split(' ').first}';
//
static IOClient newHttpClient() => IOClient(
HttpClient()..userAgent = userAgent,
);
}
+104
View File
@@ -0,0 +1,104 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:nextcloud/core.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:nextcloudapp/app/extension.dart';
import 'package:url_launcher/url_launcher.dart';
class SaberLoginFlow {
SaberLoginFlow.start({
required this.serverUrl,
}) {
// NcHttpOverrides.tempAcceptBadCertificateFrom(serverUrl);
unawaited(_run());
}
final Uri serverUrl;
final log = Logger('SaberLoginFlow');
Timer? _pollTimer;
final completer = Completer<LoginFlowV2Credentials>();
late final future = completer.future;
LoginFlowV2? init;
Future<void> openLoginUrl() async {
final init = this.init;
if (init == null) return;
if (Platform.isMacOS || Platform.isIOS || Platform.isAndroid) {
// Use FlutterWebAuth2 which returns to Saber when authenticated
log.info('Opening login link in-app: ${init.login}');
await FlutterWebAuth2.authenticate(
url: init.login,
callbackUrlScheme: 'nc',
);
} else {
// Use url_launcher to open the link in the default browser
log.info('Opening login link in browser: ${init.login}');
final loginLink = Uri.parse(init.login);
await launchUrl(loginLink);
}
}
Future<void> _run() async {
_catchHttpError(() async {
final client = NextcloudClient(serverUrl,
httpClient: NextcloudClientExtension.newHttpClient());
final flowClient = client.core.clientFlowLoginV2;
init = await flowClient.init().then((response) => response.body);
log.info('init: $init');
openLoginUrl();
_pollTimer?.cancel();
_pollTimer = Timer.periodic(
const Duration(seconds: 1),
(_) => _catch404Error(() async {
// Throws 404 if not logged in yet
final poll = await flowClient.poll(
$body: ClientFlowLoginV2PollRequestApplicationJson(
(b) => b..token = init!.poll.token,
),
);
_pollTimer?.cancel();
if (!completer.isCompleted) completer.complete(poll.body);
}),
);
});
}
Future<T?> _catchHttpError<T>(Future<T> Function() fn) async {
try {
return await fn();
} on http.ClientException catch (error, stackTrace) {
log.severe('Error while polling the login flow.', error, stackTrace);
if (!completer.isCompleted) completer.completeError(error, stackTrace);
return null;
}
}
Future<T?> _catch404Error<T>(Future<T> Function() fn) async {
try {
return await fn();
} on DynamiteStatusCodeException catch (error) {
if (error.statusCode != 404) rethrow;
log.fine('Login flow not found or completed yet, will repoll');
return null;
}
}
void dispose() {
_pollTimer?.cancel();
if (!completer.isCompleted) {
completer.completeError(TimeoutException('Login flow was disposed'));
}
}
}
+51
View File
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:nextcloudapp/components/nextcloud_routes.dart';
import 'package:nextcloudapp/service/navigation_service.dart';
class NextcloudApp extends StatelessWidget {
NextcloudApp({
super.key,
});
final NextcloudAppRoutes _nextcloudAppRoutes = NextcloudAppRoutes.instance;
final NavigationService _navigationService = NavigationService.instance;
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Nextcloud App',
navigatorKey: _navigationService.navigatorKey,
theme: ThemeData(
primaryColor: Color.fromARGB(255, 152, 120, 57),
scaffoldBackgroundColor: Colors.white,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 0,
foregroundColor: Colors.white,
backgroundColor: Color.fromARGB(255, 152, 120, 57),
shape: const StadiumBorder(),
maximumSize: const Size(double.infinity, 56),
minimumSize: const Size(double.infinity, 56),
),
),
// inputDecorationTheme: const InputDecorationTheme(
// filled: true,
// fillColor: primaryLightColor,
// iconColor: primaryColor,
// prefixIconColor: primaryColor,
// contentPadding: EdgeInsets.symmetric(
// horizontal: defaultPadding, vertical: defaultPadding),
// border: OutlineInputBorder(
// borderRadius: BorderRadius.all(Radius.circular(30)),
// borderSide: BorderSide.none,
// ),
// ),
fontFamily: 'Poppins',
),
routes: _nextcloudAppRoutes.routes,
initialRoute: "login",
);
}
}