Browse Source

Merge pull request #857 from colinin/flutter

feat(flutter): add menu drawer
pull/860/head
yx lin 2 years ago
committed by GitHub
parent
commit
ecdf1741c5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      apps/flutter/components/lib/widgets/menu/index.dart
  2. 2
      apps/flutter/core/lib/models/environment.dart
  3. 6
      apps/flutter/core/lib/models/environment.g.dart
  4. 6
      apps/flutter/dev_app/.gitignore
  5. 13
      apps/flutter/dev_app/lib/pages/main/controller.dart
  6. 14
      apps/flutter/dev_app/lib/pages/public/home/controller.dart
  7. 3
      apps/flutter/dev_app/lib/pages/public/home/state.dart
  8. 143
      apps/flutter/dev_app/lib/pages/public/home/view.dart
  9. 5
      apps/flutter/dev_app/lib/pages/public/home/widget/index.dart
  10. 76
      apps/flutter/dev_app/lib/pages/public/home/widget/menu_drawer.dart
  11. 42
      apps/flutter/dev_app/lib/pages/public/home/widget/my_favorite.dart
  12. 55
      apps/flutter/dev_app/lib/pages/public/home/widget/notification_bar.dart
  13. 33
      apps/flutter/dev_app/lib/pages/public/home/widget/quick_navigation.dart
  14. 17
      apps/flutter/dev_app/lib/services/notification.send.local.service.dart
  15. 24
      apps/flutter/dev_app/lib/services/translation.service.res.service.dart
  16. 14
      apps/flutter/dev_app/res/config/demo.json
  17. BIN
      apps/flutter/dev_app/res/images/notification.png
  18. 1
      apps/flutter/notifications/lib/models/notification.state.dart
  19. 11
      apps/flutter/notifications/lib/services/notification.state.service.dart
  20. 2
      apps/flutter/platform/lib/services/favorite.menu.state.service.dart
  21. 2
      apps/flutter/platform/lib/services/menu.state.service.dart

11
apps/flutter/components/lib/widgets/menu/index.dart

@ -21,11 +21,7 @@ class Navigation extends StatefulWidget {
class _NavigationState extends State<Navigation> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
child: _renderNavigations(widget.menus),
),
);
return _renderNavigations(widget.menus);
}
Widget _renderNavigations(List<Menu> menus) {
@ -59,7 +55,8 @@ class _NavigationState extends State<Navigation> {
return SizedBox(
height: 30,
child: ListTile(
title: Text(menu.displayName.padLeft(menu.level * 4)),
title: Text(
(menu.meta?['displayName']?.toString().tr ?? menu.displayName).padLeft(menu.level * 4)),
),
);
},
@ -81,7 +78,7 @@ class _NavigationState extends State<Navigation> {
height: 30,
margin: const EdgeInsets.only(top: 10),
child: Text(
menu.displayName.padLeft(menu.level * 8),
(menu.meta?['displayName']?.toString().tr ?? menu.displayName).padLeft(menu.level * 8),
),
),
),

2
apps/flutter/core/lib/models/environment.dart

@ -177,10 +177,12 @@ class LocalizationConfig {
this.defaultLanguage,
this.useLocalResources = true,
this.supportedLocales = const [],
this.translationFiles = const {},
});
String? defaultLanguage;
bool? useLocalResources;
List<LanguageInfo>? supportedLocales;
Map<String, List<String>>? translationFiles;
factory LocalizationConfig.fromJson(Map<String, dynamic> json) => _$LocalizationConfigFromJson(json);
Map<String, dynamic> toJson() => _$LocalizationConfigToJson(this);

6
apps/flutter/core/lib/models/environment.g.dart

