From 094d74b7469056d10056892fbdb9418969aa592a Mon Sep 17 00:00:00 2001 From: debuggerx Date: Fri, 8 Oct 2021 18:29:06 +0800 Subject: [PATCH] feat: add CopyFunc to provider generator; add local solution provider; update dde data table style. --- app/lib/builder/provider_annotation.dart | 4 +- app/lib/builder/provider_generator.dart | 12 +++ app/lib/models/local_solutions.dart | 21 +++++ app/lib/models/local_solutions_linux.dart | 68 ++++++++++++++++ app/lib/models/local_solutions_provider.dart | 1 + app/lib/models/local_solutions_web.dart | 67 ++++++++++++++++ app/lib/models/solution.dart | 4 +- app/lib/pages/gesture_editor.dart | 16 ++-- app/lib/pages/home.dart | 2 + app/lib/pages/local_manager.dart | 78 +++++++++++++++++- app/lib/widgets/dde_data_table.dart | 113 +++------------------------ app/pubspec.yaml | 1 + 12 files changed, 272 insertions(+), 115 deletions(-) create mode 100644 app/lib/models/local_solutions.dart create mode 100644 app/lib/models/local_solutions_linux.dart create mode 100644 app/lib/models/local_solutions_provider.dart create mode 100644 app/lib/models/local_solutions_web.dart diff --git a/app/lib/builder/provider_annotation.dart b/app/lib/builder/provider_annotation.dart index 3f66ed0..03b604b 100644 --- a/app/lib/builder/provider_annotation.dart +++ b/app/lib/builder/provider_annotation.dart @@ -1,5 +1,7 @@ class ProviderModel { - const ProviderModel(); + const ProviderModel({this.copyable = false}); + + final bool copyable; } class ProviderModelProp { diff --git a/app/lib/builder/provider_generator.dart b/app/lib/builder/provider_generator.dart index d6e5df7..205c260 100644 --- a/app/lib/builder/provider_generator.dart +++ b/app/lib/builder/provider_generator.dart @@ -37,6 +37,7 @@ class ProviderGenerator extends GeneratorForAnnotation { _genImports(className, needImports), _genClassDefine(element.displayName), _genNamedConstructors(element.constructors, element.displayName), + _genCopyFunc(element.displayName, fields, annotation.read('copyable').boolValue), _genSetPropsFunc(fields), ].whereNotNull(); } @@ -71,6 +72,17 @@ String? _genNamedConstructors(List constructors, String disp return _constructors.length > 0 ? _constructors.join('\n') : null; } +String? _genCopyFunc(String displayName, List fields, bool copyable) { + if (!copyable) return null; + return ''' + void copyFrom(${displayName} other) { + bool changed = false; + ${fields.map((f) => 'if (other.${f.name}.diff(this.${f.name})) {this.${f.name} = other.${f.name}; changed = true; }').join('\n')} + if (changed) notifyListeners(); + } + '''; +} + String _genSetPropsFunc(List fields) => ''' void setProps({ ${fields.map((f) => '${f.type.endsWith('?') ? '' : 'required '}${f.type} ${f.name},').join('\n')} diff --git a/app/lib/models/local_solutions.dart b/app/lib/models/local_solutions.dart new file mode 100644 index 0000000..e2249ed --- /dev/null +++ b/app/lib/models/local_solutions.dart @@ -0,0 +1,21 @@ +export 'local_solutions_web.dart' if (dart.library.io) 'local_solutions_linux.dart'; + +import 'package:dde_gesture_manager/models/solution.dart'; + +abstract class LocalSolutionEntry { + Solution solution; + DateTime lastModifyTime; + String path; + + LocalSolutionEntry({ + required this.path, + required this.solution, + required this.lastModifyTime, + }); + + save(); +} + +abstract class LocalSolutionsInterface { + Future> get solutionEntries; +} \ No newline at end of file diff --git a/app/lib/models/local_solutions_linux.dart b/app/lib/models/local_solutions_linux.dart new file mode 100644 index 0000000..80cc63e --- /dev/null +++ b/app/lib/models/local_solutions_linux.dart @@ -0,0 +1,68 @@ +import 'dart:io'; + +import 'package:dde_gesture_manager/builder/provider_annotation.dart'; +import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/models/solution.dart'; +import 'package:path/path.dart' show join; +import 'package:path_provider/path_provider.dart'; + +import 'local_solutions.dart'; + +export 'local_solutions.dart'; + +@ProviderModel() +class LocalSolutions implements LocalSolutionsInterface { + LocalSolutions() { + solutionEntries.then((value) => solutions = value); + } + + @override + Future> get solutionEntries async { + var _supportDirectory = await getApplicationSupportDirectory(); + var directory = Directory(join(_supportDirectory.path, 'solutions')); + if (!directory.existsSync()) directory.createSync(); + directory.path.sout(); + return directory + .list() + .map((f) { + LocalSolutionEntryLinux? entry; + try { + var content = File(f.path).readAsStringSync(); + entry = LocalSolutionEntryLinux( + path: f.path, solution: Solution.parse(content), lastModifyTime: f.statSync().modified); + } catch (e) { + e.sout(); + } + return entry; + }) + .where((e) => e != null) + .cast() + .toList(); + } + + @ProviderModelProp() + List? solutions; +} + +class LocalSolutionEntryLinux implements LocalSolutionEntry { + @override + String path; + + @override + Solution solution; + + @override + DateTime lastModifyTime; + + LocalSolutionEntryLinux({ + required this.path, + required this.solution, + required this.lastModifyTime, + }); + + @override + save() { + // TODO: implement save + throw UnimplementedError(); + } +} diff --git a/app/lib/models/local_solutions_provider.dart b/app/lib/models/local_solutions_provider.dart new file mode 100644 index 0000000..6a10556 --- /dev/null +++ b/app/lib/models/local_solutions_provider.dart @@ -0,0 +1 @@ +export 'local_solutions_web.provider.dart' if (dart.library.io) 'local_solutions_linux.provider.dart'; \ No newline at end of file diff --git a/app/lib/models/local_solutions_web.dart b/app/lib/models/local_solutions_web.dart new file mode 100644 index 0000000..81faed7 --- /dev/null +++ b/app/lib/models/local_solutions_web.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +import 'package:dde_gesture_manager/builder/provider_annotation.dart'; +import 'package:dde_gesture_manager/models/solution.dart'; +import 'package:dde_gesture_manager/extensions.dart'; +import 'dart:html'; + +import 'local_solutions.dart'; +export 'local_solutions.dart'; + +@ProviderModel() +class LocalSolutions implements LocalSolutionsInterface { + LocalSolutions() { + solutionEntries.then((value) => solutions = value); + } + + @override + Future> get solutionEntries async { + return window.localStorage.keys + .map((key) { + if (key.startsWith('solutions.')) { + LocalSolutionEntryWeb? entry; + try { + var content = window.localStorage[key] ?? ''; + var solutionJson = json.decode(content); + entry = LocalSolutionEntryWeb( + path: key, + solution: Solution.parse(solutionJson), + lastModifyTime: DateTime.parse(solutionJson['modified_at']), + ); + } catch (e) { + e.sout(); + } + return entry; + } + }) + .where((e) => e != null) + .cast() + .toList(); + } + + @ProviderModelProp() + List? solutions; +} + +class LocalSolutionEntryWeb implements LocalSolutionEntry { + @override + String path; + + @override + Solution solution; + + @override + DateTime lastModifyTime; + + LocalSolutionEntryWeb({ + required this.path, + required this.solution, + required this.lastModifyTime, + }); + + @override + save() { + // TODO: implement save + throw UnimplementedError(); + } +} diff --git a/app/lib/models/solution.dart b/app/lib/models/solution.dart index c7c8e69..14583ed 100644 --- a/app/lib/models/solution.dart +++ b/app/lib/models/solution.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:dde_gesture_manager/builder/provider_annotation.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; -@ProviderModel() +@ProviderModel(copyable: true) class Solution { @ProviderModelProp() String? name; @@ -45,7 +45,7 @@ enum GestureType { shortcut, } -@ProviderModel() +@ProviderModel(copyable: true) class GestureProp { @ProviderModelProp() Gesture? gesture; diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index 0dec921..566ffe5 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -30,7 +30,9 @@ class GestureEditor extends StatelessWidget { Center( child: Text('${LocaleKeys.gesture_editor_gestures}.${H.getGestureName(gesture.gesture)}').tr(), ), - Text('${LocaleKeys.gesture_editor_directions}.${H.getGestureDirectionName(gesture.direction)}').tr(), + Center( + child: Text('${LocaleKeys.gesture_editor_directions}.${H.getGestureDirectionName(gesture.direction)}') + .tr()), Center( child: Text('${gesture.fingers}'), ), @@ -126,15 +128,15 @@ class GestureEditor extends StatelessWidget { ), ), dataRowColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.hovered)) return Colors.grey; - if (states.contains(MaterialState.selected)) return Colors.green; + if (states.contains(MaterialState.hovered)) return context.t.dialogBackgroundColor; + if (states.contains(MaterialState.selected)) return Colors.blueAccent; return null; }), columns: [ - DDataColumn(label: Text(LocaleKeys.gesture_editor_gesture.tr())), - DDataColumn(label: Text(LocaleKeys.gesture_editor_direction.tr())), - DDataColumn(label: Text(LocaleKeys.gesture_editor_fingers.tr())), - DDataColumn(label: Text(LocaleKeys.gesture_editor_type.tr())), + DDataColumn(label: Text(LocaleKeys.gesture_editor_gesture.tr()), center: true), + DDataColumn(label: Text(LocaleKeys.gesture_editor_direction.tr()), center: true), + DDataColumn(label: Text(LocaleKeys.gesture_editor_fingers.tr()), center: true), + DDataColumn(label: Text(LocaleKeys.gesture_editor_type.tr()), center: true), DDataColumn(label: Text(LocaleKeys.gesture_editor_command.tr())), DDataColumn(label: Text(LocaleKeys.gesture_editor_remark.tr())), ], diff --git a/app/lib/pages/home.dart b/app/lib/pages/home.dart index a98d674..26341bc 100644 --- a/app/lib/pages/home.dart +++ b/app/lib/pages/home.dart @@ -1,4 +1,5 @@ import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/models/local_solutions_provider.dart'; import 'package:dde_gesture_manager/models/solution.provider.dart'; import 'package:dde_gesture_manager/pages/content.dart'; import 'package:dde_gesture_manager/pages/footer.dart'; @@ -65,6 +66,7 @@ class _HomePageState extends State { } ''')), ChangeNotifierProvider(create: (context) => GesturePropProvider.empty()), + ChangeNotifierProvider(create: (context) => LocalSolutionsProvider(),lazy: false), ], child: Column( mainAxisSize: MainAxisSize.max, diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index de3e9c4..7fcab24 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -1,20 +1,45 @@ import 'package:dde_gesture_manager/constants/constants.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/content_layout.provider.dart'; +import 'package:dde_gesture_manager/models/local_solutions_provider.dart'; +import 'package:dde_gesture_manager/models/solution.provider.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:flutter/animation.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; -class LocalManager extends StatelessWidget { +class LocalManager extends StatefulWidget { const LocalManager({ Key? key, }) : super(key: key); @override + State createState() => _LocalManagerState(); +} + +class _LocalManagerState extends State { + late ScrollController _scrollController; + int? _hoveringIndex; + int? _selectedIndex; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + } + + Color _getItemBackgroundColor(int index) { + Color _color = index % 2 == 0 ? context.t.scaffoldBackgroundColor : context.t.backgroundColor; + if (index == _hoveringIndex) _color = context.t.scaffoldBackgroundColor; + if (index == _selectedIndex) _color = Colors.blueAccent; + return _color; + } + + @override Widget build(BuildContext context) { var isOpen = context.watch().localManagerOpened == true; + var localSolutions = context.watch().solutions ?? []; return AnimatedContainer( duration: mediumDuration, curve: Curves.easeInOut, @@ -57,6 +82,57 @@ class LocalManager extends StatelessWidget { ), ], ), + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, index) => GestureDetector( + onDoubleTap: () { + context.read().copyFrom(localSolutions[index].solution); + setState(() { + _selectedIndex = index; + }); + }, + onTap: () { + setState(() { + _selectedIndex = index; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hoveringIndex = index; + }); + }, + child: Container( + color: _getItemBackgroundColor(index), + child: Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(localSolutions[index].solution.name ?? ''), + Text('456'), + ], + ), + ), + ), + ), + ), + itemCount: localSolutions.length, + ), + ), + Container( + height: 150, + color: Colors.black, + ), + ], + ), + ), ], ), ), diff --git a/app/lib/widgets/dde_data_table.dart b/app/lib/widgets/dde_data_table.dart index 90f0f65..b1ebe2b 100644 --- a/app/lib/widgets/dde_data_table.dart +++ b/app/lib/widgets/dde_data_table.dart @@ -30,6 +30,7 @@ class DDataColumn { this.tooltip, this.numeric = false, this.onSort, + this.center = false, }) : assert(label != null); /// The column heading. @@ -66,6 +67,8 @@ class DDataColumn { final DataColumnSortCallback? onSort; bool get _debugInteractive => onSort != null; + + final bool center; } /// Row configuration and cell data for a [DataTable]. @@ -735,68 +738,6 @@ class _DDataTableState extends State { widget.rows.any((DDataRow row) => row._debugInteractive); } - void _handleSelectAll(bool? checked, bool someChecked) { - // If some checkboxes are checked, all checkboxes are selected. Otherwise, - // use the new checked value but default to false if it's null. - final bool effectiveChecked = someChecked || (checked ?? false); - if (widget.onSelectAll != null) { - widget.onSelectAll!(effectiveChecked); - } else { - for (final DDataRow row in widget.rows) { - if (row.onSelectChanged != null && row.selected != effectiveChecked) row.onSelectChanged!(effectiveChecked); - } - } - } - - Widget _buildCheckbox({ - required BuildContext context, - required bool? checked, - required VoidCallback? onRowTap, - required ValueChanged? onCheckboxChanged, - required MaterialStateProperty? overlayColor, - required bool tristate, - }) { - final ThemeData themeData = Theme.of(context); - final double effectiveHorizontalMargin = - widget.horizontalMargin ?? themeData.dataTableTheme.horizontalMargin ?? DDataTable._horizontalMargin; - final double effectiveCheckboxHorizontalMarginStart = widget.checkboxHorizontalMargin ?? - themeData.dataTableTheme.checkboxHorizontalMargin ?? - effectiveHorizontalMargin; - final double effectiveCheckboxHorizontalMarginEnd = widget.checkboxHorizontalMargin ?? - themeData.dataTableTheme.checkboxHorizontalMargin ?? - effectiveHorizontalMargin / 2.0; - Widget contents = Semantics( - container: true, - child: Padding( - padding: EdgeInsetsDirectional.only( - start: effectiveCheckboxHorizontalMarginStart, - end: effectiveCheckboxHorizontalMarginEnd, - ), - child: Center( - child: Checkbox( - // TODO(per): Remove when Checkbox has theme, https://github.com/flutter/flutter/issues/53420. - activeColor: themeData.colorScheme.primary, - checkColor: themeData.colorScheme.onPrimary, - value: checked, - onChanged: onCheckboxChanged, - tristate: tristate, - ), - ), - ), - ); - if (onRowTap != null) { - contents = TableRowInkWell( - onTap: onRowTap, - overlayColor: overlayColor, - child: contents, - ); - } - return TableCell( - verticalAlignment: TableCellVerticalAlignment.fill, - child: contents, - ); - } - Widget _buildHeadingCell({ required BuildContext context, required EdgeInsetsGeometry padding, @@ -806,11 +747,13 @@ class _DDataTableState extends State { required VoidCallback? onSort, required bool sorted, required bool ascending, + required bool center, required MaterialStateProperty? overlayColor, }) { final ThemeData themeData = Theme.of(context); label = Row( textDirection: numeric ? TextDirection.rtl : null, + mainAxisAlignment: center ? MainAxisAlignment.spaceAround : MainAxisAlignment.start, children: [ label, if (onSort != null) ...[ @@ -935,24 +878,13 @@ class _DDataTableState extends State { ); final bool anyRowSelectable = widget.rows.any((DDataRow row) => row.onSelectChanged != null); final bool displayCheckboxColumn = widget.showCheckboxColumn && anyRowSelectable; - final Iterable rowsWithCheckbox = - displayCheckboxColumn ? widget.rows.where((DDataRow row) => row.onSelectChanged != null) : []; - final Iterable rowsChecked = rowsWithCheckbox.where((DDataRow row) => row.selected); - final bool allChecked = displayCheckboxColumn && rowsChecked.length == rowsWithCheckbox.length; - final bool anyChecked = displayCheckboxColumn && rowsChecked.isNotEmpty; - final bool someChecked = anyChecked && !allChecked; final double effectiveHorizontalMargin = widget.horizontalMargin ?? theme.dataTableTheme.horizontalMargin ?? DDataTable._horizontalMargin; - final double effectiveCheckboxHorizontalMarginStart = - widget.checkboxHorizontalMargin ?? theme.dataTableTheme.checkboxHorizontalMargin ?? effectiveHorizontalMargin; - final double effectiveCheckboxHorizontalMarginEnd = widget.checkboxHorizontalMargin ?? - theme.dataTableTheme.checkboxHorizontalMargin ?? - effectiveHorizontalMargin / 2.0; final double effectiveColumnSpacing = widget.columnSpacing ?? theme.dataTableTheme.columnSpacing ?? DDataTable._columnSpacing; final List tableColumns = List.filled( - widget.columns.length + (displayCheckboxColumn ? 1 : 0), const _NullTableColumnWidth()); + widget.columns.length, const _NullTableColumnWidth()); final List tableRows = List.generate( widget.rows.length + 1, // the +1 is for the header row (int index) { @@ -989,34 +921,6 @@ class _DDataTableState extends State { int rowIndex; int displayColumnIndex = 0; - if (displayCheckboxColumn) { - tableColumns[0] = FixedColumnWidth(0); - tableRows[0].children![0] = RectGetter.defaultKey(child: Container()); - // tableColumns[0] = FixedColumnWidth( - // effectiveCheckboxHorizontalMarginStart + Checkbox.width + effectiveCheckboxHorizontalMarginEnd); - // tableRows[0].children![0] = _buildCheckbox( - // context: context, - // checked: someChecked ? null : allChecked, - // onRowTap: null, - // onCheckboxChanged: (bool? checked) => _handleSelectAll(checked, someChecked), - // overlayColor: null, - // tristate: true, - // ); - rowIndex = 1; - for (final DDataRow row in widget.rows) { - // tableRows[rowIndex].children![0] = _buildCheckbox( - // context: context, - // checked: row.selected, - // onRowTap: row.onSelectChanged == null ? null : () => row.onSelectChanged?.call(!row.selected), - // onCheckboxChanged: row.onSelectChanged, - // overlayColor: row.color ?? effectiveDataRowColor, - // tristate: false, - // ); - tableRows[rowIndex].children![0] = Container(); - rowIndex += 1; - } - displayColumnIndex += 1; - } for (int dataColumnIndex = 0; dataColumnIndex < widget.columns.length; dataColumnIndex += 1) { final DDataColumn column = widget.columns[dataColumnIndex]; @@ -1060,6 +964,7 @@ class _DDataTableState extends State { sorted: dataColumnIndex == widget.sortColumnIndex, ascending: widget.sortAscending, overlayColor: effectiveHeadingRowColor, + center: column.center, ); rowIndex = 1; for (final DDataRow row in widget.rows) { @@ -1125,11 +1030,11 @@ class _DDataTableState extends State { fit: StackFit.passthrough, children: [ Padding( - padding: EdgeInsets.only(top: _headersRect?.first.height ?? 0), + padding: EdgeInsets.only(top: _headersRect?.last.height ?? 0), child: SingleChildScrollView( scrollDirection: Axis.vertical, child: Transform.translate( - offset: Offset(0, -(_headersRect?.first.height ?? 0)), + offset: Offset(0, -(_headersRect?.last.height ?? 0)), child: Table( columnWidths: tableColumns.asMap(), children: tableRows, diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 4abaa70..d76ae6c 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: easy_localization: ^3.0.0 glass_kit: ^2.0.1 rect_getter: ^1.0.0 + path_provider: ^2.0.5 xdg_directories_web: path: 3rd_party/xdg_directories_web