Finish up connections

This commit is contained in:
LittleSheep 2025-06-17 23:49:46 +08:00
parent 9b67d58ee4
commit eb4d2c2e2f
4 changed files with 195 additions and 85 deletions

View File

@ -156,22 +156,8 @@ class AccountSettingsScreen extends HookConsumerWidget {
getLocalizedProviderName(connection.provider), getLocalizedProviderName(connection.provider),
).tr(), ).tr(),
subtitle: subtitle:
connection.meta.isNotEmpty connection.meta['email'] != null
? Column( ? Text(connection.meta['email'])
crossAxisAlignment:
CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
for (final meta
in connection.meta.entries)
Text(
'${meta.key.split('_').map((word) => word[0].toUpperCase() + word.substring(1)).join(' ')}: ${meta.value}',
style: const TextStyle(
fontSize: 12,
),
),
],
)
: Text(connection.providedIdentifier), : Text(connection.providedIdentifier),
leading: CircleAvatar( leading: CircleAvatar(
child: getProviderIcon( child: getProviderIcon(
@ -184,7 +170,6 @@ class AccountSettingsScreen extends HookConsumerWidget {
), ),
).padding(top: 4), ).padding(top: 4),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
isThreeLine: true,
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,

View File

@ -8,6 +8,7 @@ import 'package:island/models/auth.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/account/me/settings.dart'; import 'package:island/screens/account/me/settings.dart';
import 'package:island/screens/auth/oidc.native.dart'; import 'package:island/screens/auth/oidc.native.dart';
import 'package:island/services/text.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
@ -96,6 +97,18 @@ class AccountConnectionSheet extends HookConsumerWidget {
const Gap(8), const Gap(8),
Text(getLocalizedProviderName(connection.provider)).tr(), Text(getLocalizedProviderName(connection.provider)).tr(),
const Gap(4), const Gap(4),
if (connection.meta.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
for (final meta in connection.meta.entries)
Text(
'${meta.key.replaceAll('_', ' ').capitalizeEachWord()}: ${meta.value}',
style: const TextStyle(fontSize: 12),
),
],
),
Text( Text(
connection.providedIdentifier, connection.providedIdentifier,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
@ -174,10 +187,12 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
builder: builder:
(context) => OidcScreen( (context) => OidcScreen(
provider: selectedProvider.value.toLowerCase(), provider: selectedProvider.value.toLowerCase(),
title: 'Connect with ${selectedProvider.value}', title:
'Connect with ${selectedProvider.value.capitalizeEachWord()}',
), ),
), ),
); );
if (context.mounted) Navigator.pop(context, true);
break; break;
default: default:
showSnackBar(context, 'accountConnectionAddError'.tr()); showSnackBar(context, 'accountConnectionAddError'.tr());
@ -326,7 +341,10 @@ class AccountConnectionsSheet extends HookConsumerWidget {
connection.provider, connection.provider,
), ),
).tr(), ).tr(),
subtitle: Text(connection.providedIdentifier), subtitle:
connection.meta['email'] != null
? Text(connection.meta['email'])
: Text(connection.providedIdentifier),
trailing: Text( trailing: Text(
DateFormat.yMd().format( DateFormat.yMd().format(
connection.lastUsedAt.toLocal(), connection.lastUsedAt.toLocal(),

View File

@ -2,12 +2,15 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:gap/gap.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:styled_widget/styled_widget.dart';
class OidcScreen extends ConsumerStatefulWidget { class OidcScreen extends ConsumerStatefulWidget {
final String provider; final String provider;
@ -21,6 +24,15 @@ class OidcScreen extends ConsumerStatefulWidget {
class _OidcScreenState extends ConsumerState<OidcScreen> { class _OidcScreenState extends ConsumerState<OidcScreen> {
String? authToken; String? authToken;
String? currentUrl;
final TextEditingController _urlController = TextEditingController();
bool _isLoading = true;
@override
void dispose() {
_urlController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -31,74 +43,155 @@ class _OidcScreenState extends ConsumerState<OidcScreen> {
appBar: AppBar( appBar: AppBar(
title: widget.title != null ? Text(widget.title!) : Text('login').tr(), title: widget.title != null ? Text(widget.title!) : Text('login').tr(),
), ),
body: InAppWebView( body: Column(
initialSettings: InAppWebViewSettings( children: [
userAgent: Expanded(
kIsWeb child: InAppWebView(
? null initialSettings: InAppWebViewSettings(
: Platform.isIOS userAgent:
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' kIsWeb
: Platform.isAndroid ? null
? 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36' : Platform.isIOS
: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'
), : Platform.isAndroid
initialUrlRequest: URLRequest( ? 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36'
url: WebUri( : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
(token?.token.isNotEmpty ?? false) ),
? '$serverUrl/auth/login/${widget.provider}?tk=${token!.token}' initialUrlRequest: URLRequest(
: '$serverUrl/auth/login/${widget.provider}', url: WebUri('$serverUrl/auth/login/${widget.provider}'),
), headers: {
), if (token?.token.isNotEmpty ?? false)
onWebViewCreated: (controller) { 'Authorization': 'AtField ${token!.token}',
// Register a handler to receive the token from JavaScript },
controller.addJavaScriptHandler( ),
handlerName: 'tokenHandler', onWebViewCreated: (controller) {
callback: (args) { // Register a handler to receive the token from JavaScript
// args[0] will be the token string controller.addJavaScriptHandler(
if (args.isNotEmpty && args[0] is String) { handlerName: 'tokenHandler',
callback: (args) {
// args[0] will be the token string
if (args.isNotEmpty && args[0] is String) {
setState(() {
authToken = args[0];
});
// Return the token and close the webview
Navigator.of(context).pop(authToken);
}
},
);
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
final url = navigationAction.request.url;
if (url != null) {
setState(() {
currentUrl = url.toString();
_urlController.text = currentUrl ?? '';
_isLoading = true;
});
final path = url.path;
final queryParams = url.queryParameters;
// Check if we're on the token page
if (path.contains('/auth/callback')) {
// Extract token from URL
final token = queryParams['token'] ?? true;
// Return the token and close the webview
Navigator.of(context).pop(token);
return NavigationActionPolicy.CANCEL;
}
}
return NavigationActionPolicy.ALLOW;
},
onUpdateVisitedHistory: (controller, url, androidIsReload) {
if (url != null) {
setState(() {
currentUrl = url.toString();
_urlController.text = currentUrl ?? '';
});
}
},
onLoadStop: (controller, url) {
setState(() { setState(() {
authToken = args[0]; _isLoading = false;
}); });
},
// Return the token and close the webview onLoadStart: (controller, url) {
Navigator.of(context).pop(authToken); setState(() {
} _isLoading = true;
}, });
); },
}, onLoadError: (controller, url, code, message) {
shouldOverrideUrlLoading: (controller, navigationAction) async { setState(() {
final url = navigationAction.request.url; _isLoading = false;
if (url != null) { });
final path = url.path; },
final queryParams = url.queryParameters; ),
),
// Check if we're on the token page // Loading progress indicator
if (path.contains('/auth/token') && if (_isLoading)
queryParams.containsKey('token')) { LinearProgressIndicator(
// Extract token from URL color: Theme.of(context).colorScheme.primary,
final token = queryParams['token']!; backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.zero,
// Return the token and close the webview stopIndicatorRadius: 0,
Navigator.of(context).pop(token); minHeight: 2,
return NavigationActionPolicy.CANCEL; )
} else
} ColoredBox(
return NavigationActionPolicy.ALLOW; color: Theme.of(context).colorScheme.surfaceVariant,
}, ).height(2),
onLoadStop: (controller, url) async { // Debug location bar (only visible in debug mode)
if (url != null && url.path.contains('/auth/token')) { Container(
// Inject JavaScript to call our handler with the token padding: EdgeInsets.only(
await controller.evaluateJavascript( left: 16,
source: ''' right: 0,
const urlParams = new URLSearchParams(window.location.search); bottom: MediaQuery.of(context).padding.bottom + 8,
const token = urlParams.get('token'); top: 8,
if (token) { ),
window.flutter_inappwebview.callHandler('tokenHandler', token); color: Theme.of(context).colorScheme.surface,
} child: Row(
''', children: [
); Expanded(
} child: TextField(
}, controller: _urlController,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 8,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
),
hintText: 'URL',
),
style: const TextStyle(fontSize: 12),
readOnly: true,
),
),
const Gap(4),
IconButton(
icon: const Icon(Icons.copy, size: 20),
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: () {
if (currentUrl != null) {
Clipboard.setData(ClipboardData(text: currentUrl!));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('copyToClipboard').tr(),
duration: const Duration(seconds: 1),
),
);
}
},
),
],
),
),
],
), ),
); );
} }

14
lib/services/text.dart Normal file
View File

@ -0,0 +1,14 @@
extension StringExtension on String {
String capitalizeEachWord() {
if (isEmpty) return this;
return split(' ')
.map(
(word) =>
word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}'
: '',
)
.join(' ');
}
}