Add account UI and secure session storage
This commit is contained in:
@@ -423,6 +423,28 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadForAccountChange() async {
|
||||
if (state.isSending) return;
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: const [],
|
||||
conversations: const [],
|
||||
clearActiveConversation: true,
|
||||
isLoadingHistory: true,
|
||||
isWorkspaceMode: false,
|
||||
clearSelectedWorkspacePath: true,
|
||||
availableWorkspaceFiles: const [],
|
||||
selectedWorkspaceFiles: const [],
|
||||
clearSelectedSkill: true,
|
||||
requestStatus: RequestStatus.idle,
|
||||
clearAgentProgress: true,
|
||||
clearPendingFileChangeProposal: true,
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
await Future.wait([_loadHistory(), _loadAgentSkills()]);
|
||||
}
|
||||
|
||||
Future<void> _saveCurrent(List<ChatMessage> messages) async {
|
||||
if (messages.isEmpty) return;
|
||||
final firstQuestion = messages.firstWhere(
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../audio/presentation/cubit/voice_cubit.dart';
|
||||
import '../../../audio/presentation/cubit/voice_state.dart';
|
||||
import '../../../../core/notifications/completion_notification_service.dart';
|
||||
import '../../../../core/network/api_repository.dart';
|
||||
import '../cubit/chat_cubit.dart';
|
||||
import '../cubit/chat_state.dart';
|
||||
|
||||
@@ -423,6 +424,232 @@ class _ChatPageState extends State<ChatPage> {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
Future<void> _manageAccount() async {
|
||||
final cubit = context.read<ChatCubit>();
|
||||
final api = context.read<ApiRepository>();
|
||||
if (cubit.state.isSending) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('انتظر حتى ينتهي الطلب قبل تبديل الحساب.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (api.accountEmail != null) {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: AlertDialog(
|
||||
title: const Text('تسجيل الخروج'),
|
||||
content: Text('هل تريد تسجيل الخروج من ${api.accountEmail}؟'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('إلغاء'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('تسجيل الخروج'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
try {
|
||||
await api.logout();
|
||||
await cubit.reloadForAccountChange();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم تسجيل الخروج والعودة إلى الحساب المحلي.'),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString().replaceFirst('Exception: ', '')),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final emailController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
var register = false;
|
||||
var validationError = '';
|
||||
final credentials = await showDialog<(bool, String, String)>(
|
||||
context: context,
|
||||
builder:
|
||||
(dialogContext) => StatefulBuilder(
|
||||
builder:
|
||||
(context, setDialogState) => Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: AlertDialog(
|
||||
title: Text(register ? 'إنشاء حساب' : 'تسجيل الدخول'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: emailController,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'البريد الإلكتروني',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
autofillHints: [
|
||||
register
|
||||
? AutofillHints.newPassword
|
||||
: AutofillHints.password,
|
||||
],
|
||||
onSubmitted: (_) {
|
||||
final email = emailController.text.trim();
|
||||
final password = passwordController.text;
|
||||
if (email.isEmpty || password.isEmpty) {
|
||||
setDialogState(
|
||||
() =>
|
||||
validationError =
|
||||
'أدخل البريد وكلمة المرور.',
|
||||
);
|
||||
} else if (register && password.length < 12) {
|
||||
setDialogState(
|
||||
() =>
|
||||
validationError =
|
||||
'كلمة المرور يجب أن تكون 12 محرفًا على الأقل.',
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(dialogContext, (
|
||||
register,
|
||||
email,
|
||||
password,
|
||||
));
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'كلمة المرور',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
if (register) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
'استخدم 12 محرفًا على الأقل.',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (validationError.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
validationError,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => setDialogState(() {
|
||||
register = !register;
|
||||
validationError = '';
|
||||
}),
|
||||
child: Text(
|
||||
register
|
||||
? 'لديك حساب؟ سجّل الدخول'
|
||||
: 'ليس لديك حساب؟ أنشئ حسابًا',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('إلغاء'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final email = emailController.text.trim();
|
||||
final password = passwordController.text;
|
||||
if (email.isEmpty || password.isEmpty) {
|
||||
setDialogState(
|
||||
() =>
|
||||
validationError = 'أدخل البريد وكلمة المرور.',
|
||||
);
|
||||
} else if (register && password.length < 12) {
|
||||
setDialogState(
|
||||
() =>
|
||||
validationError =
|
||||
'كلمة المرور يجب أن تكون 12 محرفًا على الأقل.',
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(dialogContext, (
|
||||
register,
|
||||
email,
|
||||
password,
|
||||
));
|
||||
}
|
||||
},
|
||||
child: Text(register ? 'إنشاء الحساب' : 'دخول'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
if (credentials == null) return;
|
||||
|
||||
try {
|
||||
if (credentials.$1) {
|
||||
await api.registerAccount(
|
||||
email: credentials.$2,
|
||||
password: credentials.$3,
|
||||
);
|
||||
} else {
|
||||
await api.login(email: credentials.$2, password: credentials.$3);
|
||||
}
|
||||
await cubit.reloadForAccountChange();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
credentials.$1
|
||||
? 'تم إنشاء الحساب وتخزين الجلسة بأمان.'
|
||||
: 'تم تسجيل الدخول وتخزين الجلسة بأمان.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString().replaceFirst('Exception: ', '')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleRecording(VoiceState voice) async {
|
||||
final cubit = context.read<VoiceCubit>();
|
||||
if (voice.status == VoiceStatus.recording) {
|
||||
@@ -1081,6 +1308,18 @@ class _ChatPageState extends State<ChatPage> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: compact ? 2 : 8),
|
||||
IconButton(
|
||||
onPressed: _manageAccount,
|
||||
tooltip:
|
||||
context.read<ApiRepository>().accountEmail == null
|
||||
? 'تسجيل الدخول أو إنشاء حساب'
|
||||
: 'الحساب: ${context.read<ApiRepository>().accountEmail} · تسجيل الخروج',
|
||||
color:
|
||||
context.read<ApiRepository>().accountEmail == null
|
||||
? _ink
|
||||
: _green,
|
||||
icon: const Icon(Icons.account_circle_outlined),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _settings,
|
||||
tooltip: 'إعداد الاتصال',
|
||||
|
||||
Reference in New Issue
Block a user