@ -102,6 +102,11 @@ LocalizationConfig _$LocalizationConfigFromJson(Map<String, dynamic> json) =>
supportedLocales: json['supportedLocales'] != null
? (json['supportedLocales'] as List<dynamic>).map((e) => LanguageInfo.fromJson(e)).toList()
: null,
translationFiles: json['translationFiles'] != null
? (json['translationFiles'] as Map<String, dynamic>)
.map((key, value) => MapEntry(key, (value as List<dynamic>)
.map((e) => e as String).toList()))
: null,
);
Map<String, dynamic> _$LocalizationConfigToJson(LocalizationConfig instance) =>
@ -109,6 +114,7 @@ Map<String, dynamic> _$LocalizationConfigToJson(LocalizationConfig instance) =>
'defaultLanguage': instance.defaultLanguage,
'useLocalResources': instance.useLocalResources,
'supportedLocales': instance.supportedLocales,
'translationFiles': instance.translationFiles,
};
RemoteService _$RemoteServiceFromJson(Map<String, dynamic> json) =>

6
apps/flutter/dev_app/.gitignore

@ -44,4 +44,8 @@ app.*.map.json
/android/app/release
# Environment config
/res/config/development.json
/res/config/development.json
# Ignored translations
/res/translations/merge-en.json
/res/translations/merge-zh-Hans.json

13
apps/flutter/dev_app/lib/pages/main/controller.dart

@ -1,13 +1,13 @@
import 'package:core/dependency/index.dart';
import 'package:core/abstracts/signalr.service.dart';
import 'package:core/services/environment.service.dart';
import 'package:core/services/notification.send.service.dart';
import 'package:dev_app/handlers/index.dart';
import 'package:get/get.dart';
import 'package:core/services/session.service.dart';
import 'package:core/services/subscription.service.dart';
import 'package:core/utils/index.dart';
import 'package:notifications/models/index.dart';
import 'package:notifications/services/notification.state.service.dart';
import 'package:notifications/tokens/index.dart';
class MainController extends GetxController {
@ -17,7 +17,7 @@ class MainController extends GetxController {
SessionService get _sessionService => injector.get();
SubscriptionService get _subscriptionService => injector.get(tag: NotificationTokens.consumer);
SignalrService get _signalrService => injector.get(tag: NotificationTokens.producer);
NotificationSendService get _notificationSendService => injector.get();
NotificationStateService get _notificationStateService => injector.get();
EnvironmentService get _environmentService => injector.get();
ErrorHandler get _errorHandler => injector.get();
@ -37,14 +37,7 @@ class MainController extends GetxController {
if (data == null) continue;
//
var notification = NotificationInfo.fromJson(data as dynamic);
//
var payload = NotificationPaylod.fromNotification(notification);
//
await _notificationSendService.send(
payload.title,
payload.body,
payload.payload,
);
_notificationStateService.addNotification(notification);
}
},
);

14
apps/flutter/dev_app/lib/pages/public/home/controller.dart

