app_bloc.dart 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import 'dart:collection';
  2. import 'package:appflowy/startup/plugin/plugin.dart';
  3. import 'package:appflowy/startup/startup.dart';
  4. import 'package:appflowy/workspace/application/app/app_listener.dart';
  5. import 'package:appflowy/workspace/application/app/app_service.dart';
  6. import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
  7. import 'package:expandable/expandable.dart';
  8. import 'package:appflowy_backend/log.dart';
  9. import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
  10. import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
  12. import 'package:flutter/foundation.dart';
  13. import 'package:freezed_annotation/freezed_annotation.dart';
  14. import 'package:flutter_bloc/flutter_bloc.dart';
  15. import 'package:dartz/dartz.dart';
  16. part 'app_bloc.freezed.dart';
  17. class AppBloc extends Bloc<AppEvent, AppState> {
  18. final AppBackendService appService;
  19. final AppListener appListener;
  20. AppBloc({required AppPB app})
  21. : appService = AppBackendService(),
  22. appListener = AppListener(appId: app.id),
  23. super(AppState.initial(app)) {
  24. on<AppEvent>((event, emit) async {
  25. await event.map(
  26. initial: (e) async {
  27. _startListening();
  28. await _loadViews(emit);
  29. },
  30. createView: (CreateView value) async {
  31. await _createView(value, emit);
  32. },
  33. loadViews: (_) async {
  34. await _loadViews(emit);
  35. },
  36. delete: (e) async {
  37. await _deleteApp(emit);
  38. },
  39. deleteView: (deletedView) async {
  40. await _deleteView(emit, deletedView.viewId);
  41. },
  42. rename: (e) async {
  43. await _renameView(e, emit);
  44. },
  45. appDidUpdate: (e) async {
  46. final latestCreatedView = state.latestCreatedView;
  47. final views = e.app.belongings.items;
  48. AppState newState = state.copyWith(
  49. views: views,
  50. app: e.app,
  51. );
  52. if (latestCreatedView != null) {
  53. final index = views
  54. .indexWhere((element) => element.id == latestCreatedView.id);
  55. if (index == -1) {
  56. newState = newState.copyWith(latestCreatedView: null);
  57. }
  58. }
  59. emit(newState);
  60. },
  61. );
  62. });
  63. }
  64. void _startListening() {
  65. appListener.start(
  66. onAppUpdated: (app) {
  67. if (!isClosed) {
  68. add(AppEvent.appDidUpdate(app));
  69. }
  70. },
  71. );
  72. }
  73. Future<void> _renameView(Rename e, Emitter<AppState> emit) async {
  74. final result =
  75. await appService.updateApp(appId: state.app.id, name: e.newName);
  76. result.fold(
  77. (l) => emit(state.copyWith(successOrFailure: left(unit))),
  78. (error) => emit(state.copyWith(successOrFailure: right(error))),
  79. );
  80. }
  81. // Delete the current app
  82. Future<void> _deleteApp(Emitter<AppState> emit) async {
  83. final result = await appService.delete(appId: state.app.id);
  84. result.fold(
  85. (unit) => emit(state.copyWith(successOrFailure: left(unit))),
  86. (error) => emit(state.copyWith(successOrFailure: right(error))),
  87. );
  88. }
  89. Future<void> _deleteView(Emitter<AppState> emit, String viewId) async {
  90. final result = await appService.deleteView(viewId: viewId);
  91. result.fold(
  92. (unit) => emit(state.copyWith(successOrFailure: left(unit))),
  93. (error) => emit(state.copyWith(successOrFailure: right(error))),
  94. );
  95. }
  96. Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
  97. final result = await appService.createView(
  98. appId: state.app.id,
  99. name: value.name,
  100. desc: value.desc ?? "",
  101. layoutType: value.pluginBuilder.layoutType!,
  102. initialData: value.initialData,
  103. ext: value.ext ?? {},
  104. );
  105. result.fold(
  106. (view) => emit(
  107. state.copyWith(
  108. latestCreatedView: view,
  109. successOrFailure: left(unit),
  110. ),
  111. ),
  112. (error) {
  113. Log.error(error);
  114. emit(state.copyWith(successOrFailure: right(error)));
  115. },
  116. );
  117. }
  118. @override
  119. Future<void> close() async {
  120. await appListener.stop();
  121. return super.close();
  122. }
  123. Future<void> _loadViews(Emitter<AppState> emit) async {
  124. final viewsOrFailed = await appService.getViews(appId: state.app.id);
  125. viewsOrFailed.fold(
  126. (views) => emit(state.copyWith(views: views)),
  127. (error) {
  128. Log.error(error);
  129. emit(state.copyWith(successOrFailure: right(error)));
  130. },
  131. );
  132. }
  133. }
  134. @freezed
  135. class AppEvent with _$AppEvent {
  136. const factory AppEvent.initial() = Initial;
  137. const factory AppEvent.createView(
  138. String name,
  139. PluginBuilder pluginBuilder, {
  140. String? desc,
  141. /// The initial data should be the JSON of the document
  142. /// For example: {"document":{"type":"editor","children":[]}}
  143. String? initialData,
  144. Map<String, String>? ext,
  145. }) = CreateView;
  146. const factory AppEvent.loadViews() = LoadApp;
  147. const factory AppEvent.delete() = DeleteApp;
  148. const factory AppEvent.deleteView(String viewId) = DeleteView;
  149. const factory AppEvent.rename(String newName) = Rename;
  150. const factory AppEvent.appDidUpdate(AppPB app) = AppDidUpdate;
  151. }
  152. @freezed
  153. class AppState with _$AppState {
  154. const factory AppState({
  155. required AppPB app,
  156. required List<ViewPB> views,
  157. ViewPB? latestCreatedView,
  158. required Either<Unit, FlowyError> successOrFailure,
  159. }) = _AppState;
  160. factory AppState.initial(AppPB app) => AppState(
  161. app: app,
  162. views: app.belongings.items,
  163. successOrFailure: left(unit),
  164. );
  165. }
  166. class AppViewDataContext extends ChangeNotifier {
  167. final String appId;
  168. final ValueNotifier<List<ViewPB>> _viewsNotifier = ValueNotifier([]);
  169. final ValueNotifier<ViewPB?> _selectedViewNotifier = ValueNotifier(null);
  170. VoidCallback? _menuSharedStateListener;
  171. ExpandableController expandController =
  172. ExpandableController(initialExpanded: false);
  173. AppViewDataContext({required this.appId}) {
  174. _setLatestView(getIt<MenuSharedState>().latestOpenView);
  175. _menuSharedStateListener =
  176. getIt<MenuSharedState>().addLatestViewListener((view) {
  177. _setLatestView(view);
  178. });
  179. }
  180. VoidCallback addSelectedViewChangeListener(void Function(ViewPB?) callback) {
  181. listener() {
  182. callback(_selectedViewNotifier.value);
  183. }
  184. _selectedViewNotifier.addListener(listener);
  185. return listener;
  186. }
  187. void removeSelectedViewListener(VoidCallback listener) {
  188. _selectedViewNotifier.removeListener(listener);
  189. }
  190. void _setLatestView(ViewPB? view) {
  191. view?.freeze();
  192. if (_selectedViewNotifier.value != view) {
  193. _selectedViewNotifier.value = view;
  194. _expandIfNeed();
  195. notifyListeners();
  196. }
  197. }
  198. ViewPB? get selectedView => _selectedViewNotifier.value;
  199. set views(List<ViewPB> views) {
  200. if (_viewsNotifier.value != views) {
  201. _viewsNotifier.value = views;
  202. _expandIfNeed();
  203. notifyListeners();
  204. }
  205. }
  206. UnmodifiableListView<ViewPB> get views =>
  207. UnmodifiableListView(_viewsNotifier.value);
  208. VoidCallback addViewsChangeListener(
  209. void Function(UnmodifiableListView<ViewPB>) callback,
  210. ) {
  211. listener() {
  212. callback(views);
  213. }
  214. _viewsNotifier.addListener(listener);
  215. return listener;
  216. }
  217. void removeViewsListener(VoidCallback listener) {
  218. _viewsNotifier.removeListener(listener);
  219. }
  220. void _expandIfNeed() {
  221. if (_selectedViewNotifier.value == null) {
  222. return;
  223. }
  224. if (!_viewsNotifier.value.contains(_selectedViewNotifier.value)) {
  225. return;
  226. }
  227. if (expandController.expanded == false) {
  228. // Workaround: Delay 150 milliseconds to make the smooth animation while expanding
  229. Future.delayed(const Duration(milliseconds: 150), () {
  230. expandController.expanded = true;
  231. });
  232. }
  233. }
  234. @override
  235. void dispose() {
  236. if (_menuSharedStateListener != null) {
  237. getIt<MenuSharedState>()
  238. .removeLatestViewListener(_menuSharedStateListener!);
  239. }
  240. super.dispose();
  241. }
  242. }