mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
[flutter]: right click action for menu's app
This commit is contained in:
parent
4798823df3
commit
dd9456e7ff
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
27
app_flowy/lib/workspace/domain/edit_action/app_edit.dart
Normal file
27
app_flowy/lib/workspace/domain/edit_action/app_edit.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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(),
|
||||
|
@ -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({
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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}");
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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(¬ify_id, trash_can.clone(), conn)?;
|
||||
let _ = notify_view_num_changed(¬ify_id, repeated_view)?;
|
||||
let _ = notify_view_num_changed(¬ify_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)
|
||||
|
Loading…
Reference in New Issue
Block a user