| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 | import 'dart:collection';import 'package:appflowy/startup/startup.dart';import 'package:appflowy/workspace/application/view/prelude.dart';import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';import 'package:expandable/expandable.dart';import 'package:appflowy_backend/log.dart';import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';import 'package:flutter/foundation.dart';import 'package:freezed_annotation/freezed_annotation.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:dartz/dartz.dart';part 'app_bloc.freezed.dart';class AppBloc extends Bloc<AppEvent, AppState> {  final ViewBackendService appService;  final ViewListener viewListener;  AppBloc({required ViewPB view})      : appService = ViewBackendService(),        viewListener = ViewListener(viewId: view.id),        super(AppState.initial(view)) {    on<AppEvent>((event, emit) async {      await event.map(        initial: (e) async {          _startListening();          await _loadViews(emit);        },        createView: (CreateView value) async {          await _createView(value, emit);        },        loadViews: (_) async {          await _loadViews(emit);        },        delete: (e) async {          await _deleteApp(emit);        },        deleteView: (deletedView) async {          await _deleteView(emit, deletedView.viewId);        },        rename: (e) async {          await _renameView(e, emit);        },        appDidUpdate: (e) async {          final latestCreatedView = state.latestCreatedView;          final views = e.view.childViews;          AppState newState = state.copyWith(            views: views,            view: e.view,          );          if (latestCreatedView != null) {            final index = views                .indexWhere((element) => element.id == latestCreatedView.id);            if (index == -1) {              newState = newState.copyWith(latestCreatedView: null);            }            emit(newState);          }          emit(newState);        },      );    });  }  void _startListening() {    viewListener.start(      onViewUpdated: (app) {        if (!isClosed) {          add(AppEvent.appDidUpdate(app));        }      },    );  }  Future<void> _renameView(Rename e, Emitter<AppState> emit) async {    final result = await ViewBackendService.updateView(      viewId: state.view.id,      name: e.newName,    );    result.fold(      (l) => emit(state.copyWith(successOrFailure: left(unit))),      (error) => emit(state.copyWith(successOrFailure: right(error))),    );  }// Delete the current app  Future<void> _deleteApp(Emitter<AppState> emit) async {    final result = await ViewBackendService.delete(viewId: state.view.id);    result.fold(      (unit) => emit(state.copyWith(successOrFailure: left(unit))),      (error) => emit(state.copyWith(successOrFailure: right(error))),    );  }  Future<void> _deleteView(Emitter<AppState> emit, String viewId) async {    final result = await ViewBackendService.deleteView(viewId: viewId);    result.fold(      (unit) => emit(state.copyWith(successOrFailure: left(unit))),      (error) => emit(state.copyWith(successOrFailure: right(error))),    );  }  Future<void> _createView(CreateView value, Emitter<AppState> emit) async {    // create a child view for the current view    final result = await ViewBackendService.createView(      parentViewId: state.view.id,      name: value.name,      desc: value.desc ?? "",      layoutType: value.layoutType,      initialDataBytes: value.initialDataBytes,      ext: value.ext ?? {},      openAfterCreate: true,    );    result.fold(      (view) => emit(        state.copyWith(          latestCreatedView: value.openAfterCreated ? view : null,          successOrFailure: left(unit),        ),      ),      (error) {        Log.error(error);        emit(state.copyWith(successOrFailure: right(error)));      },    );  }  @override  Future<void> close() async {    await viewListener.stop();    return super.close();  }  Future<void> _loadViews(Emitter<AppState> emit) async {    final viewsOrFailed =        await ViewBackendService.getChildViews(viewId: state.view.id);    viewsOrFailed.fold(      (views) => emit(state.copyWith(views: views)),      (error) {        Log.error(error);        emit(state.copyWith(successOrFailure: right(error)));      },    );  }}@freezedclass AppEvent with _$AppEvent {  const factory AppEvent.initial() = Initial;  const factory AppEvent.createView(    String name,    ViewLayoutPB layoutType, {    String? desc,    /// ~~The initial data should be the JSON of the document~~    /// ~~For example: {"document":{"type":"editor","children":[]}}~~    ///    /// - Document:    ///   the initial data should be the string that can be converted into [DocumentDataPB]    ///    List<int>? initialDataBytes,    Map<String, String>? ext,    /// open the view after created    @Default(true) bool openAfterCreated,  }) = CreateView;  const factory AppEvent.loadViews() = LoadApp;  const factory AppEvent.delete() = DeleteApp;  const factory AppEvent.deleteView(String viewId) = DeleteView;  const factory AppEvent.rename(String newName) = Rename;  const factory AppEvent.appDidUpdate(ViewPB view) = AppDidUpdate;}@freezedclass AppState with _$AppState {  const factory AppState({    required ViewPB view,    required List<ViewPB> views,    ViewPB? latestCreatedView,    required Either<Unit, FlowyError> successOrFailure,  }) = _AppState;  factory AppState.initial(ViewPB view) => AppState(        view: view,        views: view.childViews,        successOrFailure: left(unit),      );}class ViewDataContext extends ChangeNotifier {  final String viewId;  final ValueNotifier<List<ViewPB>> _viewsNotifier = ValueNotifier([]);  final ValueNotifier<ViewPB?> _selectedViewNotifier = ValueNotifier(null);  VoidCallback? _menuSharedStateListener;  ExpandableController expandController =      ExpandableController(initialExpanded: false);  ViewDataContext({required this.viewId}) {    _setLatestView(getIt<MenuSharedState>().latestOpenView);    _menuSharedStateListener =        getIt<MenuSharedState>().addLatestViewListener((view) {      _setLatestView(view);    });  }  VoidCallback onViewSelected(void Function(ViewPB?) callback) {    listener() {      callback(_selectedViewNotifier.value);    }    _selectedViewNotifier.addListener(listener);    return listener;  }  void removeOnViewSelectedListener(VoidCallback listener) {    _selectedViewNotifier.removeListener(listener);  }  void _setLatestView(ViewPB? view) {    view?.freeze();    if (_selectedViewNotifier.value != view) {      _selectedViewNotifier.value = view;      _expandIfNeed();      notifyListeners();    }  }  ViewPB? get selectedView => _selectedViewNotifier.value;  set views(List<ViewPB> views) {    if (_viewsNotifier.value != views) {      _viewsNotifier.value = views;      notifyListeners();    }  }  UnmodifiableListView<ViewPB> get views =>      UnmodifiableListView(_viewsNotifier.value);  VoidCallback onViewsChanged(    void Function(UnmodifiableListView<ViewPB>) callback,  ) {    listener() {      callback(views);    }    _viewsNotifier.addListener(listener);    return listener;  }  void removeOnViewChangedListener(VoidCallback listener) {    _viewsNotifier.removeListener(listener);  }  void _expandIfNeed() {    if (_selectedViewNotifier.value == null) {      return;    }    if (!_viewsNotifier.value        .map((e) => e.id)        .toList()        .contains(_selectedViewNotifier.value?.id)) {      return;    }    if (expandController.expanded == false) {      // Workaround: Delay 150 milliseconds to make the smooth animation while expanding      Future.delayed(const Duration(milliseconds: 150), () {        expandController.expanded = true;      });    }  }  @override  void dispose() {    if (_menuSharedStateListener != null) {      getIt<MenuSharedState>()          .removeLatestViewListener(_menuSharedStateListener!);    }    super.dispose();  }}
 |