Split welcome and add authview

This commit is contained in:
2026-01-19 11:14:50 +01:00
parent 36af248069
commit 8de8cc2283
4 changed files with 177 additions and 127 deletions

View File

@@ -32,11 +32,13 @@ class AuthenticationManager extends ChangeNotifier {
} }
bool _isAuthenticated = false; bool _isAuthenticated = false;
bool _isInitializing = true;
String _apiToken = ''; String _apiToken = '';
UserInfo? _userInfo; UserInfo? _userInfo;
BrandmeisterClient? _client; BrandmeisterClient? _client;
bool get isAuthenticated => _isAuthenticated; bool get isAuthenticated => _isAuthenticated;
bool get isInitializing => _isInitializing;
String get apiToken => _apiToken; String get apiToken => _apiToken;
UserInfo? get userInfo => _userInfo; UserInfo? get userInfo => _userInfo;
@@ -50,7 +52,6 @@ class AuthenticationManager extends ChangeNotifier {
try { try {
_userInfo = await _client!.whoami(); _userInfo = await _client!.whoami();
_isAuthenticated = true; _isAuthenticated = true;
notifyListeners();
} catch (e) { } catch (e) {
// Token is invalid, clear it // Token is invalid, clear it
await logout(); await logout();
@@ -58,6 +59,9 @@ class AuthenticationManager extends ChangeNotifier {
} }
} catch (e) { } catch (e) {
debugPrint('Error loading token: $e'); debugPrint('Error loading token: $e');
} finally {
_isInitializing = false;
notifyListeners();
} }
} }

138
lib/views/auth_view.dart Normal file
View File

@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/authentication_manager.dart';
import '../services/brandmeister_client.dart';
class AuthView extends StatefulWidget {
const AuthView({super.key});
@override
State<AuthView> createState() => _AuthViewState();
}
class _AuthViewState extends State<AuthView> {
final TextEditingController _tokenController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isLoading = false;
String? _errorMessage;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_tokenController.dispose();
_focusNode.dispose();
super.dispose();
}
Future<void> _verifyToken() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final authManager = context.read<AuthenticationManager>();
await authManager.verifyAndSaveToken(_tokenController.text);
} on BrandmeisterError catch (e) {
setState(() {
_errorMessage = e.message;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Authentication failed: ${e.toString()}';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.radio,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
'Sign In',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Enter your BrandMeister API token',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
TextField(
controller: _tokenController,
focusNode: _focusNode,
obscureText: true,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'API Token',
hintText: 'Enter your BrandMeister API token',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key),
errorText: _errorMessage,
),
onSubmitted: (_) {
if (_tokenController.text.isNotEmpty && !_isLoading) {
_verifyToken();
}
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _tokenController.text.isEmpty || _isLoading
? null
: _verifyToken,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Sign In'),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: () {
// Open BrandMeister website
},
icon: const Icon(Icons.open_in_new),
label: const Text('Get API Token from BrandMeister'),
),
],
),
),
),
),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../services/authentication_manager.dart'; import '../services/authentication_manager.dart';
import 'welcome_view.dart'; import 'welcome_view.dart';
import 'auth_view.dart';
import 'main_view.dart'; import 'main_view.dart';
class ContentView extends StatelessWidget { class ContentView extends StatelessWidget {
@@ -11,10 +12,16 @@ class ContentView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final authManager = context.watch<AuthenticationManager>(); final authManager = context.watch<AuthenticationManager>();
// Show splash screen while initializing
if (authManager.isInitializing) {
return const WelcomeView();
}
// Show main view if authenticated, otherwise show login
if (authManager.isAuthenticated) { if (authManager.isAuthenticated) {
return const MainView(); return const MainView();
} else { } else {
return const WelcomeView(); return const AuthView();
} }
} }
} }

View File

@@ -1,72 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/authentication_manager.dart';
import '../services/brandmeister_client.dart';
class WelcomeView extends StatefulWidget { class WelcomeView extends StatelessWidget {
const WelcomeView({super.key}); const WelcomeView({super.key});
@override
State<WelcomeView> createState() => _WelcomeViewState();
}
class _WelcomeViewState extends State<WelcomeView> {
final TextEditingController _tokenController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isLoading = false;
String? _errorMessage;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_tokenController.dispose();
_focusNode.dispose();
super.dispose();
}
Future<void> _verifyToken() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final authManager = context.read<AuthenticationManager>();
await authManager.verifyAndSaveToken(_tokenController.text);
} on BrandmeisterError catch (e) {
setState(() {
_errorMessage = e.message;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Authentication failed: ${e.toString()}';
_isLoading = false;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SafeArea( body: Center(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Icon( Icon(
Icons.radio, Icons.radio,
size: 80, size: 100,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -75,64 +21,19 @@ class _WelcomeViewState extends State<WelcomeView> {
style: Theme.of(context).textTheme.headlineLarge?.copyWith( style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Manage your BrandMeister devices', 'Manage your BrandMeister devices',
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600], color: Colors.grey[600],
), ),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 48), const SizedBox(height: 48),
TextField( const CircularProgressIndicator(),
controller: _tokenController,
focusNode: _focusNode,
obscureText: true,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'API Token',
hintText: 'Enter your BrandMeister API token',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key),
errorText: _errorMessage,
),
onSubmitted: (_) {
if (_tokenController.text.isNotEmpty && !_isLoading) {
_verifyToken();
}
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _tokenController.text.isEmpty || _isLoading
? null
: _verifyToken,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Sign In'),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: () {
// Open BrandMeister website
},
icon: const Icon(Icons.open_in_new),
label: const Text('Get API Token from BrandMeister'),
),
], ],
), ),
), ),
),
),
); );
} }
} }