diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 952dffd7..fdaeb535 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import '../services/plex_auth_service.dart'; import '../services/storage_service.dart'; import 'server_selection_screen.dart'; @@ -18,6 +19,8 @@ class _AuthScreenState extends State { String? _errorMessage; late PlexAuthService _authService; bool _shouldCancelPolling = false; + bool _useQrFlow = false; // whether current auth attempt is QR based + String? _qrAuthUrl; // auth URL rendered as QR @override void initState() { @@ -34,6 +37,10 @@ class _AuthScreenState extends State { _isAuthenticating = true; _errorMessage = null; _shouldCancelPolling = false; + // preserve _useQrFlow as chosen prior to calling + if (!_useQrFlow) { + _qrAuthUrl = null; // ensure stale QR cleared for browser flow + } }); try { @@ -45,12 +52,19 @@ class _AuthScreenState extends State { // Construct auth URL final authUrl = _authService.getAuthUrl(pinCode); - // Open browser (in-app for mobile, external for desktop) - final uri = Uri.parse(authUrl); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.inAppBrowserView); + if (_useQrFlow) { + // Display QR instead of launching browser + setState(() { + _qrAuthUrl = authUrl; + }); } else { - throw Exception('Could not launch auth URL'); + // Open browser (in-app for mobile, external for desktop) + final uri = Uri.parse(authUrl); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.inAppBrowserView); + } else { + throw Exception('Could not launch auth URL'); + } } // Poll for authentication with cancellation support @@ -73,10 +87,12 @@ class _AuthScreenState extends State { } // Auto-close the in-app browser on mobile (no-op on desktop) - try { - await closeInAppWebView(); - } catch (e) { - // Ignore errors - browser might already be closed or on desktop + if (!_useQrFlow) { + try { + await closeInAppWebView(); + } catch (e) { + // Ignore errors - browser might already be closed or on desktop + } } // Store the token @@ -95,6 +111,11 @@ class _AuthScreenState extends State { ), ); } + // Clear QR URL after successful auth + setState(() { + _qrAuthUrl = null; + _useQrFlow = false; + }); } catch (e) { setState(() { _isAuthenticating = false; @@ -107,6 +128,7 @@ class _AuthScreenState extends State { setState(() { _shouldCancelPolling = true; _isAuthenticating = false; + _qrAuthUrl = null; }); // Start new authentication after a brief delay to ensure cleanup Future.delayed(const Duration(milliseconds: 100), _startAuthentication); @@ -226,11 +248,30 @@ class _AuthScreenState extends State { if (_isAuthenticating) ...[ const Center(child: CircularProgressIndicator()), const SizedBox(height: 16), - const Text( - 'Waiting for authentication...\nPlease complete sign-in in your browser.', + Text( + _useQrFlow + ? 'Scan this QR code with a device logged into Plex to authenticate.' + : 'Waiting for authentication...\nPlease complete sign-in in your browser.', textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), + style: const TextStyle(color: Colors.grey), ), + if (_useQrFlow && _qrAuthUrl != null) ...[ + const SizedBox(height: 24), + Center( + child: QrImageView( + data: _qrAuthUrl!, + size: 200, + version: QrVersions.auto, + backgroundColor: Colors.white, + ), + ), + const SizedBox(height: 16), + SelectableText( + _qrAuthUrl!, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], const SizedBox(height: 24), OutlinedButton( onPressed: _retryAuthentication, @@ -242,7 +283,7 @@ class _AuthScreenState extends State { ), child: const Text('Retry'), ), - ] else ...[ + ] else ...[ // add QR button here ElevatedButton( onPressed: _startAuthentication, style: ElevatedButton.styleFrom( @@ -250,6 +291,19 @@ class _AuthScreenState extends State { ), child: const Text('Sign in with Plex'), ), + const SizedBox(height: 12), + OutlinedButton( + onPressed: () { + setState(() { + _useQrFlow = true; + }); + _startAuthentication(); + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text('Show QR Code'), + ), if (kDebugMode) ...[ const SizedBox(height: 12), OutlinedButton( diff --git a/pubspec.yaml b/pubspec.yaml index bbe25e19..c2d7acd8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: flex_color_picker: ^3.6.0 audio_service: ^0.18.18 audio_session: ^0.2.1 + qr_flutter: ^4.1.0 dependency_overrides: media_kit: