app_bloc.dart 8.2 KB

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