app_bloc.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import 'dart:collection';
  2. import 'package:appflowy/startup/startup.dart';
  3. import 'package:appflowy/workspace/application/view/prelude.dart';
  4. import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
  5. import 'package:expandable/expandable.dart';
  6. import 'package:appflowy_backend/log.dart';
  7. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  8. import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
  9. import 'package:flutter/foundation.dart';
  10. import 'package:freezed_annotation/freezed_annotation.dart';
  11. import 'package:flutter_bloc/flutter_bloc.dart';
  12. import 'package:dartz/dartz.dart';
  13. part 'app_bloc.freezed.dart';
  14. class AppBloc extends Bloc<AppEvent, AppState> {
  15. final ViewBackendService appService;
  16. final ViewListener viewListener;
  17. AppBloc({required ViewPB view})
  18. : appService = ViewBackendService(),
  19. viewListener = ViewListener(viewId: view.id),
  20. super(AppState.initial(view)) {
  21. on<AppEvent>((event, emit) async {
  22. await event.map(
  23. initial: (e) async {
  24. _startListening();
  25. await _loadViews(emit);
  26. },
  27. createView: (CreateView value) async {
  28. await _createView(value, emit);
  29. },
  30. loadViews: (_) async {
  31. await _loadViews(emit);
  32. },
  33. delete: (e) async {
  34. await _deleteApp(emit);
  35. },
  36. deleteView: (deletedView) async {
  37. await _deleteView(emit, deletedView.viewId);
  38. },
  39. rename: (e) async {
  40. await _renameView(e, emit);
  41. },
  42. appDidUpdate: (e) async {
  43. final latestCreatedView = state.latestCreatedView;
  44. final views = e.view.childViews;
  45. AppState newState = state.copyWith(
  46. views: views,
  47. view: e.view,
  48. );
  49. if (latestCreatedView != null) {
  50. final index = views
  51. .indexWhere((element) => element.id == latestCreatedView.id);
  52. if (index == -1) {
  53. newState = newState.copyWith(latestCreatedView: null);
  54. }
  55. emit(newState);
  56. }
  57. emit(newState);
  58. },
  59. );
  60. });
  61. }
  62. void _startListening() {
  63. viewListener.start(
  64. onViewUpdated: (app) {
  65. if (!isClosed) {
  66. add(AppEvent.appDidUpdate(app));
  67. }
  68. },
  69. );
  70. }
  71. Future<void> _renameView(Rename e, Emitter<AppState> emit) async {
  72. final result = await ViewBackendService.updateView(
  73. viewId: state.view.id,
  74. name: e.newName,
  75. );
  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 ViewBackendService.delete(viewId: state.view.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 ViewBackendService.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. // create a child view for the current view
  98. final result = await ViewBackendService.createView(
  99. parentViewId: state.view.id,
  100. name: value.name,
  101. desc: value.desc ?? "",
  102. layoutType: value.layoutType,
  103. initialDataBytes: value.initialDataBytes,
  104. ext: value.ext ?? {},
  105. openAfterCreate: true,
  106. );
  107. result.fold(
  108. (view) => emit(
  109. state.copyWith(
  110. latestCreatedView: value.openAfterCreated ? view : null,
  111. successOrFailure: left(unit),
  112. ),
  113. ),
  114. (error) {
  115. Log.error(error);
  116. emit(state.copyWith(successOrFailure: right(error)));
  117. },
  118. );
  119. }
  120. @override
  121. Future<void> close() async {
  122. await viewListener.stop();
  123. return super.close();
  124. }
  125. Future<void> _loadViews(Emitter<AppState> emit) async {
  126. final viewsOrFailed =
  127. await ViewBackendService.getChildViews(viewId: state.view.id);
  128. viewsOrFailed.fold(
  129. (views) => emit(state.copyWith(views: views)),
  130. (error) {
  131. Log.error(error);
  132. emit(state.copyWith(successOrFailure: right(error)));
  133. },
  134. );
  135. }
  136. }
  137. @freezed
  138. class AppEvent with _$AppEvent {
  139. const factory AppEvent.initial() = Initial;
  140. const factory AppEvent.createView(
  141. String name,
  142. ViewLayoutPB layoutType, {
  143. String? desc,
  144. /// ~~The initial data should be the JSON of the document~~
  145. /// ~~For example: {"document":{"type":"editor","children":[]}}~~
  146. ///
  147. /// - Document:
  148. /// the initial data should be the string that can be converted into [DocumentDataPB]
  149. ///
  150. List<int>? initialDataBytes,
  151. Map<String, String>? ext,
  152. /// open the view after created
  153. @Default(true) bool openAfterCreated,
  154. }) = CreateView;
  155. const factory AppEvent.loadViews() = LoadApp;
  156. const factory AppEvent.delete() = DeleteApp;
  157. const factory AppEvent.deleteView(String viewId) = DeleteView;
  158. const factory AppEvent.rename(String newName) = Rename;
  159. const factory AppEvent.appDidUpdate(ViewPB view) = AppDidUpdate;
  160. }
  161. @freezed
  162. class AppState with _$AppState {
  163. const factory AppState({
  164. required ViewPB view,
  165. required List<ViewPB> views,
  166. ViewPB? latestCreatedView,
  167. required Either<Unit, FlowyError> successOrFailure,
  168. }) = _AppState;
  169. factory AppState.initial(ViewPB view) => AppState(
  170. view: view,
  171. views: view.childViews,
  172. successOrFailure: left(unit),
  173. );
  174. }
  175. class ViewDataContext extends ChangeNotifier {
  176. final String viewId;
  177. final ValueNotifier<List<ViewPB>> _viewsNotifier = ValueNotifier([]);
  178. final ValueNotifier<ViewPB?> _selectedViewNotifier = ValueNotifier(null);
  179. VoidCallback? _menuSharedStateListener;
  180. ExpandableController expandController =
  181. ExpandableController(initialExpanded: false);
  182. ViewDataContext({required this.viewId}) {
  183. _setLatestView(getIt<MenuSharedState>().latestOpenView);
  184. _menuSharedStateListener =
  185. getIt<MenuSharedState>().addLatestViewListener((view) {
  186. _setLatestView(view);
  187. });
  188. }
  189. VoidCallback onViewSelected(void Function(ViewPB?) callback) {
  190. listener() {
  191. callback(_selectedViewNotifier.value);
  192. }
  193. _selectedViewNotifier.addListener(listener);
  194. return listener;
  195. }
  196. void removeOnViewSelectedListener(VoidCallback listener) {
  197. _selectedViewNotifier.removeListener(listener);
  198. }
  199. void _setLatestView(ViewPB? view) {
  200. view?.freeze();
  201. if (_selectedViewNotifier.value != view) {
  202. _selectedViewNotifier.value = view;
  203. _expandIfNeed();
  204. notifyListeners();
  205. }
  206. }
  207. ViewPB? get selectedView => _selectedViewNotifier.value;
  208. set views(List<ViewPB> views) {
  209. if (_viewsNotifier.value != views) {
  210. _viewsNotifier.value = views;
  211. notifyListeners();
  212. }
  213. }
  214. UnmodifiableListView<ViewPB> get views =>
  215. UnmodifiableListView(_viewsNotifier.value);
  216. VoidCallback onViewsChanged(
  217. void Function(UnmodifiableListView<ViewPB>) callback,
  218. ) {
  219. listener() {
  220. callback(views);
  221. }
  222. _viewsNotifier.addListener(listener);
  223. return listener;
  224. }
  225. void removeOnViewChangedListener(VoidCallback listener) {
  226. _viewsNotifier.removeListener(listener);
  227. }
  228. void _expandIfNeed() {
  229. if (_selectedViewNotifier.value == null) {
  230. return;
  231. }
  232. if (!_viewsNotifier.value
  233. .map((e) => e.id)
  234. .toList()
  235. .contains(_selectedViewNotifier.value?.id)) {
  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. }