@ -1,6 +1,7 @@
import 'package:core/models/common.dart';
import 'package:get/get.dart';
import 'package:core/dependency/index.dart';
import 'package:notifications/services/notification.state.service.dart';
import 'package:platforms/services/index.dart';
import 'state.dart';
@ -8,6 +9,7 @@ import 'state.dart';
class HomeController extends GetxController {
MenuStateService get _menuStateService => injector.get<MenuStateService>();
FavoriteMenuStateService get _favoriteMenuStateService => injector.get<FavoriteMenuStateService>();
NotificationStateService get _notificationStateService => injector.get<NotificationStateService>();
final Rx<HomeState> _state = Rx<HomeState>(HomeState());
HomeState get state => _state.value;
@ -27,6 +29,18 @@ class HomeController extends GetxController {
val?.favoriteMenus = menus;
});
});
_notificationStateService.getNotifications$()
.listen((payload) {
var notifications = state.notifications.reversed.take(5).toList();
notifications.add(payload);
_state.update((val) {
val?.notifications = notifications;
});
});
}
Future<void> refreshMenus() async {
await _menuStateService.refreshState();
}
void redirectToRoute(String route) {

3
apps/flutter/dev_app/lib/pages/public/home/state.dart

@ -1,4 +1,5 @@
import 'package:core/models/common.dart';
import 'package:notifications/models/common.dart';
import 'package:platforms/modes/menu.dto.dart';
class HomeState {
@ -6,10 +7,12 @@ class HomeState {
this.activedMenu,
this.menus = const [],
this.favoriteMenus = const [],
this.notifications = const [],
});
String? activedMenu;
List<MenuDto> menus;
List<UserFavoriteMenuDto> favoriteMenus;
List<NotificationPaylod> notifications;
List<Menu> getMenus() => _buildTreeRecursive(menus, null, 0);

143
apps/flutter/dev_app/lib/pages/public/home/view.dart

@ -1,13 +1,13 @@
import 'package:account/pages/route.name.dart';
import 'package:components/index.dart';
import 'package:core/utils/index.dart';
import 'package:dev_app/pages/public/home/widget/search.dart';
import 'package:dev_app/pages/system/route.name.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
import './widget/index.dart';
class HomePage extends BasePage<HomeController> {
const HomePage({super.key});
@ -35,114 +35,47 @@ class HomePage extends BasePage<HomeController> {
),
body: ListView(
children: [
ExpansionTile(
initiallyExpanded: true,
title: Text('Label:QuickNavigation'.tr,
style: Theme.of(context).textTheme.titleMedium,
),
children: [
SizedBox(
height: 120,
child: GridView.count(
shrinkWrap: true,
crossAxisCount: 4,
crossAxisSpacing: 5,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildMenu(
SystemRoutes.settings,
SystemRoutes.settings,
icon: 'res/images/setting.png',
displayName: "Label:SystemSettings".tr,
color: Colors.red.hex),
_buildMenu(
AccountRoutes.profile,
AccountRoutes.profile,
icon: 'res/images/profile.png',
displayName: "Page:UserProfile".tr,
color: const Color.fromARGB(255, 68, 160, 206).hex),
],
),
),
],
),
ExpansionTile(
initiallyExpanded: true,
title: Text('Label:MyFavorite'.tr,
style: Theme.of(context).textTheme.titleMedium,
),
children: [
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: bloc.state.favoriteMenus.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 5,
),
itemBuilder: (BuildContext context, int index) {
if (index >= bloc.state.favoriteMenus.length) {
return Empty.none;
}
var favoriteMenu = bloc.state.favoriteMenus[index];
return _buildMenu(
favoriteMenu.name,
favoriteMenu.path,
aliasName: favoriteMenu.aliasName,
//icon: favoriteMenu.icon,
// TODO:
icon: 'res/images/setting.png',
color: favoriteMenu.color,
displayName: favoriteMenu.displayName,
);
},
),
],
Obx(() => NotificationBar(notifications: bloc.state.notifications)),
QuickNavigation(
menus: [
_buildMenu(
SystemRoutes.settings,
SystemRoutes.settings,
icon: 'res/images/setting.png',
displayName: "Label:SystemSettings".tr,
color: Colors.red.hex),
_buildMenu(
AccountRoutes.profile,
AccountRoutes.profile,
icon: 'res/images/profile.png',
displayName: "Page:UserProfile".tr,
color: const Color.fromARGB(255, 68, 160, 206).hex),
],
),
Obx(() => MyFavorite(
favoriteMenus: bloc.state.favoriteMenus,
favoriteMenuBuilder: (favoriteMenu) {
return _buildMenu(
favoriteMenu.name,
favoriteMenu.path,
aliasName: favoriteMenu.aliasName,
//icon: favoriteMenu.icon,
// TODO:
icon: 'res/images/setting.png',
color: favoriteMenu.color,
displayName: favoriteMenu.displayName,
);
},
)),
],
),
drawer: SafeArea(
child: Container(
width: 260,
color: const Color.fromARGB(255, 44, 115, 141),
child: Column(
children: [
Container(
height: 24,
margin: const EdgeInsets.all(10),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 10),
child: Image.asset(
'res/images/logo.png',
height: 20,
width: 20,
),
),
const Padding(
padding: EdgeInsets.only(left: 10),
child: Text(
'abp flutter',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w400,
),
),
),
],
),
),
Expanded(
child: Obx(() => Navigation(
activedMenu: bloc.state.activedMenu,
menus: bloc.state.getMenus(),
onMenuExpanded: bloc.onMenuExpanded,
)),
),
],
),
)
child: Obx(() => MenuDrawer(
activedMenu: bloc.state.activedMenu,
menus: bloc.state.getMenus(),
onMenuExpanded: bloc.onMenuExpanded,
onMenuRefresh: bloc.refreshMenus,
)),
),
);
}

