[flutter]: right click action for menu's app

This commit is contained in:
appflowy 2021-10-30 14:44:43 +08:00
parent 4798823df3
commit dd9456e7ff
17 changed files with 426 additions and 293 deletions

View File

@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:window_size/window_size.dart';
import 'package:app_flowy/startup/launcher.dart';
import 'package:bloc/bloc.dart';
import 'package:flowy_log/flowy_log.dart';
class AppWidgetTask extends LaunchTask {
@override
@ -14,6 +16,7 @@ class AppWidgetTask extends LaunchTask {
Future<void> initialize(LaunchContext context) {
final widget = context.getIt<EntryPoint>().create();
final app = ApplicationWidget(child: widget);
Bloc.observer = ApplicationBlocObserver();
runApp(app);
return Future(() => {});
@ -53,3 +56,19 @@ class AppGlobals {
static GlobalKey<NavigatorState> rootNavKey = GlobalKey();
static NavigatorState get nav => rootNavKey.currentState!;
}
class ApplicationBlocObserver extends BlocObserver {
@override
// ignore: unnecessary_overrides
void onTransition(Bloc bloc, Transition transition) {
// Log.debug("[current]: ${transition.currentState} \n\n[next]: ${transition.nextState}");
Log.debug("${transition.nextState}");
super.onTransition(bloc, transition);
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
Log.debug(error);
super.onError(bloc, error, stackTrace);
}
}

View File

@ -1,11 +1,9 @@
import 'dart:io';
import 'package:app_flowy/startup/launcher.dart';
import 'package:app_flowy/startup/startup.dart';
import 'package:bloc/bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flowy_sdk/flowy_sdk.dart';
import 'package:flutter/material.dart';
import 'package:flowy_log/flowy_log.dart';
class RustSDKInitTask extends LaunchTask {
@override
@ -15,8 +13,6 @@ class RustSDKInitTask extends LaunchTask {
Future<void> initialize(LaunchContext context) async {
WidgetsFlutterBinding.ensureInitialized();
Bloc.observer = ApplicationBlocObserver();
Directory directory = await getApplicationDocumentsDirectory();
final documentPath = directory.path;
@ -35,18 +31,3 @@ class RustSDKInitTask extends LaunchTask {
});
}
}
class ApplicationBlocObserver extends BlocObserver {
@override
// ignore: unnecessary_overrides
void onTransition(Bloc bloc, Transition transition) {
// Log.debug("[current]: ${transition.currentState} \n[next]: ${transition.nextState}");
super.onTransition(bloc, transition);
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
Log.debug(error);
super.onError(bloc, error, stackTrace);
}
}

View File

@ -0,0 +1,27 @@
import 'package:flowy_infra/image.dart';
import 'package:flutter/material.dart';
enum AppDisclosureAction {
rename,
delete,
}
extension AppDisclosureExtension on AppDisclosureAction {
String get name {
switch (this) {
case AppDisclosureAction.rename:
return 'rename';
case AppDisclosureAction.delete:
return 'delete';
}
}
Widget get icon {
switch (this) {
case AppDisclosureAction.rename:
return svg('editor/edit');
case AppDisclosureAction.delete:
return svg('editor/delete');
}
}
}

View File

@ -1,31 +1,31 @@
import 'package:flowy_infra/image.dart';
import 'package:flutter/material.dart';
enum ViewAction {
enum ViewDisclosureAction {
rename,
delete,
duplicate,
}
extension ViewActionExtension on ViewAction {
extension ViewDisclosureExtension on ViewDisclosureAction {
String get name {
switch (this) {
case ViewAction.rename:
case ViewDisclosureAction.rename:
return 'rename';
case ViewAction.delete:
case ViewDisclosureAction.delete:
return 'delete';
case ViewAction.duplicate:
case ViewDisclosureAction.duplicate:
return 'duplicate';
}
}
Widget get icon {
switch (this) {
case ViewAction.rename:
case ViewDisclosureAction.rename:
return svg('editor/edit');
case ViewAction.delete:
case ViewDisclosureAction.delete:
return svg('editor/delete');
case ViewAction.duplicate:
case ViewDisclosureAction.duplicate:
return svg('editor/copy');
}
}

View File

@ -128,8 +128,7 @@ class HomeMenu extends StatelessWidget {
List<MenuItem> items = [];
items.add(MenuUser(user));
List<MenuItem> appWidgets =
apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(MenuAppContext(app))).toList());
List<MenuItem> appWidgets = apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(app)).toList());
items.addAll(appWidgets);
return items;
@ -166,7 +165,7 @@ class MenuList extends StatelessWidget {
if (index == 0) {
return const VSpace(29);
} else {
return const VSpace(24);
return VSpace(MenuAppSizes.appVPadding);
}
},
physics: StyledScrollPhysics(),

View File

@ -1,76 +1,14 @@
import 'package:expandable/expandable.dart';
import 'package:flowy_infra/flowy_icon_data_icons.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'menu_app.dart';
class MenuAppHeader extends StatelessWidget {
final App app;
const MenuAppHeader(
this.app, {
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkWell(
onTap: () {
ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
},
child: ExpandableIcon(
theme: ExpandableThemeData(
expandIcon: FlowyIconData.drop_down_show,
collapseIcon: FlowyIconData.drop_down_hide,
iconColor: theme.shader1,
iconSize: MenuAppSizes.expandedIconSize,
iconPadding: EdgeInsets.zero,
hasIcon: false,
),
),
),
HSpace(MenuAppSizes.expandedIconPadding),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) {
ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
},
child: FlowyText.medium(
app.name,
fontSize: 12,
),
)),
AddButton(
onSelected: (viewType) {
context.read<AppBloc>().add(AppEvent.createView("New view", "", viewType));
},
).padding(right: MenuAppSizes.expandedIconPadding),
],
),
);
}
}
class AddButton extends StatelessWidget {
final Function(ViewType) onSelected;
const AddButton({

View File

@ -0,0 +1,93 @@
import 'package:expandable/expandable.dart';
import 'package:flowy_infra/flowy_icon_data_icons.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
import '../menu_app.dart';
import 'add_button.dart';
import 'right_click_action.dart';
class MenuAppHeader extends StatelessWidget {
final App app;
const MenuAppHeader(
this.app, {
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return SizedBox(
height: MenuAppSizes.headerHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_renderExpandedIcon(context, theme),
HSpace(MenuAppSizes.iconPadding),
_renderTitle(context),
_renderAddButton(context),
],
),
);
}
Widget _renderExpandedIcon(BuildContext context, AppTheme theme) {
return SizedBox(
width: MenuAppSizes.headerHeight,
height: MenuAppSizes.headerHeight,
child: InkWell(
onTap: () {
ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
},
child: ExpandableIcon(
theme: ExpandableThemeData(
expandIcon: FlowyIconData.drop_down_show,
collapseIcon: FlowyIconData.drop_down_hide,
iconColor: theme.shader1,
iconSize: MenuAppSizes.iconSize,
iconPadding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
hasIcon: false,
),
),
),
);
}
Widget _renderTitle(BuildContext context) {
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
// Open the document
ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
},
onSecondaryTap: () {
AppDisclosureActions(onSelected: (action) {
print(action);
}).show(context, context, anchorDirection: AnchorDirection.bottomWithCenterAligned);
},
child: FlowyText.medium(
app.name,
fontSize: 12,
),
),
);
}
Widget _renderAddButton(BuildContext context) {
return AddButton(
onSelected: (viewType) {
context.read<AppBloc>().add(AppEvent.createView("New view", "", viewType));
},
).padding(right: MenuAppSizes.headerPadding);
}
}

