Parcourir la source

login & logout

ignalxy il y a 4 ans
Parent
commit
2a71e6cb8f

+ 1 - 1
android/app/build.gradle

@@ -35,7 +35,7 @@ android {
     defaultConfig {
         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
         applicationId "xyz.ignatz.e2ee_chat"
-        minSdkVersion 16
+        minSdkVersion 18
         targetSdkVersion 30
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName

+ 4 - 0
android/app/src/main/AndroidManifest.xml

@@ -1,5 +1,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="xyz.ignatz.e2ee_chat">
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <application
         android:label="e2ee_chat"
         android:icon="@mipmap/ic_launcher">

+ 79 - 0
lib/common/api.dart

@@ -0,0 +1,79 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:crypto/crypto.dart';
+import 'package:dio/adapter.dart';
+import 'package:dio/dio.dart';
+import 'package:e2ee_chat/common/global.dart';
+
+class Api {
+  Api();
+
+  static Dio dio = Dio(BaseOptions(
+    baseUrl: 'http://10.122.237.112:80/',
+  ));
+
+  static init() async {
+    if (!Global.isRelease) {
+      (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
+          (client) {
+        client.findProxy = (uri) {
+          return "PROXY 10.122.237.112:8080";
+        };
+        //代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
+        client.badCertificateCallback =
+            (X509Certificate cert, String host, int port) => true;
+      };
+    }
+
+    // debug(await Dio().get('http://10.122.237.112:80/'));
+  }
+
+  Future<String?> login({required String username, String? password, String? token}) async {
+    debug('Api login begin');
+    String _password = "";
+    String? _token;
+    if (password != null) {
+      var bytes = utf8.encode(password);
+      var digest = sha1.convert(bytes);
+      _password = digest.toString();
+    }
+    var data = {"username": username, "token": token ?? Global.emptyToken, "password": password};
+    debug('Api login data: $data');
+    try {
+      var r = await dio.post("/account/login/", data: FormData.fromMap(data),
+        // options: _options,
+      );
+      debug('Api login response: $r');
+      var json = jsonDecode(r.data);
+      _token = json["token"].toString();
+      debug('Api login new token: $_token');
+    } on DioError catch(e) {
+      var data = e.response?.data;
+      var status = e.response?.statusCode;
+      debug('Api login error, statusCode: $status, data: $data');
+      throw e;
+    }
+    debug('Api login end');
+    return _token;
+  }
+
+  Future<bool> logout() async {
+    try {
+      await dio.post("/account/logout/");
+      return true;
+    } on DioError catch(e) {
+      return false;
+    }
+  }
+
+  Future<bool> register({required String username, required String password}) async {
+    try {
+      var data = {"username": username, "password": password, "password2": password};
+      await dio.post('/account/register/', data: FormData.fromMap(data));
+    } on DioError catch(e) {
+      return false;
+    }
+    return true;
+  }
+}

+ 67 - 16
lib/common/global.dart

@@ -1,11 +1,14 @@
 // 提供五套可选主题色
 import 'dart:convert';
 
-import 'package:e2ee_chat/common/profile.dart';
-import 'package:e2ee_chat/network/api.dart';
+import 'package:dio/dio.dart';
+import 'package:e2ee_chat/entities/profile.dart';
+import 'package:e2ee_chat/common/api.dart';
 import 'package:e2ee_chat/models/user_model.dart';
+import 'package:e2ee_chat/objectbox.g.dart';
 import 'package:flutter/material.dart';
-import 'package:shared_preferences/shared_preferences.dart';
+import 'package:objectbox/objectbox.dart';
+import 'package:permission_handler/permission_handler.dart';
 
 const _themes = <MaterialColor>[
   Colors.blue,
@@ -15,7 +18,16 @@ const _themes = <MaterialColor>[
   Colors.red,
 ];
 
+void debug(Object? o) {
+  if (!Global.isRelease) {
+    print(o);
+  }
+}
+
 class Global {
+  static GlobalKey<NavigatorState> navigatorKey = GlobalKey();
+
+  static final int profileId = 1;
   static Profile profile = Profile();
 
   // 可选的主题列表
@@ -26,25 +38,64 @@ class Global {
 
   static final String emptyToken = "token";
 
-  //初始化全局信息,会在APP启动时执行
-  static init() async {
-    var _prefs = await SharedPreferences.getInstance();
-    var _profile = _prefs.getString("profile");
-    if (_profile != null) {
-      try {
-        profile = Profile.fromJson(jsonDecode(_profile));
-        profile.isLogin = false;
-      } catch (e) {
-        print(e);
+  static get locales => [
+    const Locale('zh'), // 中文简体
+    const Locale('en'), // 美国英语
+    //其它Locales
+  ];
+
+  static Future<bool> checkPermission() async {
+    bool status = await Permission.storage.isGranted;
+    return status ? true : await Permission.storage.request().isGranted;
+  }
+
+  static listenIsLogin() async {
+    while (true) {
+      if (!profile.isLogin && (profile.isLogout || !await UserModel().login())) {
+        debug("offline detected! navigate to login route");
+        await Global.navigatorKey.currentState?.pushNamed("login");
       }
+      await Future.delayed(Duration(seconds: 1));
     }
+  }
+
+  //初始化全局信息,会在APP启动时执行
+  static init() async {
+    debug('Init begin');
+
+    bool status = await checkPermission();
+    assert(status, "permission error");
+
+    Store store = await openStore();
+    var box = store.box<Profile>();
+    profile = box.get(profileId) ?? Profile();
+    store.close();
+    profile.isLogin = false;
+    debug('Init profile: id: ${profile.id}, username: ${profile.username}, token: ${await profile.token}, isLogout: ${profile.isLogout}');
+
     await Api.init();
-    UserModel().login();
+
+    listenIsLogin();
+
+    /*
+    debug('Init login');
+    try {
+      await UserModel().login(); // must await, or can not catch error
+    } on DioError catch(e) {
+      debug('Init login failed');
+    }
+     */
+
+    debug('Global init end');
   }
 
   // 持久化Profile信息
   static saveProfile() async {
-    var _prefs = await SharedPreferences.getInstance();
-    _prefs.setString("profile", jsonEncode(profile.toJson()));
+    debug('save profile: username: ${profile.username}, isLogout: ${profile.isLogout}');
+    Store store = await openStore();
+    var box = store.box<Profile>();
+    box.removeAll();
+    box.put(profile);
+    store.close();
   }
 }

+ 0 - 36
lib/common/profile.dart

@@ -1,36 +0,0 @@
-import 'package:flutter_secure_storage/flutter_secure_storage.dart';
-import 'package:json_annotation/json_annotation.dart';
-
-import 'package:e2ee_chat/common/global.dart';
-
-part 'Profile.g.dart';
-
-@JsonSerializable()
-class Profile {
-  Profile();
-
-  factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
-  Map<String, dynamic> toJson() => _$ProfileToJson(json);
-
-  int theme = Global.themes[0].value;
-  String? username;
-  bool isLogin = false;
-  String locale = "zh";
-
-
-  String? get token {
-    if (username == null) return null;
-    final storage = FlutterSecureStorage();
-    String? token;
-    storage.read(key: _tokenKey).then((v) => token = v);
-    return token;
-  }
-
-  set token(String? token) {
-    assert(username != null, "username == null when setting token");
-    final storage = FlutterSecureStorage();
-    storage.write(key: _tokenKey, value: token);
-  }
-
-  String get _tokenKey => "e2ee_chat token of $username";
-}

+ 34 - 0
lib/entities/profile.dart

@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+
+import 'package:e2ee_chat/common/global.dart';
+import 'package:objectbox/objectbox.dart';
+
+
+@Entity()
+class Profile {
+  Profile();
+
+  int id = 0;
+
+  int theme = Global.themes[0].value;
+  String? username;
+  bool isLogin = false;
+  bool isLogout = false;
+  Locale? locale;
+
+
+  @Transient()
+  Future<String?> get token async {
+    if (username == null) return null;
+    final storage = FlutterSecureStorage();
+    return await storage.read(key: _tokenKey);
+  }
+
+  setToken(String? token) async {
+    final storage = FlutterSecureStorage();
+    return await storage.write(key: _tokenKey, value: token);
+  }
+
+  String get _tokenKey => "e2ee_chat token of $username";
+}

+ 51 - 0
lib/l10n/localization_intl.dart

@@ -0,0 +1,51 @@
+import 'package:e2ee_chat/common/global.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+
+class GmLocalizations {
+
+  static GmLocalizations of(BuildContext context) {
+    return Localizations.of<GmLocalizations>(context, GmLocalizations) ?? GmLocalizations();
+  }
+
+  //TODO: Localizations
+
+  String get title => Intl.message('MeChat', name: 'title');
+
+  String get userName => Intl.message('Username', name: 'username');
+  String get userNameOrEmail => Intl.message('Username or Email', name: 'username or email');
+  String get userNameRequired => Intl.message('Username required', name: 'username required');
+  String get password => Intl.message('Password', name: 'password');
+  String get passwordRequired => Intl.message('PasswordRequired', name: 'password required');
+  String get loading => Intl.message('Loading...', name: 'loading');
+  String get userNameOrPasswordWrong => Intl.message('Username or password wrong', name: 'username or password wrong');
+  String get home => Intl.message('Home', name: 'home');
+  String get login => Intl.message('Login', name: 'login');
+  String get logout => Intl.message('Logout', name: 'logout');
+  String get language => Intl.message('Language', name: 'language');
+  String get auto => Intl.message('Auto', name: 'auto');
+  String get theme => Intl.message('Theme', name: 'theme');
+}
+
+//Locale代理类
+class GmLocalizationsDelegate extends LocalizationsDelegate<GmLocalizations> {
+  const GmLocalizationsDelegate();
+
+  //是否支持某个Local
+  @override
+  bool isSupported(Locale locale) => Global.locales.contains(locale.languageCode);
+
+  // Flutter会调用此类加载相应的Locale资源类
+  @override
+  Future<GmLocalizations> load(Locale locale) {
+    print("$locale");
+    return SynchronousFuture<GmLocalizations>(
+        GmLocalizations()
+    );
+  }
+
+  // 当Localizations Widget重新build时,是否调用load重新加载Locale资源.
+  @override
+  bool shouldReload(GmLocalizationsDelegate old) => false;
+}

+ 10 - 14
lib/main.dart

@@ -1,3 +1,4 @@
+import 'package:e2ee_chat/objectbox.g.dart';
 import 'package:e2ee_chat/routes/home.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
@@ -5,6 +6,7 @@ import 'package:provider/single_child_widget.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
 
 import 'common/global.dart';
+import 'l10n/localization_intl.dart';
 import 'models/locale_model.dart';
 import 'models/theme_model.dart';
 import 'models/user_model.dart';
@@ -13,6 +15,7 @@ import 'routes/login.dart';
 import 'routes/theme_change.dart';
 
 void main() {
+  WidgetsFlutterBinding.ensureInitialized();
   Global.init().then((e) => runApp(MyApp()));
 }
 
@@ -23,26 +26,20 @@ class MyApp extends StatelessWidget {
     return MultiProvider(
       providers: <SingleChildWidget>[
         ChangeNotifierProvider.value(value: ThemeModel()),
-        ChangeNotifierProvider.value(value: UserModel()),
         ChangeNotifierProvider.value(value: LocaleModel()),
+        ChangeNotifierProvider.value(value: UserModel()),
       ],
       child: Consumer2<ThemeModel, LocaleModel>(
         builder: (BuildContext context, themeModel, localeModel, Widget? child) {
           return MaterialApp(
+            navigatorKey: Global.navigatorKey,
             theme: ThemeData(
               primarySwatch: themeModel.theme,
             ),
-            onGenerateTitle: (context){
-              return GmLocalizations.of(context).title;
-            },
             home: HomeRoute(), //应用主页
-            locale: localeModel.getLocale(),
+            locale: localeModel.locale,
             //我们只支持美国英语和中文简体
-            supportedLocales: [
-              const Locale('en', 'US'), // 美国英语
-              const Locale('zh', 'CN'), // 中文简体
-              //其它Locales
-            ],
+            supportedLocales: Global.locales,
             localizationsDelegates: [
               // 本地化的代理类
               GlobalMaterialLocalizations.delegate,
@@ -51,18 +48,17 @@ class MyApp extends StatelessWidget {
             ],
             localeResolutionCallback:
                 (Locale? _locale, Iterable<Locale> supportedLocales) {
-              if (localeModel.getLocale() != null) {
+              if (localeModel.locale != null) {
                 //如果已经选定语言,则不跟随系统
-                return localeModel.getLocale();
+                return localeModel.locale;
               } else {
-
                 Locale? locale;
                 //APP语言跟随系统语言,如果系统语言不是中文简体或美国英语,
                 //则默认使用美国英语
                 if (supportedLocales.contains(_locale)) {
                   locale = _locale;
                 } else {
-                  locale = Locale('en', 'US');
+                  locale = Locale('en');
                 }
                 return locale;
               }

+ 2 - 4
lib/models/locale_model.dart

@@ -4,11 +4,9 @@ import 'package:flutter/material.dart';
 class LocaleModel extends ProfileChangeNotifier {
   LocaleModel();
 
-  String get locale => profile.locale;
+  Locale? get locale => profile.locale;
 
-  Locale? getLocale() => Locale(locale);
-
-  set locale(String value) {
+  set locale(Locale? value) {
     profile.locale = value;
     notifyListeners();
   }

+ 1 - 1
lib/models/profile.dart

@@ -1,4 +1,4 @@
-import 'package:e2ee_chat/common/profile.dart';
+import 'package:e2ee_chat/entities/profile.dart';
 import 'package:flutter/cupertino.dart';
 
 import '../common/global.dart';

+ 41 - 21
lib/models/user_model.dart

@@ -1,35 +1,55 @@
-import 'package:e2ee_chat/network/api.dart';
+import 'package:dio/dio.dart';
+import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/common/api.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
 
 import 'profile.dart';
 
 class UserModel extends ProfileChangeNotifier {
 
-  bool get isLogin => profile.isLogin;
+  /*
+  UserModel._();
 
-  _login(String token) {
-    profile.token = token;
-    profile.isLogin = true;
-    notifyListeners();
-  }
+  static UserModel _instance = UserModel._();
 
-  Future<void> login({String? username, String? password}) async {
-    String? token;
-    if (username != null && password != null) {
-      token = await Api().login(username: username, password: password);
-    } else if (!profile.isLogin && profile.username != null) {
-      if (profile.token != null) {
-        token = await Api().login(username: profile.username!, token: profile.token);
+  factory UserModel() => _instance!;
+   */
+
+  // TODO: online keep status; A logout, B logout?
+  bool get isLogin => profile.isLogin;
+
+  Future<bool> login({String? username, String? password}) async {
+    debug('UserModel login begin');
+    profile.username = username ?? profile.username;
+    bool _result = false;
+    String? token = await profile.token;
+    if (profile.username != null && (password != null || token != null)) {
+      String? _token;
+      try {
+        _token = await Api().login(username: profile.username!, password: password, token: token);
+        _result = _token != null;
+      } on DioError catch(e) {
+        print(e);
+      }
+      profile.setToken(_token);
+      debug('UserModel login get token: ${await profile.token}');
+      if (await profile.token != null) {
+        debug('UserModel login success!');
+        profile.isLogin = true;
+        profile.isLogout = false;
+        notifyListeners();
       }
     }
-    if (token != null) {
-      _login(token);
-    }
+    debug('UserModel login end');
+    return _result;
   }
 
-  Future<bool> logout() async {
-    if (profile.isLogin) {
-      // TODO: logout
+  Future<void> logout() async {
+    if (profile.isLogin && await Api().logout()) {
+      profile.isLogin = false;
+      profile.isLogout = true;
+      notifyListeners();
     }
-    return false;
   }
 }

+ 0 - 31
lib/network/api.dart

@@ -1,31 +0,0 @@
-import 'dart:convert';
-import 'dart:io';
-
-import 'package:crypto/crypto.dart';
-import 'package:dio/dio.dart';
-import 'package:e2ee_chat/common/global.dart';
-
-class Api {
-  Api();
-
-  static Dio dio = Dio(BaseOptions(
-    baseUrl: 'http://127.0.0.1:8000/',
-  ));
-
-  static init() async {
-  }
-
-  Future<String?> login({required String username, String? password, String? token}) async {
-    var data = {"username": username, "token": token ?? Global.emptyToken, "password": password};
-    String _password = "";
-    if (password != null) {
-      var bytes = utf8.encode(password);
-      var digest = sha1.convert(bytes);
-      _password = digest.toString();
-    }
-    var r = await dio.post("/account/login", data: data,
-      // options: _options,
-    );
-    return r.data["token"];
-  }
-}

+ 26 - 11
lib/objectbox-model.json

@@ -4,36 +4,51 @@
   "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
   "entities": [
     {
-      "id": "1:3444477729893015694",
-      "lastPropertyId": "2:2305308568754167021",
-      "name": "User",
+      "id": "2:1438354990151910015",
+      "lastPropertyId": "6:4818206049188692305",
+      "name": "Profile",
       "properties": [
         {
-          "id": "1:5463014948149082651",
+          "id": "1:3873481118333862410",
           "name": "id",
           "type": 6,
           "flags": 1
         },
         {
-          "id": "2:2305308568754167021",
+          "id": "2:9078004558710481468",
+          "name": "theme",
+          "type": 6
+        },
+        {
+          "id": "3:7647214962273172849",
           "name": "username",
-          "type": 9,
-          "flags": 2080,
-          "indexId": "1:2185831944762227781"
+          "type": 9
+        },
+        {
+          "id": "4:5923665807684456265",
+          "name": "isLogin",
+          "type": 1
         }
       ],
       "relations": []
     }
   ],
-  "lastEntityId": "1:3444477729893015694",
+  "lastEntityId": "2:1438354990151910015",
   "lastIndexId": "1:2185831944762227781",
   "lastRelationId": "0:0",
   "lastSequenceId": "0:0",
   "modelVersion": 5,
   "modelVersionParserMinimum": 5,
-  "retiredEntityUids": [],
+  "retiredEntityUids": [
+    3444477729893015694
+  ],
   "retiredIndexUids": [],
-  "retiredPropertyUids": [],
+  "retiredPropertyUids": [
+    5463014948149082651,
+    2305308568754167021,
+    649621747167423523,
+    4818206049188692305
+  ],
   "retiredRelationUids": [],
   "version": 1
 }

+ 60 - 30
lib/objectbox.g.dart

@@ -9,28 +9,37 @@ import 'package:objectbox/internal.dart'; // generated code can access "internal
 import 'package:objectbox/objectbox.dart';
 import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart';
 
-import 'entity/login.dart';
+import 'entities/profile.dart';
 
 export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file
 
 final _entities = <ModelEntity>[
   ModelEntity(
-      id: const IdUid(1, 3444477729893015694),
-      name: 'User',
-      lastPropertyId: const IdUid(2, 2305308568754167021),
+      id: const IdUid(2, 1438354990151910015),
+      name: 'Profile',
+      lastPropertyId: const IdUid(6, 4818206049188692305),
       flags: 0,
       properties: <ModelProperty>[
         ModelProperty(
-            id: const IdUid(1, 5463014948149082651),
+            id: const IdUid(1, 3873481118333862410),
             name: 'id',
             type: 6,
             flags: 1),
         ModelProperty(
-            id: const IdUid(2, 2305308568754167021),
+            id: const IdUid(2, 9078004558710481468),
+            name: 'theme',
+            type: 6,
+            flags: 0),
+        ModelProperty(
+            id: const IdUid(3, 7647214962273172849),
             name: 'username',
             type: 9,
-            flags: 2080,
-            indexId: const IdUid(1, 2185831944762227781))
+            flags: 0),
+        ModelProperty(
+            id: const IdUid(4, 5923665807684456265),
+            name: 'isLogin',
+            type: 1,
+            flags: 0)
       ],
       relations: <ModelRelation>[],
       backlinks: <ModelBacklink>[])
@@ -56,32 +65,41 @@ Future<Store> openStore(
 ModelDefinition getObjectBoxModel() {
   final model = ModelInfo(
       entities: _entities,
-      lastEntityId: const IdUid(1, 3444477729893015694),
+      lastEntityId: const IdUid(2, 1438354990151910015),
       lastIndexId: const IdUid(1, 2185831944762227781),
       lastRelationId: const IdUid(0, 0),
       lastSequenceId: const IdUid(0, 0),
-      retiredEntityUids: const [],
+      retiredEntityUids: const [3444477729893015694],
       retiredIndexUids: const [],
-      retiredPropertyUids: const [],
+      retiredPropertyUids: const [
+        5463014948149082651,
+        2305308568754167021,
+        649621747167423523,
+        4818206049188692305
+      ],
       retiredRelationUids: const [],
       modelVersion: 5,
       modelVersionParserMinimum: 5,
       version: 1);
 
   final bindings = <Type, EntityDefinition>{
-    Login: EntityDefinition<Login>(
+    Profile: EntityDefinition<Profile>(
         model: _entities[0],
-        toOneRelations: (Login object) => [],
-        toManyRelations: (Login object) => {},
-        getId: (Login object) => object.id,
-        setId: (Login object, int id) {
+        toOneRelations: (Profile object) => [],
+        toManyRelations: (Profile object) => {},
+        getId: (Profile object) => object.id,
+        setId: (Profile object, int id) {
           object.id = id;
         },
-        objectToFB: (Login object, fb.Builder fbb) {
-          final usernameOffset = fbb.writeString(object.username);
-          fbb.startTable(3);
+        objectToFB: (Profile object, fb.Builder fbb) {
+          final usernameOffset = object.username == null
+              ? null
+              : fbb.writeString(object.username!);
+          fbb.startTable(7);
           fbb.addInt64(0, object.id);
-          fbb.addOffset(1, usernameOffset);
+          fbb.addInt64(1, object.theme);
+          fbb.addOffset(2, usernameOffset);
+          fbb.addBool(3, object.isLogin);
           fbb.finish(fbb.endTable());
           return object.id;
         },
@@ -89,10 +107,13 @@ ModelDefinition getObjectBoxModel() {
           final buffer = fb.BufferContext(fbData);
           final rootOffset = buffer.derefObject(0);
 
-          final object = Login(
-              username:
-                  const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''))
-            ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
+          final object = Profile()
+            ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0)
+            ..theme = const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0)
+            ..username =
+                const fb.StringReader().vTableGetNullable(buffer, rootOffset, 8)
+            ..isLogin =
+                const fb.BoolReader().vTableGet(buffer, rootOffset, 10, false);
 
           return object;
         })
@@ -101,11 +122,20 @@ ModelDefinition getObjectBoxModel() {
   return ModelDefinition(model, bindings);
 }
 
-/// [Login] entity fields to define ObjectBox queries.
-class User_ {
-  /// see [Login.id]
-  static final id = QueryIntegerProperty<Login>(_entities[0].properties[0]);
+/// [Profile] entity fields to define ObjectBox queries.
+class Profile_ {
+  /// see [Profile.id]
+  static final id = QueryIntegerProperty<Profile>(_entities[0].properties[0]);
+
+  /// see [Profile.theme]
+  static final theme =
+      QueryIntegerProperty<Profile>(_entities[0].properties[1]);
+
+  /// see [Profile.username]
+  static final username =
+      QueryStringProperty<Profile>(_entities[0].properties[2]);
 
-  /// see [Login.username]
-  static final username = QueryStringProperty<Login>(_entities[0].properties[1]);
+  /// see [Profile.isLogin]
+  static final isLogin =
+      QueryBooleanProperty<Profile>(_entities[0].properties[3]);
 }

+ 42 - 34
lib/routes/home.dart

@@ -1,8 +1,10 @@
 import 'package:e2ee_chat/common/global.dart';
-import 'package:e2ee_chat/network/api.dart';
+import 'package:e2ee_chat/l10n/localization_intl.dart';
+import 'package:e2ee_chat/common/api.dart';
+import 'package:e2ee_chat/models/theme_model.dart';
 import 'package:e2ee_chat/models/user_model.dart';
 import 'package:e2ee_chat/widgets/empty.dart';
-import 'package:flukit/flukit.dart';
+import 'package:e2ee_chat/widgets/mydrawer.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 
@@ -12,50 +14,56 @@ class HomeRoute extends StatefulWidget {
 }
 
 class _HomeRouteState extends State<HomeRoute> {
+
+  int _index = 0;
+
+  @override
+  void initState() {
+    super.initState();
+  }
+
   @override
   Widget build(BuildContext context) {
+    var gm = GmLocalizations.of(context);
     return Scaffold(
       appBar: AppBar(
-        title: Text(GmLocalizations.of(context).home),
+        title: Text(gm.home),
+        actions: <Widget>[
+          IconButton(onPressed: () {
+            Scaffold.of(context).openDrawer();
+          }, icon: Icon(Icons.add))
+        ],
+      ),
+      bottomNavigationBar: BottomNavigationBar(
+        items: [
+          BottomNavigationBarItem(icon: Icon(Icons.chat_bubble), label: "message"),
+          BottomNavigationBarItem(icon: Icon(Icons.people), label: "friends"),
+          BottomNavigationBarItem(icon: Icon(Icons.person), label: "mine"),
+        ],
+        currentIndex: _index,
+        fixedColor: Provider.of<ThemeModel>(context).theme,
+        onTap: _onItemTapped,
       ),
       body: _buildBody(), // 构建主页面
     );
   }
 
   Widget _buildBody() {
-    UserModel userModel = Provider.of<UserModel>(context);
-    if (!userModel.isLogin) {
-      //用户未登录,显示登录按钮
+    if (_index == 2) {
+      UserModel userModel = Provider.of<UserModel>(context);
       return Center(
         child: ElevatedButton(
-          child: Text(GmLocalizations.of(context).login),
-          onPressed: () => Navigator.of(context).pushNamed("login"),
-        ),
-      );
-    } else {
-      return EmptyWidget();
-      //已登录,则展示项目列表
-      /*
-      return InfiniteListView<Repo>(
-        onRetrieveData: (int page, List<Repo> items, bool refresh) async {
-          var data = await Api(context).getRepos(
-            refresh: refresh,
-            queryParameters: {
-              'page': page,
-              'page_size': 20,
-            },
-          );
-          //把请求到的新数据添加到items中
-          items.addAll(data);
-          // 如果接口返回的数量等于'page_size',则认为还有数据,反之则认为最后一页
-          return data.length==20;
-        },
-        itemBuilder: (List list, int index, BuildContext ctx) {
-          // 项目信息列表项
-          return RepoItem(list[index]);
-        },
-      );
-       */
+          child: Text(GmLocalizations.of(context).logout),
+          onPressed: () => userModel.logout()
+          ,)
+        ,);
     }
+    return EmptyWidget();
+  }
+
+  void _onItemTapped(int value) {
+    setState(() {
+      _index = value;
+    });
   }
 }

+ 3 - 3
lib/routes/language_route.dart

@@ -1,3 +1,4 @@
+import 'package:e2ee_chat/l10n/localization_intl.dart';
 import 'package:e2ee_chat/models/locale_model.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
@@ -7,7 +8,6 @@ class LanguageRoute extends StatelessWidget {
   Widget build(BuildContext context) {
     var color = Theme.of(context).primaryColor;
     var localeModel = Provider.of<LocaleModel>(context);
-    var gm = GmLocalizations.of(context);
     //构建语言选择项
     Widget _buildLanguageItem(String lan, value) {
       return ListTile(
@@ -27,13 +27,13 @@ class LanguageRoute extends StatelessWidget {
 
     return Scaffold(
       appBar: AppBar(
-        title: Text(gm.language),
+        title: Text(GmLocalizations.of(context).language),
       ),
       body: ListView(
         children: <Widget>[
           _buildLanguageItem("中文简体", "zh_CN"),
           _buildLanguageItem("English", "en_US"),
-          _buildLanguageItem(gm.auto, null),
+          _buildLanguageItem(GmLocalizations.of(context).auto, null),
         ],
       ),
     );

+ 17 - 14
lib/routes/login.dart

@@ -1,5 +1,6 @@
 import 'package:dio/dio.dart';
 import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/l10n/localization_intl.dart';
 import 'package:e2ee_chat/models/theme_model.dart';
 import 'package:e2ee_chat/models/user_model.dart';
 import 'package:flutter/material.dart';
@@ -30,9 +31,8 @@ class _LoginRouteState extends State<LoginRoute> {
 
   @override
   Widget build(BuildContext context) {
-    var gm = GmLocalizations.of(context);
     return Scaffold(
-      appBar: AppBar(title: Text(gm.login)),
+      appBar: AppBar(title: Text(GmLocalizations.of(context).login)),
       body: Padding(
         padding: const EdgeInsets.all(16.0),
         child: Form(
@@ -44,20 +44,20 @@ class _LoginRouteState extends State<LoginRoute> {
                   autofocus: _nameAutoFocus,
                   controller: _unameController,
                   decoration: InputDecoration(
-                    labelText: gm.userName,
-                    hintText: gm.userNameOrEmail,
+                    labelText: GmLocalizations.of(context).userName,
+                    hintText: GmLocalizations.of(context).userNameOrEmail,
                     prefixIcon: Icon(Icons.person),
                   ),
                   // 校验用户名(不能为空)
                   validator: (v) {
-                    return v!.trim().isNotEmpty ? null : gm.userNameRequired;
+                    return v!.trim().isNotEmpty ? null : GmLocalizations.of(context).userNameRequired;
                   }),
               TextFormField(
                 controller: _pwdController,
                 autofocus: !_nameAutoFocus,
                 decoration: InputDecoration(
-                    labelText: gm.password,
-                    hintText: gm.password,
+                    labelText: GmLocalizations.of(context).password,
+                    hintText: GmLocalizations.of(context).password,
                     prefixIcon: Icon(Icons.lock),
                     suffixIcon: IconButton(
                       icon: Icon(
@@ -71,7 +71,7 @@ class _LoginRouteState extends State<LoginRoute> {
                 obscureText: !pwdShow,
                 //校验密码(不能为空)
                 validator: (v) {
-                  return v!.trim().isNotEmpty ? null : gm.passwordRequired;
+                  return v!.trim().isNotEmpty ? null : GmLocalizations.of(context).passwordRequired;
                 },
               ),
               Padding(
@@ -83,7 +83,7 @@ class _LoginRouteState extends State<LoginRoute> {
                       foregroundColor: MaterialStateProperty.all<Color>(Provider.of<ThemeModel>(context).theme),
                     ),
                     onPressed: _onLogin,
-                    child: Text(gm.login),
+                    child: Text(GmLocalizations.of(context).login),
                   ),
                 ),
               ),
@@ -101,7 +101,8 @@ class _LoginRouteState extends State<LoginRoute> {
       try {
         // user = await Git(context).login(_unameController.text, _pwdController.text);
         // 因为登录页返回后,首页会build,所以我们传false,更新user后不触发更新
-        Provider.of<UserModel>(context).login(username: _unameController.text, password: _pwdController.text);
+        await UserModel().login(username: _unameController.text, password: _pwdController.text);
+        debug('isLogin: ${Global.profile.isLogin}');
       } on DioError catch(e) {
         //登录失败则提示
         if (e.response?.statusCode == 401) {
@@ -109,10 +110,12 @@ class _LoginRouteState extends State<LoginRoute> {
         } else {
           Fluttertoast.showToast(msg: e.toString());
         }
-      }
-      if (Global.profile.isLogin) {
-        // 返回
-        Navigator.of(context).pop();
+      } finally {
+        if (Global.profile.isLogin) {
+          debug('Login!');
+          // 返回
+          Navigator.of(context).pop();
+        }
       }
     }
   }

+ 2 - 0
lib/routes/theme_change.dart

@@ -1,11 +1,13 @@
 
 
 import 'package:e2ee_chat/common/global.dart';
+import 'package:e2ee_chat/l10n/localization_intl.dart';
 import 'package:e2ee_chat/models/theme_model.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 
 class ThemeChangeRoute extends StatelessWidget{
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(

+ 57 - 0
lib/widgets/mydrawer.dart

@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+
+class MyDrawer extends StatelessWidget {
+  const MyDrawer({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Drawer(
+      child: MediaQuery.removePadding(
+        context: context,
+        //移除抽屉菜单顶部默认留白
+        removeTop: true,
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: <Widget>[
+            Padding(
+              padding: const EdgeInsets.only(top: 38.0),
+              child: Row(
+                children: <Widget>[
+                  Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
+                    child: ClipOval(
+                      child: Image.asset(
+                        "imgs/avatar.png",
+                        width: 80,
+                      ),
+                    ),
+                  ),
+                  Text(
+                    "Wendux",
+                    style: TextStyle(fontWeight: FontWeight.bold),
+                  )
+                ],
+              ),
+            ),
+            Expanded(
+              child: ListView(
+                children: <Widget>[
+                  ListTile(
+                    leading: const Icon(Icons.add),
+                    title: const Text('Add account'),
+                  ),
+                  ListTile(
+                    leading: const Icon(Icons.settings),
+                    title: const Text('Manage accounts'),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 28 - 63
pubspec.lock

@@ -49,14 +49,14 @@ packages:
       name: build_config
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "0.4.7"
+    version: "1.0.0"
   build_daemon:
     dependency: transitive
     description:
       name: build_daemon
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "2.1.10"
+    version: "3.0.0"
   build_resolvers:
     dependency: transitive
     description:
@@ -70,14 +70,14 @@ packages:
       name: build_runner
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.12.2"
+    version: "2.0.5"
   build_runner_core:
     dependency: transitive
     description:
       name: build_runner_core
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "6.1.12"
+    version: "7.0.0"
   built_collection:
     dependency: transitive
     description:
@@ -133,7 +133,7 @@ packages:
       name: code_builder
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "3.7.0"
+    version: "4.0.0"
   collection:
     dependency: transitive
     description:
@@ -204,13 +204,6 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
-  flukit:
-    dependency: "direct main"
-    description:
-      name: flukit
-      url: "https://pub.flutter-io.cn"
-    source: hosted
-    version: "1.0.2"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -245,6 +238,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "8.0.7"
+  frontend_server_client:
+    dependency: transitive
+    description:
+      name: frontend_server_client
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.0"
   glob:
     dependency: transitive
     description:
@@ -258,7 +258,7 @@ packages:
       name: graphs
       url: "https://pub.flutter-io.cn"
     source: hosted
-    version: "1.0.0"
+    version: "2.0.0"
   http_multi_server:
     dependency: transitive
     description:
@@ -274,7 +274,7 @@ packages:
     source: hosted
     version: "4.0.0"
   intl:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: intl
       url: "https://pub.flutter-io.cn"
@@ -301,13 +301,6 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "4.0.1"
-  json_serializable:
-    dependency: "direct dev"
-    description:
-      name: json_serializable
-      url: "https://pub.flutter-io.cn"
-    source: hosted
-    version: "4.1.3"
   logging:
     dependency: transitive
     description:
@@ -420,6 +413,20 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.11.1"
+  permission_handler:
+    dependency: "direct main"
+    description:
+      name: permission_handler
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "8.1.2"
+  permission_handler_platform_interface:
+    dependency: transitive
+    description:
+      name: permission_handler_platform_interface
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.6.0"
   platform:
     dependency: transitive
     description:
@@ -469,48 +476,6 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
-  shared_preferences:
-    dependency: "direct main"
-    description:
-      name: shared_preferences
-      url: "https://pub.flutter-io.cn"
-    source: hosted
-    version: "2.0.6"
-  shared_preferences_linux:
-    dependency: transitive
-    description:
-      name: shared_preferences_linux
-      url: "https://pub.flutter-io.cn"
-    source: hosted
-    version: "2.0.0"
-  shared_preferences_macos:
-    dependency: transitive
-    description:
-      name: shared_preferences_macos
-      url: "https://pub.flutter-io.cn"
-    source: hosted
-    version: "2.0.0"
-  shared_preferences_platform_interface:
-    dependency: transitive
-    description:
-      name: shared_preferences_platform_interface
-      url: "https://pub.flutter-io.cn"
-    source: hosted
-    version: "2.0.0"
-  shared_preferences_web:
-    dependency: transitive
-    description:
-      name: shared_preferences_web
-      url: "https://pub.flutter-io.cn"
-    source: hosted
-    version: "2.0.0"
-  shared_preferences_windows:
-    dependency: transitive
-    description:
-      name: shared_preferences_windows
-      url: "https://pub.flutter-io.cn"
-    source: hosted
-    version: "2.0.0"
   shelf:
     dependency: transitive
     description:

+ 4 - 6
pubspec.yaml

@@ -27,8 +27,6 @@ dependencies:
     sdk: flutter
   fluttertoast: ^8.0.7
   provider: ^5.0.0
-
-  shared_preferences: ^2.0.6
   flutter_secure_storage: ^4.2.0
   objectbox: ^1.0.0
   objectbox_flutter_libs: any
@@ -37,19 +35,19 @@ dependencies:
 
   dio: ^4.0.0
   crypto: ^3.0.1
-
-  flukit: ^1.0.2
+  permission_handler: ^8.1.2
 
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.2
 
+  intl: ^0.17.0
+
 dev_dependencies:
   flutter_test:
     sdk: flutter
-  build_runner: ^1.0.0
+  build_runner: ^2.0.5
   objectbox_generator: any
-  json_serializable: ^4.1.3
 
 # For information on the generic Dart part of this file, see the
 # following page: https://dart.dev/tools/pub/pubspec