import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:chepuhagram/logic/auth_provider.dart'; import 'package:chepuhagram/data/models/session_model.dart'; import 'package:timeago/timeago.dart' as timeago; class SessionsScreen extends StatefulWidget { const SessionsScreen({super.key}); @override State createState() => _SessionsScreenState(); } class _SessionsScreenState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { Provider.of(context, listen: false).fetchSessions(); } }); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.background, appBar: AppBar( title: Text( 'Активные сеансы', style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface), ), iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface), elevation: 0, backgroundColor: Colors.transparent, ), body: Consumer( builder: (context, auth, child) { if (auth.isLoading && auth.sessions.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (auth.error != null) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Ошибка: ${auth.error}', textAlign: TextAlign.center), const SizedBox(height: 20), ElevatedButton( onPressed: () => auth.fetchSessions(), child: const Text('Попробовать снова'), ), ], ), ), ); } return RefreshIndicator( onRefresh: () => auth.fetchSessions(), child: _SessionsList(sessions: auth.sessions), ); }, ), ); } } class _SessionsList extends StatelessWidget { final List sessions; const _SessionsList({required this.sessions}); Future _revokeSession(BuildContext context, int sessionId) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Завершить сеанс?'), content: const Text( 'Вы уверены, что хотите завершить этот сеанс? Это действие нельзя будет отменить.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Отмена'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), style: TextButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.error, ), child: const Text('Завершить'), ), ], ), ); if (confirmed == true && context.mounted) { await Provider.of( context, listen: false, ).revokeSession(sessionId); } } Future _revokeAllOtherSessions(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Завершить все другие сеансы?'), content: const Text( 'Это завершит все сеансы на всех устройствах, кроме текущего.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Отмена'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), style: TextButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.error, ), child: const Text('Завершить'), ), ], ), ); if (confirmed == true && context.mounted) { await Provider.of( context, listen: false, ).revokeAllOtherSessions(); } } IconData _getIconForDevice(String deviceName) { final name = deviceName.toLowerCase(); if (name.contains('windows') || name.contains('linux') || name.contains('macos')) { return Icons.desktop_windows_outlined; } if (name.contains('web') || name.contains('chrome')) { return Icons.language_outlined; } if (name.contains('android') || name.contains('ios') || name.contains('mobile')) { return Icons.phone_android_outlined; } return Icons.device_unknown_outlined; } @override Widget build(BuildContext context) { final Session? currentSession = sessions.isEmpty ? null : sessions.firstWhere((s) => s.isCurrent, orElse: () => sessions.first); final otherSessions = sessions .where((s) => s.id != currentSession?.id) .toList(); return ListView( padding: EdgeInsets.zero, children: [ if (currentSession != null) ...[ _buildSectionHeader(context, 'Текущий сеанс'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildGroupContainer( context, child: _buildSessionTile( context, currentSession, isCurrent: true, ), ), ), ], const SizedBox(height: 24), if (otherSessions.isNotEmpty) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildGroupContainer( context, isError: true, child: ListTile( contentPadding: const EdgeInsets.symmetric( horizontal: 20, vertical: 4, ), leading: Icon( Icons.phonelink_erase_outlined, color: Theme.of(context).colorScheme.error, ), title: Text( 'Завершить все остальные сеансы', style: TextStyle( color: Theme.of(context).colorScheme.error, fontWeight: FontWeight.w600, ), ), onTap: () => _revokeAllOtherSessions(context), ), ), ), const SizedBox(height: 24), _buildSectionHeader(context, 'Активные сеансы'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _buildGroupContainer( context, child: Column( children: otherSessions.map((session) { return Column( children: [ _buildSessionTile(context, session), if (otherSessions.last != session) _buildDivider(context), ], ); }).toList(), ), ), ), ] else if (sessions.isNotEmpty) ...[ Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Нет других активных сеансов.', style: Theme.of(context).textTheme.bodySmall, ), ), ), ], if (sessions.isEmpty && !Provider.of(context, listen: false).isLoading) Center( child: Padding( padding: const EdgeInsets.all(32.0), child: Text( 'Нет информации о сеансах.', style: Theme.of(context).textTheme.bodySmall, ), ), ), ], ); } Widget _buildSessionTile( BuildContext context, Session session, { bool isCurrent = false, }) { final colorScheme = Theme.of(context).colorScheme; DateTime now = DateTime.now(); Duration offset = now.timeZoneOffset; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.primary.withOpacity(isCurrent ? 0.12 : 0.08), borderRadius: BorderRadius.circular(12), ), child: Icon( _getIconForDevice(session.deviceName), color: colorScheme.primary, size: 24, ), ), title: Text( session.deviceName, style: TextStyle( fontWeight: isCurrent ? FontWeight.bold : FontWeight.w600, fontSize: 16, ), ), subtitle: Text( '${session.ipAddress}\nАктивность: ${timeago.format(session.lastActive.toLocal(), locale: 'ru')}', style: TextStyle(color: colorScheme.outline, fontSize: 13), ), trailing: isCurrent ? null : TextButton( onPressed: () => _revokeSession(context, session.id), child: Text('Выйти', style: TextStyle(color: colorScheme.error)), ), ); } Widget _buildGroupContainer( BuildContext context, { required Widget child, bool isError = false, }) { final colorScheme = Theme.of(context).colorScheme; return Container( decoration: BoxDecoration( color: isError ? colorScheme.errorContainer.withOpacity(0.15) : colorScheme.surfaceVariant.withOpacity(0.2), borderRadius: BorderRadius.circular(24), border: Border.all( color: isError ? colorScheme.errorContainer.withOpacity(0.2) : colorScheme.outlineVariant.withOpacity(0.1), ), ), child: child, ); } Widget _buildDivider(BuildContext context) => Divider( height: 1, indent: 68, color: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.15), ); Widget _buildSectionHeader(BuildContext context, String title) { return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), child: Text( title.toUpperCase(), style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, fontSize: 12, letterSpacing: 0.5, ), ), ); } }