This commit is contained in:
2026-09-29 14:26:17 +02:00
parent 88a7c2eba0
commit 06fe43bba3
24 changed files with 985 additions and 174 deletions

View File

@@ -0,0 +1,208 @@
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 '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: AppBar(title: const Text('Swifty Companion')),
body: const Padding(
padding: EdgeInsets.all(24),
child: Text(
'Fichier .env manquant dans swifty_companion/.env',
),
),
);
}
final isAuthenticated = _auth.isAuthenticated;
return Scaffold(
appBar: AppBar(
title: const Text('Swifty Companion'),
backgroundColor: const Color(0xFF00BABC),
foregroundColor: Colors.black,
actions: [
if (isAuthenticated)
IconButton(
onPressed: _connecting || _searching ? null : _disconnect,
icon: const Icon(Icons.logout),
tooltip: 'Déconnexion API',
),
],
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!isAuthenticated) ...[
const Text(
'Connexion API 42',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Connecte-toi une fois ; le token servira pour toutes les recherches.',
),
const SizedBox(height: 16),
FilledButton(
onPressed: _connecting ? null : _connect,
child: _connecting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Se connecter avec 42'),
),
] else ...[
Text(
'Session : ${_sessionLogin ?? 'connecté'}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 24),
const Text(
'Rechercher un étudiant',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: _loginController,
decoration: const InputDecoration(
labelText: 'Login 42',
hintText: 'ex. fgras-ca',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.search,
onSubmitted: (_) {
if (!_searching) _search();
},
enabled: !_searching,
),
const SizedBox(height: 16),
FilledButton(
onPressed: _searching ? null : _search,
child: _searching
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Rechercher'),
),
],
if (_error != null) ...[
const SizedBox(height: 16),
Text(
_error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
],
),
),
);
}
}