5
apps/flutter/dev_app/lib/pages/public/home/widget/index.dart

@ -0,0 +1,5 @@
export 'search.dart';
export 'menu_drawer.dart';
export 'my_favorite.dart';
export 'notification_bar.dart';
export 'quick_navigation.dart';

76
apps/flutter/dev_app/lib/pages/public/home/widget/menu_drawer.dart

@ -0,0 +1,76 @@
import 'package:components/widgets/menu/index.dart';
import 'package:core/models/common.dart';
import 'package:flutter/material.dart';
class MenuDrawer extends StatelessWidget {
const MenuDrawer({
super.key,
this.activedMenu,
this.menus = const [],
required this.onMenuRefresh,
this.onMenuExpanded,
});
final String? activedMenu;
final List<Menu> menus;
final void Function(Menu menu)? onMenuExpanded;
final Future<void> Function() onMenuRefresh;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: onMenuRefresh,
child: Drawer(
width: 260,
child: Column(
children: [
_buildLogo(),
Expanded(
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
Navigation(
activedMenu: activedMenu,
menus: menus,
onMenuExpanded: onMenuExpanded,
),
],
),
)
),
],
),
),
);
}
Widget _buildLogo() {
return Container(
height: 24,
margin: const EdgeInsets.all(10),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 10),
child: Image.asset(
'res/images/logo.png',
height: 20,
width: 20,
),
),
const Padding(
padding: EdgeInsets.only(left: 10),
child: Text(
'abp flutter',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}
}

42
apps/flutter/dev_app/lib/pages/public/home/widget/my_favorite.dart

@ -0,0 +1,42 @@
import 'package:components/widgets/empty/index.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:platforms/modes/menu.dto.dart';
class MyFavorite extends StatelessWidget {
const MyFavorite({
super.key,
required this.favoriteMenus,
required this.favoriteMenuBuilder,
});
final List<UserFavoriteMenuDto> favoriteMenus;
final Widget Function(UserFavoriteMenuDto favoriteMenu) favoriteMenuBuilder;
@override
Widget build(BuildContext context) {
return ExpansionTile(
initiallyExpanded: true,
title: Text('Label:MyFavorite'.tr,
style: Theme.of(context).textTheme.titleMedium,
),
children: [
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: favoriteMenus.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 5,
),
itemBuilder: (BuildContext context, int index) {
if (index >= favoriteMenus.length) {
return Empty.none;
}
return favoriteMenuBuilder(favoriteMenus[index]);
},
),
],
);
}
}

55
apps/flutter/dev_app/lib/pages/public/home/widget/notification_bar.dart

