Files
swift_companion/swiftyCompanion/swifty_companion/lib/screens/search_screen.dart
2026-09-30 08:24:20 +02:00

217 lines
6.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import '../config/intra_config.dart';
import '../services/intra_api_exception.dart';
import '../services/intra_auth_service.dart';
import '../services/intra_user_service.dart';
import '../theme/swifty_ui.dart';
import 'profile_screen.dart';
/// Vue 1 — recherche par login 42.
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final _auth = IntraAuthService.instance;
final _users = IntraUserService.instance;
final _loginController = TextEditingController();
bool _connecting = false;
bool _searching = false;
String? _error;
String? _sessionLogin;
@override
void dispose() {
_loginController.dispose();
super.dispose();
}
Future<void> _connect() async {
setState(() {
_connecting = true;
_error = null;
});
try {
await _auth.signIn();
final me = await _auth.fetchMe();
if (!mounted) return;
setState(() {
_sessionLogin = me['login'] as String?;
});
} on FlutterAppAuthUserCancelledException {
if (!mounted) return;
setState(() => _error = 'Connexion annulée');
} on FlutterAppAuthPlatformException catch (e) {
final d = e.platformErrorDetails;
if (!mounted) return;
setState(() {
_error = d.errorDescription ??
d.error ??
d.errorDebugDescription ??
e.message ??
'Échec OAuth';
});
} on PlatformException catch (e) {
if (!mounted) return;
setState(() => _error = e.message ?? 'Erreur OAuth');
} catch (e) {
if (!mounted) return;
setState(() => _error = e.toString());
} finally {
if (mounted) setState(() => _connecting = false);
}
}
void _disconnect() {
_auth.signOut();
setState(() {
_sessionLogin = null;
_error = null;
});
}
Future<void> _search() async {
setState(() {
_searching = true;
_error = null;
});
try {
final user = await _users.fetchUserByLogin(_loginController.text);
if (!mounted) return;
await Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (_) => ProfileScreen(user: user),
),
);
} on IntraApiException catch (e) {
if (!mounted) return;
setState(() => _error = e.message);
} catch (e) {
if (!mounted) return;
setState(() => _error = e.toString());
} finally {
if (mounted) setState(() => _searching = false);
}
}
@override
Widget build(BuildContext context) {
if (!IntraConfig.isConfigured) {
return Scaffold(
appBar: SwiftyUi.appBar(title: 'Swifty Companion'),
body: const ResponsiveScrollBody(
maxWidth: SwiftyUi.searchMaxWidth,
child: Text(
'Fichier .env manquant dans swifty_companion/.env',
),
),
);
}
final isAuthenticated = _auth.isAuthenticated;
final busy = _connecting || _searching;
return Scaffold(
appBar: SwiftyUi.appBar(
title: 'Swifty Companion',
actions: [
if (isAuthenticated)
IconButton(
onPressed: busy ? null : _disconnect,
icon: const Icon(Icons.logout),
tooltip: 'Déconnexion API',
),
],
),
body: ResponsiveScrollBody(
maxWidth: SwiftyUi.searchMaxWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!isAuthenticated) ...[
Text(
'Connexion API 42',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Connecte-toi une fois ; le token servira pour toutes les recherches.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 20),
LoadingFilledButton(
label: 'Se connecter avec 42',
loading: _connecting,
onPressed: _connect,
),
] else ...[
Text(
'Session : ${_sessionLogin ?? 'connecté'}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 28),
Text(
'Rechercher un étudiant',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
TextField(
controller: _loginController,
decoration: const InputDecoration(
labelText: 'Login 42',
hintText: 'ex. fgras-ca',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.search,
autocorrect: false,
enableSuggestions: false,
onSubmitted: (_) {
if (!_searching) _search();
},
enabled: !_searching,
),
const SizedBox(height: 16),
LoadingFilledButton(
label: 'Rechercher',
loading: _searching,
onPressed: _search,
),
if (kDebugMode) ...[
const SizedBox(height: 12),
TextButton(
onPressed: _auth.canRefresh
? () {
_auth.debugInvalidateAccessToken();
setState(() {
_error =
'DEBUG : access token invalidé (refresh conservé). '
'Étape suivante : saisis un login et appuie sur « Rechercher ». '
'Si le bonus marche, le profil s’ouvre sans repasser par OAuth 42.';
});
}
: null,
child: const Text('DEBUG : simuler token expiré'),
),
],
],
if (_error != null) ...[
const SizedBox(height: 20),
ErrorMessage(message: _error!),
],
],
),
),
);
}
}