View File

@ -0,0 +1,50 @@
import 'package:app_flowy/workspace/domain/edit_action/app_edit.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class AppDisclosureActions with ActionList<AppDisclosureActionWrapper> implements FlowyOverlayDelegate {
final Function(dartz.Option<AppDisclosureAction>) onSelected;
final _items = AppDisclosureAction.values.map((action) => AppDisclosureActionWrapper(action)).toList();
AppDisclosureActions({
required this.onSelected,
});
@override
String get identifier => "ViewDisclosureActions";
@override
List<AppDisclosureActionWrapper> get items => _items;
@override
double get maxWidth => 162;
@override
void Function(dartz.Option<AppDisclosureActionWrapper> p1) get selectCallback => (result) {
result.fold(
() => onSelected(dartz.none()),
(wrapper) => onSelected(dartz.some(wrapper.inner)),
);
};
@override
FlowyOverlayDelegate? get delegate => this;
@override
void didRemove() {
onSelected(dartz.none());
}
}
class AppDisclosureActionWrapper extends ActionItemData {
final AppDisclosureAction inner;
AppDisclosureActionWrapper(this.inner);
@override
Widget get icon => inner.icon;
@override
String get name => inner.name;
}

View File

@ -1,4 +1,4 @@
import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/header.dart';
import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/header/header.dart';
import 'package:expandable/expandable.dart';
import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
@ -10,36 +10,12 @@ import 'package:app_flowy/workspace/presentation/widgets/menu/menu.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'section/section.dart';
// [[diagram: MenuApp]]
//
// AppBloc
//
//
// 1.1 fetch views
// 1.2 update the MenuAppContext
// with the views
// 3.render sections
//
// MenuApp MenuAppContext ViewSection
//
//
//
// hold
// bind
//
// ViewListNotifier ViewSectionNotifier
// AppListenBloc
//
// 4.notifier binding. So The ViewSection
// 2.1 listen on the app will be re rebuild if the the number of
// 2.2 notify if the number of the app's view the views in MenuAppContext was changed.
// was changed
// 2.3 update MenuAppContext with the new
// views
class MenuApp extends MenuItem {
final MenuAppContext appCtx;
MenuApp(this.appCtx, {Key? key}) : super(key: appCtx.valueKey());
final App app;
final notifier = AppDataNotifier();
MenuApp(this.app, {Key? key}) : super(key: ValueKey("${app.id}${app.version}"));
@override
Widget build(BuildContext context) {
@ -47,7 +23,7 @@ class MenuApp extends MenuItem {
providers: [
BlocProvider<AppBloc>(
create: (context) {
final appBloc = getIt<AppBloc>(param1: appCtx.app.id);
final appBloc = getIt<AppBloc>(param1: app.id);
appBloc.add(const AppEvent.initial());
return appBloc;
},
@ -55,13 +31,12 @@ class MenuApp extends MenuItem {
],
child: BlocListener<AppBloc, AppState>(
listenWhen: (p, c) => p.selectedView != c.selectedView,
listener: (context, state) => appCtx.viewList.selectView = state.selectedView,
listener: (context, state) => notifier.selectView = state.selectedView,
child: BlocBuilder<AppBloc, AppState>(
buildWhen: (p, c) => p.views != c.views,
builder: (context, state) {
appCtx.viewList.views = state.views;
final child = _renderViewSection(appCtx.viewList);
return expandableWrapper(context, child);
notifier.views = state.views;
return expandableWrapper(context, _renderViewSection(notifier));
},
),
),
@ -71,7 +46,7 @@ class MenuApp extends MenuItem {
ExpandableNotifier expandableWrapper(BuildContext context, Widget child) {
return ExpandableNotifier(
child: ScrollOnExpand(
scrollOnExpand: true,
scrollOnExpand: false,
scrollOnCollapse: false,
child: Column(
children: <Widget>[
@ -84,7 +59,7 @@ class MenuApp extends MenuItem {
iconPadding: EdgeInsets.zero,
hasIcon: false,
),
header: MenuAppHeader(appCtx.app),
header: MenuAppHeader(app),
expanded: child,
collapsed: const SizedBox(),
),
@ -94,12 +69,10 @@ class MenuApp extends MenuItem {
);
}
Widget _renderViewSection(ViewListNotifier viewListNotifier) {
Widget _renderViewSection(AppDataNotifier viewListNotifier) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: viewListNotifier),
],
child: Consumer(builder: (context, ViewListNotifier notifier, child) {
providers: [ChangeNotifierProvider.value(value: viewListNotifier)],
child: Consumer(builder: (context, AppDataNotifier notifier, child) {
return const ViewSection().padding(vertical: 8);
}),
);
@ -110,16 +83,19 @@ class MenuApp extends MenuItem {
}
class MenuAppSizes {
static double expandedIconSize = 16;
static double expandedIconPadding = 6;
static double iconSize = 16;
static double headerHeight = 26;
static double headerPadding = 6;
static double iconPadding = 6;
static double appVPadding = 14;
static double scale = 1;
static double get expandedPadding => expandedIconSize * scale + expandedIconPadding;
static double get expandedPadding => iconSize * scale + headerPadding;
}
class ViewListNotifier extends ChangeNotifier {
class AppDataNotifier extends ChangeNotifier {
List<View> _views = [];
View? _selectedView;
ViewListNotifier();
AppDataNotifier();
set views(List<View>? items) {
_views = items ?? List.empty(growable: false);
@ -135,12 +111,3 @@ class ViewListNotifier extends ChangeNotifier {
List<View> get views => _views;
}
class MenuAppContext {
final App app;
final viewList = ViewListNotifier();
MenuAppContext(this.app);
Key valueKey() => ValueKey("${app.id}${app.version}");
}

View File

@ -1,85 +0,0 @@
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:app_flowy/workspace/domain/view_edit.dart';
class ViewActionList implements FlowyOverlayDelegate {
final Function(dartz.Option<ViewAction>) onSelected;
final BuildContext anchorContext;
final String _identifier = 'ViewActionList';
const ViewActionList({required this.anchorContext, required this.onSelected});
void show(BuildContext buildContext) {
final items = ViewAction.values
.map((action) => ActionItem(
action: action,
onSelected: (action) {
FlowyOverlay.of(buildContext).remove(_identifier);
onSelected(dartz.some(action));
}))
.toList();
ListOverlay.showWithAnchor(
buildContext,
identifier: _identifier,
itemCount: items.length,
itemBuilder: (context, index) => items[index],
anchorContext: anchorContext,
anchorDirection: AnchorDirection.bottomRight,
maxWidth: 162,
maxHeight: ViewAction.values.length * 32,
delegate: this,
);
}
@override
void didRemove() {
onSelected(dartz.none());
}
}
class ActionItem extends StatelessWidget {
final ViewAction action;
final Function(ViewAction) onSelected;
const ActionItem({
Key? key,
required this.action,
required this.onSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return FlowyHover(
config: HoverDisplayConfig(hoverColor: theme.hover),
builder: (context, onHover) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onSelected(action),
child: Row(
children: [
action.icon,
const HSpace(10),
FlowyText.medium(
action.name,
fontSize: 12,
),
],
).padding(
horizontal: 6,
vertical: 6,
),
);
},
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:app_flowy/workspace/domain/edit_action/view_edit.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
// [[Widget: LifeCycle]]
// https://flutterbyexample.com/lesson/stateful-widget-lifecycle
class ViewDisclosureButton extends StatelessWidget
with ActionList<ViewDisclosureActionWrapper>
implements FlowyOverlayDelegate {
final Function() onTap;
final Function(dartz.Option<ViewDisclosureAction>) onSelected;
final _items = ViewDisclosureAction.values.map((action) => ViewDisclosureActionWrapper(action)).toList();
ViewDisclosureButton({
Key? key,
required this.onTap,
required this.onSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyIconButton(
iconPadding: const EdgeInsets.all(5),
width: 26,
onPressed: () {
onTap();
show(context, context);
},
icon: svg("editor/details"),
);
}
@override
String get identifier => "ViewDisclosureActions";
@override
List<ViewDisclosureActionWrapper> get items => _items;
@override
double get maxWidth => 162;
@override
void Function(dartz.Option<ViewDisclosureActionWrapper> p1) get selectCallback => (result) {
result.fold(
() => onSelected(dartz.none()),
(wrapper) => onSelected(dartz.some(wrapper.inner)),
);
};
@override
FlowyOverlayDelegate? get delegate => this;
@override
void didRemove() {
onSelected(dartz.none());
}
}
class ViewDisclosureActionWrapper extends ActionItemData {
final ViewDisclosureAction inner;
ViewDisclosureActionWrapper(this.inner);
@override
Widget get icon => inner.icon;
@override
String get name => inner.name;
}

View File

@ -1,13 +1,10 @@
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/view/view_bloc.dart';
import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
import 'package:app_flowy/workspace/domain/view_ext.dart';
import 'package:app_flowy/workspace/domain/edit_action/view_edit.dart';
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
@ -15,12 +12,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:app_flowy/workspace/domain/image.dart';
import 'package:app_flowy/workspace/domain/view_edit.dart';
import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/menu_app.dart';
import 'action.dart';
import 'disclosure_action.dart';
// ignore: must_be_immutable
class ViewSectionItem extends StatelessWidget {
@ -81,15 +76,15 @@ class ViewSectionItem extends StatelessWidget {
height: 26,
child: Row(children: children).padding(
left: MenuAppSizes.expandedPadding,
right: MenuAppSizes.expandedIconPadding,
right: MenuAppSizes.headerPadding,
),
);
}
void _handleAction(BuildContext context, dartz.Option<ViewAction> action) {
void _handleAction(BuildContext context, dartz.Option<ViewDisclosureAction> action) {
action.foldRight({}, (action, previous) {
switch (action) {
case ViewAction.rename:
case ViewDisclosureAction.rename:
TextFieldDialog(
title: 'Rename',
value: context.read<ViewBloc>().state.view.name,
@ -99,42 +94,13 @@ class ViewSectionItem extends StatelessWidget {
).show(context);
break;
case ViewAction.delete:
case ViewDisclosureAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case ViewAction.duplicate:
case ViewDisclosureAction.duplicate:
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
}
});
}
}
// [[Widget: LifeCycle]]
// https://flutterbyexample.com/lesson/stateful-widget-lifecycle
class ViewDisclosureButton extends StatelessWidget {
final Function() onTap;
final Function(dartz.Option<ViewAction>) onSelected;
const ViewDisclosureButton({
Key? key,
required this.onTap,
required this.onSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyIconButton(
iconPadding: const EdgeInsets.all(5),
width: 26,
onPressed: () {
onTap();
ViewActionList(
anchorContext: context,
onSelected: onSelected,
).show(context);
},
icon: svg("editor/details"),
);
}
}

View File

@ -12,20 +12,20 @@ import 'item.dart';
import 'package:async/async.dart';
class ViewSectionNotifier with ChangeNotifier {
List<View> innerViews;
List<View> _views;
View? _selectedView;
CancelableOperation? _notifyListenerOperation;
ViewSectionNotifier(this.innerViews);
ViewSectionNotifier(List<View> views) : _views = views;
set views(List<View> views) => innerViews = views;
List<View> get views => innerViews;
set setViews(List<View> views) {
if (innerViews != views) {
innerViews = views;
set views(List<View> views) {
if (_views != views) {
_views = views;
_notifyListeners();
}
}
List<View> get views => _views;
set selectView(View? view) {
if (_selectedView == view) {
return;
@ -48,8 +48,8 @@ class ViewSectionNotifier with ChangeNotifier {
View? get selectedView => _selectedView;
void update(ViewListNotifier notifier) {
setViews = notifier.views;
void update(AppDataNotifier notifier) {
views = notifier.views;
selectView = notifier.selectedView;
}
@ -69,9 +69,9 @@ class ViewSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
// The ViewListNotifier will be updated after ViewListData changed passed by parent widget
return ChangeNotifierProxyProvider<ViewListNotifier, ViewSectionNotifier>(
return ChangeNotifierProxyProvider<AppDataNotifier, ViewSectionNotifier>(
create: (_) {
final views = Provider.of<ViewListNotifier>(context, listen: false).views;
final views = Provider.of<AppDataNotifier>(context, listen: false).views;
return ViewSectionNotifier(views);
},
update: (_, notifier, controller) => controller!..update(notifier),

View File

@ -0,0 +1,99 @@
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:dartz/dartz.dart' as dartz;
abstract class ActionList<T extends ActionItemData> {
List<T> get items;
String get identifier;
double get maxWidth;
void Function(dartz.Option<T>) get selectCallback;
FlowyOverlayDelegate? get delegate;
void show(BuildContext buildContext, BuildContext anchorContext,
{AnchorDirection anchorDirection = AnchorDirection.bottomRight}) {
final widgets = items
.map((action) => ActionItem<T>(
action: action,
onSelected: (action) {
FlowyOverlay.of(buildContext).remove(identifier);
selectCallback(dartz.some(action));
}))
.toList();
double totalHeight = widgets.length * (ActionListSizes.itemHeight + ActionListSizes.padding * 2);
ListOverlay.showWithAnchor(
buildContext,
identifier: identifier,
itemCount: widgets.length,
itemBuilder: (context, index) => widgets[index],
anchorContext: anchorContext,
anchorDirection: anchorDirection,
maxWidth: maxWidth,
maxHeight: totalHeight,
delegate: delegate,
);
}
}
abstract class ActionItemData {
Widget get icon;
String get name;
}
class ActionListSizes {
static double itemHPadding = 10;
static double itemHeight = 16;
static double padding = 6;
}
class ActionItem<T extends ActionItemData> extends StatelessWidget {
final T action;
final Function(T) onSelected;
const ActionItem({
Key? key,
required this.action,
required this.onSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return FlowyHover(
config: HoverDisplayConfig(hoverColor: theme.hover),
builder: (context, onHover) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onSelected(action),
child: SizedBox(
height: ActionListSizes.itemHeight,
child: Row(
children: [
action.icon,
HSpace(ActionListSizes.itemHPadding),
FlowyText.medium(
action.name,
fontSize: 12,
),
],
),
).padding(
horizontal: ActionListSizes.padding,
vertical: ActionListSizes.padding,
),
);
},
);
}
}

View File

@ -20,13 +20,14 @@ class ListOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
const padding = EdgeInsets.symmetric(horizontal: 6, vertical: 6);
return Material(
type: MaterialType.transparency,
child: Container(
constraints: BoxConstraints.tight(Size(maxWidth, maxHeight)),
constraints: BoxConstraints.tight(Size(maxWidth, maxHeight + padding.vertical)),
decoration: FlowyDecoration.decoration(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
padding: padding,
child: ListView.builder(
shrinkWrap: true,
itemBuilder: itemBuilder,

View File

@ -9,7 +9,7 @@ use crate::{
use async_stream::stream;
use flowy_ot::core::{Delta, OperationTransformable};
use futures::stream::StreamExt;
use std::{convert::TryFrom, sync::Arc, thread};
use std::{convert::TryFrom, sync::Arc};
use tokio::sync::{mpsc, RwLock};
pub struct DocumentActor {

View File

@ -164,6 +164,9 @@ impl ViewController {
.payload(updated_view.clone())
.send();
//
let _ = notify_view_num_changed(&updated_view.belong_to_id, self.trash_can.clone(), conn)?;
let _ = self.update_view_on_server(params);
Ok(updated_view)
}
@ -267,8 +270,7 @@ async fn handle_trash_event(
let _ = conn.immediate_transaction::<_, WorkspaceError, _>(|| {
for identifier in identifiers.items {
let view_table = ViewTableSql::read_view(&identifier.id, conn)?;
let repeated_view = read_belonging_view(&view_table.belong_to_id, trash_can.clone(), conn)?;
let _ = notify_view_num_changed(&view_table.belong_to_id, repeated_view)?;
let _ = notify_view_num_changed(&view_table.belong_to_id, trash_can.clone(), conn)?;
}
Ok(())
})?;
@ -289,8 +291,7 @@ async fn handle_trash_event(
}
for notify_id in notify_ids {
let repeated_view = read_belonging_view(&notify_id, trash_can.clone(), conn)?;
let _ = notify_view_num_changed(&notify_id, repeated_view)?;
let _ = notify_view_num_changed(&notify_id, trash_can.clone(), conn)?;
}
Ok(())
@ -302,8 +303,13 @@ async fn handle_trash_event(
}
}
#[tracing::instrument(skip(repeated_view), fields(view_count), err)]
fn notify_view_num_changed(belong_to_id: &str, repeated_view: RepeatedView) -> WorkspaceResult<()> {
#[tracing::instrument(skip(belong_to_id, trash_can, conn), fields(view_count), err)]
fn notify_view_num_changed(
belong_to_id: &str,
trash_can: Arc<TrashCan>,
conn: &SqliteConnection,
) -> WorkspaceResult<()> {
let repeated_view = read_belonging_view(belong_to_id, trash_can.clone(), conn)?;
tracing::Span::current().record("view_count", &format!("{}", repeated_view.len()).as_str());
send_dart_notification(&belong_to_id, WorkspaceNotification::AppViewsChanged)
.payload(repeated_view)