@ -0,0 +1,55 @@
import 'package:bruno/bruno.dart';
import 'package:components/widgets/empty/index.dart';
import 'package:flutter/material.dart';
import 'package:notifications/models/common.dart';
import 'package:notifications/models/notification.dart';
class NotificationBar extends StatelessWidget {
const NotificationBar({
super.key,
required this.notifications
});
final List<NotificationPaylod> notifications;
@override
Widget build(BuildContext context) {
if (notifications.isEmpty) {
return Empty.none;
}
return SizedBox(
height: 40,
child: SingleChildScrollView(
child: Column(
children: notifications.map<BrnNoticeBar>((payload) {
return BrnNoticeBar(
padding: const EdgeInsets.only(left: 5, right: 5, top: 3),
leftWidget: Image.asset(
'res/images/notification.png',
height: 30,
width: 30,
),
content: payload.title,
marquee: true,
noticeStyle: _mapNoticeStyles(payload.severity),
);
}).toList(),
),
),
);
}
NoticeStyle _mapNoticeStyles(NotificationSeverity? severity) {
if (severity == null) return NoticeStyles.normalNoticeWithArrow;
switch (severity) {
case NotificationSeverity.info:
case NotificationSeverity.success:
return NoticeStyles.succeedWithArrow;
case NotificationSeverity.fatal:
case NotificationSeverity.error:
return NoticeStyles.failWithArrow;
case NotificationSeverity.warn:
return NoticeStyles.warningWithArrow;
}
}
}

33
apps/flutter/dev_app/lib/pages/public/home/widget/quick_navigation.dart

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class QuickNavigation extends StatelessWidget {
const QuickNavigation({
super.key,
this.menus = const [],
});
final List<Widget> menus;
@override
Widget build(BuildContext context) {
return ExpansionTile(
initiallyExpanded: true,
title: Text('Label:QuickNavigation'.tr,
style: Theme.of(context).textTheme.titleMedium,
),
children: [
SizedBox(
height: 120,
child: GridView.count(
shrinkWrap: true,
crossAxisCount: 4,
crossAxisSpacing: 5,
physics: const NeverScrollableScrollPhysics(),
children: menus,
),
),
],
);
}
}

