Add account UI and secure session storage
This commit is contained in:
@@ -162,9 +162,10 @@ python scripts/eval_knowledge_retrieval.py --dataset evals/knowledge_retrieval_p
|
|||||||
|
|
||||||
- تحفظ FastAPI المحادثات والرسائل في SQLite محليًا. على Windows يوجد الملف في `%LOCALAPPDATA%\SovereignAI\data\sovereign_ai.sqlite3`، وخارج مجلد المشروع لتجنب مزامنة قاعدة البيانات مع OneDrive.
|
- تحفظ FastAPI المحادثات والرسائل في SQLite محليًا. على Windows يوجد الملف في `%LOCALAPPDATA%\SovereignAI\data\sovereign_ai.sqlite3`، وخارج مجلد المشروع لتجنب مزامنة قاعدة البيانات مع OneDrive.
|
||||||
- لكل سجل محادثة `user_id`، وتُفلتر القراءة والتعديل والحذف بهوية جلسة Bearer موثقة؛ لم يعد `X-User-ID` يمنح أي صلاحية. في الوضع المحلي تطلب الواجهة تلقائيًا جلسة للمستخدم المحلي، ولا يصدرها الخادم إلا لاتصال loopback.
|
- لكل سجل محادثة `user_id`، وتُفلتر القراءة والتعديل والحذف بهوية جلسة Bearer موثقة؛ لم يعد `X-User-ID` يمنح أي صلاحية. في الوضع المحلي تطلب الواجهة تلقائيًا جلسة للمستخدم المحلي، ولا يصدرها الخادم إلا لاتصال loopback.
|
||||||
- يوفّر API الآن `POST /v1/auth/register` و`POST /v1/auth/login` و`GET /v1/auth/me` و`POST /v1/auth/logout`. كلمات المرور تُخزن بتجزئة PBKDF2 مع salt؛ رمز الجلسة العشوائي يُخزن كـSHA-256 وينتهي بعد 7 أيام ويمكن إلغاؤه. تسجيل الحساب يعمل عبر الـAPI، لكن واجهة الدخول والحفظ الآمن للجلسة على الأجهزة غير مكتملين.
|
- يوفّر API الآن `POST /v1/auth/register` و`POST /v1/auth/login` و`GET /v1/auth/me` و`POST /v1/auth/logout`. كلمات المرور تُخزن بتجزئة PBKDF2 مع salt؛ رمز الجلسة العشوائي يُخزن كـSHA-256 وينتهي بعد 7 أيام ويمكن إلغاؤه. أضاف Flutter واجهة تسجيل ودخول وخروج، ويحفظ رمز الحساب عبر `flutter_secure_storage` حيث تتوفر آلية النظام. على الويب تعتمد الحزمة على تخزين المتصفح؛ دعمها تجريبي ويحتاج HTTPS أو localhost، لذا لا نعدّه حماية مماثلة لمخزن مفاتيح نظام التشغيل.
|
||||||
|
- بناء Windows يتطلب مكوّن C++ ATL ضمن Visual Studio Build Tools بسبب إضافة `flutter_secure_storage_windows`؛ إذا ظهر خطأ `atlstr.h` ثبّت المكوّن ثم أعد بناء التطبيق. على Android الحد الأدنى صار API 23.
|
||||||
- كل عمليات `/v1/*` الخاصة تتطلب الآن Bearer session؛ الاستثناءات العامة هي الصحة وقائمة النماذج وتسجيل/دخول الحساب وإنشاء الجلسة المحلية. عميل Flutter يرسل الجلسة للمحادثة والوكيل والملفات والمعرفة والبحث والصوت. ملكية سجل تدقيق الوكيل وفهرس المعرفة ومقترحات تعديل الملفات مرتبطة بمعرّف الحساب.
|
- كل عمليات `/v1/*` الخاصة تتطلب الآن Bearer session؛ الاستثناءات العامة هي الصحة وقائمة النماذج وتسجيل/دخول الحساب وإنشاء الجلسة المحلية. عميل Flutter يرسل الجلسة للمحادثة والوكيل والملفات والمعرفة والبحث والصوت. ملكية سجل تدقيق الوكيل وفهرس المعرفة ومقترحات تعديل الملفات مرتبطة بمعرّف الحساب.
|
||||||
- أساس API متعدد الحسابات لا يكفي لفتح الخدمة للشبكة: واجهة تسجيل الدخول غير موجودة، ورمز الحساب لا يُخزن بعد إغلاق التطبيق، ولا توجد حدود لمحاولات الدخول. كذلك يجب حصر مساحات العمل التي يستطيع كل حساب قراءتها ضمن مجلدات مصرح بها؛ عملية FastAPI ترى صلاحيات ملفات نظام التشغيل الذي يشغلها. أبقِ الخدمة على `127.0.0.1` إلى أن تكتمل هذه الضوابط ويُختبر النشر عبر TLS.
|
- أساس API متعدد الحسابات لا يكفي لفتح الخدمة للشبكة: لا توجد حدود لمحاولات الدخول أو استعادة كلمة المرور، ويجب حصر مساحات العمل التي يستطيع كل حساب قراءتها ضمن مجلدات مصرح بها؛ عملية FastAPI ترى صلاحيات ملفات نظام التشغيل الذي يشغلها. أبقِ الخدمة على `127.0.0.1` إلى أن تكتمل هذه الضوابط ويُختبر النشر عبر TLS.
|
||||||
- رسائل الدردشة ترسل إلى Ollama المحلي. التسجيل الصوتي يحوّل إلى نص عبر Groq Whisper؛ لذلك يُرسل الصوت إلى Groq عند الضغط على إيقاف التسجيل.
|
- رسائل الدردشة ترسل إلى Ollama المحلي. التسجيل الصوتي يحوّل إلى نص عبر Groq Whisper؛ لذلك يُرسل الصوت إلى Groq عند الضغط على إيقاف التسجيل.
|
||||||
- مفتاح Groq يجب أن يبقى في متغير البيئة `GROQ_API_KEY` الخاص بخادم FastAPI، ولا يوضع في Flutter أو في ملفات المشروع.
|
- مفتاح Groq يجب أن يبقى في متغير البيئة `GROQ_API_KEY` الخاص بخادم FastAPI، ولا يوضع في Flutter أو في ملفات المشروع.
|
||||||
- على جهاز التطوير الحالي حُفظ المتغير في بيئة Windows الخاصة بالمستخدم، وليس في ملف `.env`. يقرأه `start-api.ps1` عند تشغيل الخادم، ويظهر `/health` حالة الإعداد فقط دون إظهار المفتاح. عند استضافة الخادم لاحقًا، أضف المفتاح إلى إعدادات البيئة السرية في خدمة الاستضافة.
|
- على جهاز التطوير الحالي حُفظ المتغير في بيئة Windows الخاصة بالمستخدم، وليس في ملف `.env`. يقرأه `start-api.ps1` عند تشغيل الخادم، ويظهر `/health` حالة الإعداد فقط دون إظهار المفتاح. عند استضافة الخادم لاحقًا، أضف المفتاح إلى إعدادات البيئة السرية في خدمة الاستضافة.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
- Flutter Windows Debug وFastAPI يعملان محليًا؛ مسار تحليل المرفقات يدعم PDF الرقمي، ويحوّل أول 3 صفحات من PDF الممسوح محليًا ثم يرسلها لنموذج الرؤية المحلي. تجربة حية على صورة اللقاء استخرجت العنوان والمكان عمان والتاريخ 15 تشرين الأول والوقت 6:30 مساءً دون ذكر سنة غير ظاهرة. أُضيفت جلسات حسابات وعزل سجل المحادثات والمعرفة والتدقيق، لكن واجهة الدخول وصلاحيات نظام الملفات وحدود الدخول لم تكتمل؛ لا يوجد نشر شبكي آمن بعد. الصوت يعتمد على Groq خارجي.
|
- Flutter Windows Debug وFastAPI يعملان محليًا؛ مسار تحليل المرفقات يدعم PDF الرقمي، ويحوّل أول 3 صفحات من PDF الممسوح محليًا ثم يرسلها لنموذج الرؤية المحلي. تجربة حية على صورة اللقاء استخرجت العنوان والمكان عمان والتاريخ 15 تشرين الأول والوقت 6:30 مساءً دون ذكر سنة غير ظاهرة. أُضيفت جلسات حسابات وعزل سجل المحادثات والمعرفة والتدقيق، لكن واجهة الدخول وصلاحيات نظام الملفات وحدود الدخول لم تكتمل؛ لا يوجد نشر شبكي آمن بعد. الصوت يعتمد على Groq خارجي.
|
||||||
- التدريب والضبط الدقيق وتوزيع Windows مراحل لاحقة، وليست مما يفعّله التطبيق حاليًا.
|
- التدريب والضبط الدقيق وتوزيع Windows مراحل لاحقة، وليست مما يفعّله التطبيق حاليًا.
|
||||||
- 2026-10-03: اكتمل فرض Bearer على كل عمليات `/v1` الخاصة (عدا الصحة وقائمة النماذج ومسارات بدء المصادقة العامة)، وربط Flutter بالجلسة لكل طلب محادثة/وكيل/ملف/معرفة/ويب/صوت. أصبحت ملكية فهرس المعرفة وسجل التدقيق حسب الحساب، ومعاينة تعديل الملف لا تُطبق إلا بجلسة صاحبها. تحقق OpenAPI وواجهات رفض الرمز وعزل حسابين وترحيل SQLite: 61 اختبار Python ناجح، و11 اختبار Flutter وتحليل Flutter بلا ملاحظات. يبقى قصر الوصول إلى مسارات نظام الملفات لكل مستخدم، وواجهة الدخول وتخزين الرمز الآمن وحدود محاولات الدخول؛ لذلك يظل التشغيل loopback فقط.
|
- 2026-10-03: اكتمل فرض Bearer على كل عمليات `/v1` الخاصة (عدا الصحة وقائمة النماذج ومسارات بدء المصادقة العامة)، وربط Flutter بالجلسة لكل طلب محادثة/وكيل/ملف/معرفة/ويب/صوت. أصبحت ملكية فهرس المعرفة وسجل التدقيق حسب الحساب، ومعاينة تعديل الملف لا تُطبق إلا بجلسة صاحبها. تحقق OpenAPI وواجهات رفض الرمز وعزل حسابين وترحيل SQLite: 61 اختبار Python ناجح، و11 اختبار Flutter وتحليل Flutter بلا ملاحظات. يبقى قصر الوصول إلى مسارات نظام الملفات لكل مستخدم، وواجهة الدخول وتخزين الرمز الآمن وحدود محاولات الدخول؛ لذلك يظل التشغيل loopback فقط.
|
||||||
|
- 2026-10-03: أضيفت شاشة Flutter للحساب (إنشاء/دخول/خروج)، وعند تبديل الهوية تمسح المحادثات المحملة ثم تعيد قراءتها تحت الجلسة الجديدة. رمز الحساب يمر عبر `flutter_secure_storage` ويُستعاد بالتحقق من `/v1/auth/me`؛ اختبار Flutter 11/11 و`flutter analyze` بلا ملاحظات. Android مضبوط على API 23. **لم يكتمل بناء Windows:** Visual Studio Build Tools يفتقد ATL (`atlstr.h`) المطلوب من الإضافة، ومحاولة إضافة المكوّن لم تُثبته؛ يجب إصلاح مكوّن البناء ثم إعادة `flutter build windows --debug` قبل اعتبار خطوة Windows مجربة. بناء الويب بدأ لكن لم يُحسم بعد.
|
||||||
|
|
||||||
## المرحلة 1 — تجربة المحادثة
|
## المرحلة 1 — تجربة المحادثة
|
||||||
|
|
||||||
@@ -64,8 +65,11 @@
|
|||||||
- [x] فرض Bearer على جميع عمليات `/v1` الخاصة وإرسال الجلسة من Flutter للمحادثة/الوكيل/الملفات/المعرفة/البحث والصوت؛ فحص OpenAPI يضمن ألا توجد عملية خاصة بلا HTTP Bearer.
|
- [x] فرض Bearer على جميع عمليات `/v1` الخاصة وإرسال الجلسة من Flutter للمحادثة/الوكيل/الملفات/المعرفة/البحث والصوت؛ فحص OpenAPI يضمن ألا توجد عملية خاصة بلا HTTP Bearer.
|
||||||
- [x] عزل فهرس المعرفة وسجل التدقيق بمعرّف الحساب، وربط رمز معاينة تعديل الملفات بصاحبها؛ اختبار API أثبت عدم استرجاع حساب لمحتوى فهرسه حساب آخر.
|
- [x] عزل فهرس المعرفة وسجل التدقيق بمعرّف الحساب، وربط رمز معاينة تعديل الملفات بصاحبها؛ اختبار API أثبت عدم استرجاع حساب لمحتوى فهرسه حساب آخر.
|
||||||
- [ ] قصر مسارات مساحة العمل على مجلدات مصرح بها لكل مستخدم، وإزالة صلاحيات FastAPI العامة عن ملفات المضيف قبل السماح بعميل شبكي أو خدمة مستضافة.
|
- [ ] قصر مسارات مساحة العمل على مجلدات مصرح بها لكل مستخدم، وإزالة صلاحيات FastAPI العامة عن ملفات المضيف قبل السماح بعميل شبكي أو خدمة مستضافة.
|
||||||
- [ ] بناء شاشة إنشاء الحساب/الدخول والخروج، وتخزين الرموز في مخزن آمن مناسب لكل منصة؛ حاليًا عميل Flutter يطلب جلسة محلية تلقائيًا، ودوال الحساب غير موصولة بواجهة ولا تحفظ رمزها بعد إغلاق التطبيق.
|
- [x] بناء واجهة إنشاء الحساب/الدخول والخروج وربطها بجلسات API؛ عند تبديل الحساب تمسح الواجهة الحالة المحلية ثم تعيد تحميل المحادثات والمهارات تحت الهوية الجديدة.
|
||||||
|
- [x] حفظ رمز الحساب عبر `flutter_secure_storage` في مخزن النظام على المنصات المدعومة، واستعادة `/v1/auth/me` عند بدء التطبيق؛ Android مضبوط على API 23 كحد أدنى. تخزين الويب تجريبي ويعتمد على المتصفح، لذلك لا يُعامل كحماية نظام تشغيل.
|
||||||
- [ ] إضافة حدود لمحاولات الدخول/التسجيل وتدفق استعادة كلمة المرور واختبار نشر محلي عبر TLS قبل الوصول الشبكي.
|
- [ ] إضافة حدود لمحاولات الدخول/التسجيل وتدفق استعادة كلمة المرور واختبار نشر محلي عبر TLS قبل الوصول الشبكي.
|
||||||
|
- [ ] تهيئة تخزين آمن للحساب على الويب أو تعطيل حفظ الرمز هناك؛ التحقق من متطلبات Linux keyring وWindows/macOS على منصات بناء فعلية.
|
||||||
|
- [ ] تثبيت/تفعيل مكوّن C++ ATL في Visual Studio Build Tools وإعادة بناء Windows؛ المحاولة بتاريخ 2026-10-03 ما زالت تُظهر الخطأ `atlstr.h` المفقود.
|
||||||
- تصميم بيانات المستخدمين والمحادثات والمرفقات ونسخ الإجابات مع ملكية واضحة وفهارس وترحيلات قاعدة بيانات.
|
- تصميم بيانات المستخدمين والمحادثات والمرفقات ونسخ الإجابات مع ملكية واضحة وفهارس وترحيلات قاعدة بيانات.
|
||||||
- SQLite مناسب لنسخة محلية أحادية الجهاز. عند تشغيل خدمة لعدة مستخدمين/أجهزة، ننتقل إلى PostgreSQL، مع نسخ احتياطية وسياسة حذف وتصدير.
|
- SQLite مناسب لنسخة محلية أحادية الجهاز. عند تشغيل خدمة لعدة مستخدمين/أجهزة، ننتقل إلى PostgreSQL، مع نسخ احتياطية وسياسة حذف وتصدير.
|
||||||
- تخزين الملفات الكبيرة في مساحة ملفات منظّمة، وحفظ بياناتها الوصفية ومراجعها في قاعدة البيانات، لا في سجل الرسائل ككتل ضخمة.
|
- تخزين الملفات الكبيرة في مساحة ملفات منظّمة، وحفظ بياناتها الوصفية ومراجعها في قاعدة البيانات، لا في سجل الرسائل ككتل ضخمة.
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ android {
|
|||||||
applicationId = "com.example.flutter_app"
|
applicationId = "com.example.flutter_app"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
// flutter_secure_storage 10.x requires Android API 23 or newer.
|
||||||
|
minSdk = 23
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
|
abstract interface class SessionTokenStore {
|
||||||
|
Future<String?> read();
|
||||||
|
|
||||||
|
Future<void> write(String token);
|
||||||
|
|
||||||
|
Future<void> delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecureSessionTokenStore implements SessionTokenStore {
|
||||||
|
SecureSessionTokenStore({FlutterSecureStorage? storage})
|
||||||
|
: _storage = storage ?? const FlutterSecureStorage();
|
||||||
|
|
||||||
|
static const _key = 'sovereignai.account.access_token';
|
||||||
|
|
||||||
|
final FlutterSecureStorage _storage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> read() => _storage.read(key: _key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> write(String token) => _storage.write(key: _key, value: token);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete() => _storage.delete(key: _key);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:http_parser/http_parser.dart';
|
|||||||
import 'package:file_selector/file_selector.dart';
|
import 'package:file_selector/file_selector.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../auth/session_token_store.dart';
|
||||||
import '../../features/chat/presentation/cubit/chat_state.dart';
|
import '../../features/chat/presentation/cubit/chat_state.dart';
|
||||||
|
|
||||||
const _uuid = Uuid();
|
const _uuid = Uuid();
|
||||||
@@ -18,13 +19,25 @@ class AgentRunResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ApiRepository {
|
class ApiRepository {
|
||||||
ApiRepository({required String baseUrl}) : _baseUrl = baseUrl;
|
ApiRepository({required String baseUrl, SessionTokenStore? sessionTokenStore})
|
||||||
|
: _baseUrl = baseUrl,
|
||||||
|
_sessionTokenStore = sessionTokenStore ?? SecureSessionTokenStore();
|
||||||
|
|
||||||
String _baseUrl;
|
String _baseUrl;
|
||||||
|
final SessionTokenStore _sessionTokenStore;
|
||||||
String? _accessToken;
|
String? _accessToken;
|
||||||
Future<String>? _loadingLocalSession;
|
Future<String>? _loadingLocalSession;
|
||||||
|
Future<void>? _restoringSession;
|
||||||
|
Map<String, dynamic>? _currentUser;
|
||||||
|
bool _sessionRestored = false;
|
||||||
http.Client? _activeRequestClient;
|
http.Client? _activeRequestClient;
|
||||||
String get baseUrl => _baseUrl;
|
String get baseUrl => _baseUrl;
|
||||||
|
Map<String, dynamic>? get currentUser => _currentUser;
|
||||||
|
String? get accountEmail {
|
||||||
|
final user = _currentUser;
|
||||||
|
if (user == null || user['mode'] != 'account') return null;
|
||||||
|
return user['email'] as String?;
|
||||||
|
}
|
||||||
|
|
||||||
void cancelActiveRequest() {
|
void cancelActiveRequest() {
|
||||||
_activeRequestClient?.close();
|
_activeRequestClient?.close();
|
||||||
@@ -37,6 +50,44 @@ class ApiRepository {
|
|||||||
_baseUrl = value.replaceFirst(RegExp(r'/+$'), '');
|
_baseUrl = value.replaceFirst(RegExp(r'/+$'), '');
|
||||||
_accessToken = null;
|
_accessToken = null;
|
||||||
_loadingLocalSession = null;
|
_loadingLocalSession = null;
|
||||||
|
_currentUser = null;
|
||||||
|
_sessionRestored = false;
|
||||||
|
_restoringSession = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initializeSession() async {
|
||||||
|
if (_sessionRestored) return;
|
||||||
|
final pending = _restoringSession;
|
||||||
|
if (pending != null) return pending;
|
||||||
|
final restoring = _restoreAccountSession();
|
||||||
|
_restoringSession = restoring;
|
||||||
|
try {
|
||||||
|
await restoring;
|
||||||
|
} finally {
|
||||||
|
_restoringSession = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _restoreAccountSession() async {
|
||||||
|
try {
|
||||||
|
final token = await _sessionTokenStore.read();
|
||||||
|
if (token == null || token.isEmpty) return;
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('$_baseUrl/v1/auth/me'),
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
);
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
|
await _sessionTokenStore.delete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
_accessToken = token;
|
||||||
|
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||||
|
} catch (_) {
|
||||||
|
// Local single-user mode remains available if secure storage is absent.
|
||||||
|
} finally {
|
||||||
|
_sessionRestored = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getModelName() async {
|
Future<String> getModelName() async {
|
||||||
@@ -64,6 +115,7 @@ class ApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _localSessionToken() async {
|
Future<String> _localSessionToken() async {
|
||||||
|
await initializeSession();
|
||||||
final cached = _accessToken;
|
final cached = _accessToken;
|
||||||
if (cached != null) return cached;
|
if (cached != null) return cached;
|
||||||
final existing = _loadingLocalSession;
|
final existing = _loadingLocalSession;
|
||||||
@@ -86,6 +138,7 @@ class ApiRepository {
|
|||||||
_checkStatus(response);
|
_checkStatus(response);
|
||||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
_accessToken = data['access_token'] as String;
|
_accessToken = data['access_token'] as String;
|
||||||
|
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||||
return _accessToken!;
|
return _accessToken!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,20 +169,56 @@ class ApiRepository {
|
|||||||
);
|
);
|
||||||
_checkStatus(response);
|
_checkStatus(response);
|
||||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
_accessToken = data['access_token'] as String;
|
final token = data['access_token'] as String;
|
||||||
|
try {
|
||||||
|
await _sessionTokenStore.write(token);
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await http.post(
|
||||||
|
Uri.parse('$_baseUrl/v1/auth/logout'),
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// The short-lived server session expires automatically if revocation fails.
|
||||||
|
}
|
||||||
|
throw Exception(
|
||||||
|
'تعذر حفظ جلسة الحساب في التخزين الآمن على هذا الجهاز: $error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_accessToken = token;
|
||||||
|
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||||
|
_sessionRestored = true;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
|
await initializeSession();
|
||||||
final token = _accessToken;
|
final token = _accessToken;
|
||||||
if (token == null) return;
|
if (token == null) return;
|
||||||
|
Object? logoutError;
|
||||||
|
try {
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse('$_baseUrl/v1/auth/logout'),
|
Uri.parse('$_baseUrl/v1/auth/logout'),
|
||||||
headers: {'Authorization': 'Bearer $token'},
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
);
|
);
|
||||||
_checkStatus(response);
|
_checkStatus(response);
|
||||||
|
} catch (error) {
|
||||||
|
logoutError = error;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await _sessionTokenStore.delete();
|
||||||
|
} finally {
|
||||||
_accessToken = null;
|
_accessToken = null;
|
||||||
|
_currentUser = null;
|
||||||
_loadingLocalSession = null;
|
_loadingLocalSession = null;
|
||||||
|
_sessionRestored = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (logoutError != null) {
|
||||||
|
throw Exception(
|
||||||
|
'أُزيل الرمز من الجهاز، لكن تعذر تأكيد إلغاء جلسة الخادم: $logoutError',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkStatus(http.Response response) {
|
void _checkStatus(http.Response response) {
|
||||||
|
|||||||
@@ -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 {
|
Future<void> _saveCurrent(List<ChatMessage> messages) async {
|
||||||
if (messages.isEmpty) return;
|
if (messages.isEmpty) return;
|
||||||
final firstQuestion = messages.firstWhere(
|
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_cubit.dart';
|
||||||
import '../../../audio/presentation/cubit/voice_state.dart';
|
import '../../../audio/presentation/cubit/voice_state.dart';
|
||||||
import '../../../../core/notifications/completion_notification_service.dart';
|
import '../../../../core/notifications/completion_notification_service.dart';
|
||||||
|
import '../../../../core/network/api_repository.dart';
|
||||||
import '../cubit/chat_cubit.dart';
|
import '../cubit/chat_cubit.dart';
|
||||||
import '../cubit/chat_state.dart';
|
import '../cubit/chat_state.dart';
|
||||||
|
|
||||||
@@ -423,6 +424,232 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
controller.dispose();
|
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 {
|
Future<void> _toggleRecording(VoiceState voice) async {
|
||||||
final cubit = context.read<VoiceCubit>();
|
final cubit = context.read<VoiceCubit>();
|
||||||
if (voice.status == VoiceStatus.recording) {
|
if (voice.status == VoiceStatus.recording) {
|
||||||
@@ -1081,6 +1308,18 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: compact ? 2 : 8),
|
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(
|
IconButton(
|
||||||
onPressed: _settings,
|
onPressed: _settings,
|
||||||
tooltip: 'إعداد الاتصال',
|
tooltip: 'إعداد الاتصال',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
#include <local_notifier/local_notifier_plugin.h>
|
#include <local_notifier/local_notifier_plugin.h>
|
||||||
#include <record_linux/record_linux_plugin.h>
|
#include <record_linux/record_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
@@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
|
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
|
||||||
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
|
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
|
flutter_secure_storage_linux
|
||||||
local_notifier
|
local_notifier
|
||||||
record_linux
|
record_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Foundation
|
|||||||
|
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
|
import flutter_secure_storage_darwin
|
||||||
import local_notifier
|
import local_notifier
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import record_macos
|
import record_macos
|
||||||
@@ -16,6 +17,7 @@ import url_launcher_macos
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
|
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||||
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||||
|
|||||||
@@ -270,6 +270,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.0"
|
version: "8.0.0"
|
||||||
|
flutter_secure_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage
|
||||||
|
sha256: fe638107c5f69119156ada2db5a57734385fac3f64430bd7252a00d3ead2ca4b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.3.4"
|
||||||
|
flutter_secure_storage_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_darwin
|
||||||
|
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.2"
|
||||||
|
flutter_secure_storage_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_linux
|
||||||
|
sha256: caa75bd78f017422912e3a904933113b2428eb79f981ac37acd2dd2a700458ea
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
flutter_secure_storage_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_platform_interface
|
||||||
|
sha256: "64951127f001f546891c86f414972bf49ef2c4c8a4c92c65f5790cc0c9100045"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
flutter_secure_storage_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_web
|
||||||
|
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
flutter_secure_storage_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_windows
|
||||||
|
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ dependencies:
|
|||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
|
flutter_secure_storage: 10.3.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <local_notifier/local_notifier_plugin.h>
|
#include <local_notifier/local_notifier_plugin.h>
|
||||||
#include <record_windows/record_windows_plugin_c_api.h>
|
#include <record_windows/record_windows_plugin_c_api.h>
|
||||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
LocalNotifierPluginRegisterWithRegistrar(
|
LocalNotifierPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
||||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
flutter_secure_storage_windows
|
||||||
local_notifier
|
local_notifier
|
||||||
record_windows
|
record_windows
|
||||||
share_plus
|
share_plus
|
||||||
|
|||||||
Reference in New Issue
Block a user