17
apps/flutter/dev_app/lib/services/notification.send.local.service.dart

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:core/index.dart';
import 'package:get/get.dart';
import 'package:notifications/services/notification.state.service.dart';
import 'package:rxdart/rxdart.dart' hide Notification;
import 'package:core/models/notifications.dart';
@ -14,6 +15,22 @@ class FlutterLocalNotificationsSendService extends NotificationSendService {
final Subject<String?> _selectedNotifications$ = BehaviorSubject<String?>();
EnvironmentService get _environmentService => resolve<EnvironmentService>();
NotificationStateService get _notificationStateService => resolve<NotificationStateService>();
@override
void onInit() {
super.onInit();
_notificationStateService
.getNotifications$()
.listen((payload) async {
//
await send(
payload.title,
payload.body,
payload.payload,
);
});
}
Future<void> initAsync() async {
var environment = _environmentService.getEnvironment();

24
apps/flutter/dev_app/lib/services/translation.service.res.service.dart

@ -1,5 +1,6 @@
import 'dart:ui';
import 'package:core/services/environment.service.dart';
import 'package:core/services/localization.service.dart';
import 'package:core/services/service.base.dart';
import 'package:core/services/session.service.dart';
@ -17,6 +18,7 @@ class TranslationResService extends ServiceBase implements TranslationService {
final InternalStore<TranslationState> _store = InternalStore<TranslationState>(state: TranslationState());
SessionService get _sessionService => resolve<SessionService>();
EnvironmentService get _environmentService => resolve<EnvironmentService>();
LocalizationService get _localizationService => resolve<LocalizationService>();
@override
@ -44,13 +46,21 @@ class TranslationResService extends ServiceBase implements TranslationService {
Future<TranslationState> _mapTranslationsMap(String language) async {
Map<String, Map<String, String>> translationsMap = {};
var filePath = 'res/translations/$language.json';
var content = await rootBundle.loadString(filePath);
var translationsObject = jsonDecode(content) as Map<String, dynamic>;
translationsMap.putIfAbsent(
language,
() => translationsObject.map((key, value) => MapEntry(key, value))
);
var environment = _environmentService.getEnvironment();
var translationFiles = environment.localization.translationFiles?[language] ?? ['$language.json'];
for (var translationFile in translationFiles) {
try {
var filePath = 'res/translations/$translationFile';
var content = await rootBundle.loadString(filePath);
var translationsObject = jsonDecode(content) as Map<String, dynamic>;
var translations = translationsMap[language] ?? {};
translations.addAll(translationsObject.map((key, value) => MapEntry(key, value)));
translationsMap.putIfAbsent(language, () => translations);
} catch (e) {
logger.error(e);
}
}
return TranslationState(
language: language,
translations: translationsMap,

14
apps/flutter/dev_app/res/config/demo.json

@ -13,6 +13,20 @@
"localization": {
"useLocalResources": true,
"defaultLanguage": "zh-Hans",
"translationFiles": {
"zh-Hans": [
"zh-Hans.json"
],
"zh_CN": [
"zh-Hans.json"
],
"en": [
"en.json"
],
"en_US": [
"en.json"
]
},
"supportedLocales": [
{
"cultureName": "en",

BIN
apps/flutter/dev_app/res/images/notification.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

1
apps/flutter/notifications/lib/models/notification.state.dart

@ -14,6 +14,7 @@ class NotificationState {
});
bool isEnabled;
List<NotificationGroup> groups;
NotificationGroup? findGroup(String name) {
return groups.firstWhereOrNull((item) => item.name == name);

11
apps/flutter/notifications/lib/services/notification.state.service.dart

@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:notifications/models/common.dart';
import 'package:rxdart/rxdart.dart' hide Notification;
import 'package:notifications/models/notification.dart';
import 'package:core/services/session.service.dart';
@ -20,6 +21,8 @@ class NotificationStateService extends ServiceBase {
NotificationService get _notificationService => resolve<NotificationService>();
SignalrService get _signalrService => resolve<SignalrService>(tag: NotificationTokens.producer);
final BehaviorSubject<NotificationPaylod> _notifications = BehaviorSubject<NotificationPaylod>();
final InternalStore<NotificationState> _store = InternalStore<NotificationState>(
state: _initState()
);
@ -41,6 +44,10 @@ class NotificationStateService extends ServiceBase {
return _store.sliceUpdate((state) => state);
}
Stream<NotificationPaylod> getNotifications$() {
return _notifications;
}
NotificationGroup? findGroup(String name) {
return _store.state.findGroup(name);
}
@ -75,6 +82,10 @@ class NotificationStateService extends ServiceBase {
return configState ?? NotificationState(isEnabled: true, groups: []);
}
void addNotification(NotificationInfo notification) {
_notifications.add(NotificationPaylod.fromNotification(notification));
}
Future<List<NotificationGroup>> getGroupAndCombineWithNotification(List<NotificationGroupDto> groupItems) {
return _notificationService.getMySubscribedListAsync()
.then((subscres) {

2
apps/flutter/platform/lib/services/favorite.menu.state.service.dart

@ -33,7 +33,7 @@ class FavoriteMenuStateService extends ServiceBase {
Future<void> refreshState() async {
var environment = _environmentService.getEnvironment();
var framework = environment.application.framework ?? 'flutter';
var framework = environment.application.framework ?? 'abp-flutter';
var result = await _favoriteMenuService.getMyFavoriteMenuList(framework);
_state.patch((state) => state.menus = result.items);
}

2
apps/flutter/platform/lib/services/menu.state.service.dart

@ -33,7 +33,7 @@ class MenuStateService extends ServiceBase {
Future<void> refreshState() async {
var environment = _environmentService.getEnvironment();
var framework = environment.application.framework ?? 'flutter';
var framework = environment.application.framework ?? 'abp-flutter';
var result = await _menuService.getCurrentUserMenuList(framework);
_state.patch((state) => state.menus = result.items);
}

Loading…
Cancel
Save