From f024c39e1fa0971e04d35215debcaa84f6c3cec8 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Fri, 19 Nov 2021 18:45:09 +0800 Subject: [PATCH 01/14] feat: add name text field. --- app/lib/pages/gesture_editor.dart | 28 +++++++++++++- app/lib/widgets/dde_text_field.dart | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 app/lib/widgets/dde_text_field.dart diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index aa74efb..bd56562 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -10,6 +10,7 @@ import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:dde_gesture_manager/utils/keyboard_mapper.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:dde_gesture_manager/widgets/dde_data_table.dart'; +import 'package:dde_gesture_manager/widgets/dde_text_field.dart'; import 'package:dde_gesture_manager/widgets/table_cell_shortcut_listener.dart'; import 'package:dde_gesture_manager/widgets/table_cell_text_field.dart'; import 'package:flutter/cupertino.dart'; @@ -235,7 +236,6 @@ class GestureEditor extends StatelessWidget { ); }), Container( - height: 300, decoration: BoxDecoration( borderRadius: BorderRadius.circular(defaultBorderRadius), border: Border.all( @@ -243,6 +243,30 @@ class GestureEditor extends StatelessWidget { color: context.t.dividerColor, ), ), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text('name:'), + ), + Expanded( + child: DTextField( + onComplete: (val) { + schemeProvider.setProps(name: val); + /// todo: change name to local list. + }, + ), + ), + ], + ), + ], + ), + ), ), ], ), @@ -301,7 +325,7 @@ List _buildRowCellsEditing(BuildContext context) { .where((node) => !node.fullFiled) .map((e) => e.type); if (!availableGestures.any((type) => type == gesture.gesture)) { - availableGestures = [...availableGestures, gesture.gesture!]..sort(); + availableGestures = [...availableGestures, gesture.gesture!]..sort((a, b) => a.index - b.index); } var availableDirection = schemeTree.nodes diff --git a/app/lib/widgets/dde_text_field.dart b/app/lib/widgets/dde_text_field.dart new file mode 100644 index 0000000..91ba092 --- /dev/null +++ b/app/lib/widgets/dde_text_field.dart @@ -0,0 +1,77 @@ +import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/models/settings.provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:dde_gesture_manager/extensions.dart'; + +class DTextField extends StatefulWidget { + final String? initText; + final String? hint; + final Function(String value) onComplete; + + const DTextField({ + Key? key, + this.initText, + this.hint, + required this.onComplete, + }) : super(key: key); + + @override + _DTextFieldState createState() => _DTextFieldState(); +} + +class _DTextFieldState extends State { + final FocusNode _focusNode = FocusNode(); + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + _focusNode.addListener(_handleFocusChange); + _controller.text = widget.initText ?? ''; + super.initState(); + } + + @override + void dispose() { + _focusNode.removeListener(_handleFocusChange); + super.dispose(); + } + + _handleFocusChange() { + if (!_focusNode.hasFocus) { + widget.onComplete(_controller.text); + } + } + + @override + Widget build(BuildContext context) { + return Focus( + child: Builder(builder: (context) { + return Container( + height: kMinInteractiveDimension * .86, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(defaultBorderRadius), + color: Colors.grey.withOpacity(.3), + border: Border.all( + width: 2, + color: Focus.of(context).hasFocus + ? context.watch().activeColor ?? Color(0xff565656) + : Color(0xff565656)), + ), + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 15), + child: TextField( + focusNode: _focusNode, + cursorColor: context.watch().activeColor, + decoration: InputDecoration.collapsed(hintText: widget.hint), + controller: _controller, + ), + ), + ), + ); + }), + ); + } +} -- 2.11.0 From aa367cf3ba6d8dac55cc571e1062613b2daef4a6 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Mon, 22 Nov 2021 18:59:33 +0800 Subject: [PATCH 02/14] feat: save scheme name logic. --- app/lib/models/local_schemes.dart | 4 ++-- app/lib/models/local_schemes_linux.dart | 13 ++++++++++--- app/lib/models/local_schemes_web.dart | 8 +++++++- app/lib/models/scheme.dart | 19 ++++++++++++++++++- app/lib/pages/gesture_editor.dart | 19 ++++++++++++++++++- app/lib/pages/local_manager.dart | 28 +++++++++++++++------------- app/lib/widgets/dde_text_field.dart | 10 +++++++++- 7 files changed, 79 insertions(+), 22 deletions(-) diff --git a/app/lib/models/local_schemes.dart b/app/lib/models/local_schemes.dart index 8d841df..9108a32 100644 --- a/app/lib/models/local_schemes.dart +++ b/app/lib/models/local_schemes.dart @@ -2,7 +2,7 @@ import 'package:dde_gesture_manager/models/scheme.dart'; export 'local_schemes_web.dart' if (dart.library.io) 'local_schemes_linux.dart'; -abstract class LocalSchemeEntry { +abstract class LocalSchemeEntry implements Comparable { Scheme scheme; DateTime lastModifyTime; String path; @@ -25,4 +25,4 @@ abstract class LocalSchemeEntry { abstract class LocalSchemesInterface { Future> get schemeEntries; -} \ No newline at end of file +} diff --git a/app/lib/models/local_schemes_linux.dart b/app/lib/models/local_schemes_linux.dart index 105dd79..e118cb0 100644 --- a/app/lib/models/local_schemes_linux.dart +++ b/app/lib/models/local_schemes_linux.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:dde_gesture_manager/builder/provider_annotation.dart'; @@ -13,7 +14,7 @@ export 'local_schemes.dart'; @ProviderModel() class LocalSchemes implements LocalSchemesInterface { LocalSchemes() { - schemeEntries.then((value) => schemes = [LocalSchemeEntryLinux.systemDefault(), ...value]); + schemeEntries.then((value) => schemes = [LocalSchemeEntryLinux.systemDefault(), ...value..sort()]); } @override @@ -68,7 +69,13 @@ class LocalSchemeEntryLinux implements LocalSchemeEntry { @override save() { - // TODO: implement save - throw UnimplementedError(); + var file = File(path); + file.writeAsStringSync(json.encode(scheme)); + } + + @override + int compareTo(other) { + assert(other is LocalSchemeEntry); + return lastModifyTime.isAfter(other.lastModifyTime) ? -1 : 1; } } diff --git a/app/lib/models/local_schemes_web.dart b/app/lib/models/local_schemes_web.dart index c8c1814..e252c36 100644 --- a/app/lib/models/local_schemes_web.dart +++ b/app/lib/models/local_schemes_web.dart @@ -12,7 +12,7 @@ export 'local_schemes.dart'; @ProviderModel() class LocalSchemes implements LocalSchemesInterface { LocalSchemes() { - schemeEntries.then((value) => schemes = [LocalSchemeEntryWeb.systemDefault(), ...value]); + schemeEntries.then((value) => schemes = [LocalSchemeEntryWeb.systemDefault(), ...value..sort()]); } @override @@ -72,4 +72,10 @@ class LocalSchemeEntryWeb implements LocalSchemeEntry { // TODO: implement save throw UnimplementedError(); } + + @override + int compareTo(other) { + assert(other is LocalSchemeEntry); + return lastModifyTime.isAfter(other.lastModifyTime) ? -1 : 1; + } } diff --git a/app/lib/models/scheme.dart b/app/lib/models/scheme.dart index b88d465..8f9d42c 100644 --- a/app/lib/models/scheme.dart +++ b/app/lib/models/scheme.dart @@ -128,7 +128,7 @@ class Scheme { Scheme.parse(scheme) { if (scheme is String) scheme = json.decode(scheme); assert(scheme is Map); - id = scheme['id']; + id = scheme['id'] ?? Uuid().v1(); name = scheme['name']; description = scheme['desc']; gestures = (scheme['gestures'] as List? ?? []).map((ele) => GestureProp.parse(ele)).toList()..sort(); @@ -154,6 +154,13 @@ class Scheme { }); return schemeTree; } + + Map toJson() => { + 'id': id, + 'name': name, + 'desc': description, + 'gestures': gestures, + }; } enum Gesture { @@ -221,6 +228,16 @@ class GestureProp implements Comparable { return 'GestureProp{gesture: $gesture, direction: $direction, fingers: $fingers, type: $type, command: $command, remark: $remark}'; } + Map toJson() => { + 'id': id, + 'gesture': H.getGestureName(gesture), + 'direction': H.getGestureDirectionName(direction), + 'fingers': fingers, + 'type': H.getGestureTypeName(type), + 'command': command, + 'remark': remark, + }; + GestureProp.empty() : this.id = Uuid.NAMESPACE_NIL; GestureProp.parse(props) { diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index bd56562..21fb172 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -2,6 +2,7 @@ import 'package:adaptive_scrollbar/adaptive_scrollbar.dart'; 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_schemes_provider.dart'; import 'package:dde_gesture_manager/models/scheme.dart'; import 'package:dde_gesture_manager/models/scheme.provider.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; @@ -256,9 +257,25 @@ class GestureEditor extends StatelessWidget { ), Expanded( child: DTextField( + initText: schemeProvider.name, onComplete: (val) { + val = val.trim(); schemeProvider.setProps(name: val); - /// todo: change name to local list. + var localSchemesProvider = context.read(); + if (!localSchemesProvider.schemes!.every((element) => element.scheme.name != val)) { + /// show error info; + 'duplicate name'.sout(); + return; + } + ; + var localSchemeEntry = localSchemesProvider.schemes! + .firstWhere((ele) => ele.scheme.id == schemeProvider.id); + localSchemeEntry.scheme.name = val; + localSchemeEntry.save(); + localSchemesProvider.schemeEntries.then((value) { + localSchemesProvider + .setProps(schemes: [localSchemesProvider.schemes!.first, ...value..sort()]); + }); }, ), ), diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index da9e169..da3fbe9 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -10,6 +10,7 @@ import 'package:flutter/animation.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; +import 'package:uuid/uuid.dart'; class LocalManager extends StatefulWidget { const LocalManager({ @@ -22,22 +23,22 @@ class LocalManager extends StatefulWidget { class _LocalManagerState extends State { late ScrollController _scrollController; - int? _hoveringIndex; - late int _selectedIndex; + String? _hoveringItem; + late String _selectedItem; @override void initState() { super.initState(); /// todo: load from sp - _selectedIndex = 0; + _selectedItem = Uuid.NAMESPACE_NIL; _scrollController = ScrollController(); } - Color _getItemBackgroundColor(int index) { + Color _getItemBackgroundColor(int index, String itemId) { Color _color = index % 2 == 0 ? context.t.scaffoldBackgroundColor : context.t.backgroundColor; - if (index == _hoveringIndex) _color = context.t.scaffoldBackgroundColor; - if (index == _selectedIndex) _color = context.read().currentActiveColor; + if (itemId == _hoveringItem) _color = context.t.scaffoldBackgroundColor; + if (itemId == _selectedItem) _color = context.read().currentActiveColor; return _color; } @@ -110,7 +111,7 @@ class _LocalManagerState extends State { onTap: () { context.read().copyFrom(localSchemes[index].scheme); setState(() { - _selectedIndex = index; + _selectedItem = localSchemes[index].scheme.id!; }); context.read().copyFrom(GestureProp.empty()); }, @@ -118,16 +119,17 @@ class _LocalManagerState extends State { cursor: SystemMouseCursors.click, onEnter: (_) { setState(() { - _hoveringIndex = index; + _hoveringItem = localSchemes[index].scheme.id!; }); }, child: Container( - color: _getItemBackgroundColor(index), + color: _getItemBackgroundColor(index, localSchemes[index].scheme.id!), child: Padding( padding: const EdgeInsets.only(left: 6, right: 12.0), child: DefaultTextStyle( style: context.t.textTheme.bodyText2!.copyWith( - color: _selectedIndex == index ? Colors.white : null, + color: + _selectedItem == localSchemes[index].scheme.id! ? Colors.white : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -152,9 +154,9 @@ class _LocalManagerState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ DButton.add(enabled: true), - DButton.delete(enabled: _selectedIndex > 0), - DButton.duplicate(enabled: _selectedIndex > 0), - DButton.apply(enabled: _selectedIndex > 0), + DButton.delete(enabled: _selectedItem != Uuid.NAMESPACE_NIL), + DButton.duplicate(enabled: _selectedItem != Uuid.NAMESPACE_NIL), + DButton.apply(enabled: _selectedItem != Uuid.NAMESPACE_NIL), ] .map((e) => Padding( padding: const EdgeInsets.only(right: 4), diff --git a/app/lib/widgets/dde_text_field.dart b/app/lib/widgets/dde_text_field.dart index 91ba092..1dd1724 100644 --- a/app/lib/widgets/dde_text_field.dart +++ b/app/lib/widgets/dde_text_field.dart @@ -1,8 +1,8 @@ import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:dde_gesture_manager/extensions.dart'; class DTextField extends StatefulWidget { final String? initText; @@ -44,6 +44,14 @@ class _DTextFieldState extends State { } @override + void didUpdateWidget(covariant DTextField oldWidget) { + if (oldWidget.initText != widget.initText) { + _controller.text = widget.initText ?? ''; + } + super.didUpdateWidget(oldWidget); + } + + @override Widget build(BuildContext context) { return Focus( child: Builder(builder: (context) { -- 2.11.0 From 7977116d755cba37f0f8eeb96c5f45a7a1ce2f4a Mon Sep 17 00:00:00 2001 From: debuggerx Date: Tue, 23 Nov 2021 19:00:37 +0800 Subject: [PATCH 03/14] feat: save scheme gestures logic. --- app/lib/models/local_schemes.dart | 4 +- app/lib/models/local_schemes_linux.dart | 7 ++- app/lib/models/local_schemes_web.dart | 3 +- app/lib/models/scheme.dart | 6 ++ app/lib/pages/gesture_editor.dart | 103 +++++++++++++++++++------------- app/lib/pages/home.dart | 50 +--------------- app/lib/widgets/dde_button.dart | 2 +- app/lib/widgets/dde_text_field.dart | 5 +- 8 files changed, 85 insertions(+), 95 deletions(-) diff --git a/app/lib/models/local_schemes.dart b/app/lib/models/local_schemes.dart index 9108a32..9a79be2 100644 --- a/app/lib/models/local_schemes.dart +++ b/app/lib/models/local_schemes.dart @@ -1,5 +1,7 @@ import 'package:dde_gesture_manager/models/scheme.dart'; +import 'local_schemes_provider.dart'; + export 'local_schemes_web.dart' if (dart.library.io) 'local_schemes_linux.dart'; abstract class LocalSchemeEntry implements Comparable { @@ -20,7 +22,7 @@ abstract class LocalSchemeEntry implements Comparable { /// max value of DateTime ![Time Values and Time Range](https://262.ecma-international.org/11.0/#sec-time-values-and-time-range) this.lastModifyTime = DateTime.fromMillisecondsSinceEpoch(8640000000000000); - save(); + save(LocalSchemesProvider provider); } abstract class LocalSchemesInterface { diff --git a/app/lib/models/local_schemes_linux.dart b/app/lib/models/local_schemes_linux.dart index e118cb0..e74d98e 100644 --- a/app/lib/models/local_schemes_linux.dart +++ b/app/lib/models/local_schemes_linux.dart @@ -8,6 +8,7 @@ import 'package:path/path.dart' show join; import 'package:path_provider/path_provider.dart'; import 'local_schemes.dart'; +import 'local_schemes_provider.dart'; export 'local_schemes.dart'; @@ -68,9 +69,11 @@ class LocalSchemeEntryLinux implements LocalSchemeEntry { this.lastModifyTime = DateTime.fromMillisecondsSinceEpoch(8640000000000000); @override - save() { + save(LocalSchemesProvider provider) { var file = File(path); - file.writeAsStringSync(json.encode(scheme)); + file.writeAsStringSync(JsonEncoder.withIndent(' ' * 4).convert(scheme)); + provider.schemes!.firstWhere((ele) => ele.scheme.id == scheme.id).lastModifyTime = DateTime.now(); + provider.setProps(schemes: [...provider.schemes!]..sort()); } @override diff --git a/app/lib/models/local_schemes_web.dart b/app/lib/models/local_schemes_web.dart index e252c36..582141c 100644 --- a/app/lib/models/local_schemes_web.dart +++ b/app/lib/models/local_schemes_web.dart @@ -3,6 +3,7 @@ import 'dart:html'; import 'package:dde_gesture_manager/builder/provider_annotation.dart'; import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/models/local_schemes_provider.dart'; import 'package:dde_gesture_manager/models/scheme.dart'; import 'local_schemes.dart'; @@ -68,7 +69,7 @@ class LocalSchemeEntryWeb implements LocalSchemeEntry { this.lastModifyTime = DateTime.fromMillisecondsSinceEpoch(8640000000000000); @override - save() { + save(LocalSchemesProvider provider) { // TODO: implement save throw UnimplementedError(); } diff --git a/app/lib/models/scheme.dart b/app/lib/models/scheme.dart index 8f9d42c..d6cfde6 100644 --- a/app/lib/models/scheme.dart +++ b/app/lib/models/scheme.dart @@ -117,6 +117,9 @@ class Scheme { String? id; @ProviderModelProp() + bool? readOnly; + + @ProviderModelProp() String? name; @ProviderModelProp() @@ -129,6 +132,7 @@ class Scheme { if (scheme is String) scheme = json.decode(scheme); assert(scheme is Map); id = scheme['id'] ?? Uuid().v1(); + readOnly = scheme['readOnly'] ?? false; name = scheme['name']; description = scheme['desc']; gestures = (scheme['gestures'] as List? ?? []).map((ele) => GestureProp.parse(ele)).toList()..sort(); @@ -136,6 +140,7 @@ class Scheme { Scheme.systemDefault() { this.id = Uuid.NAMESPACE_NIL; + this.readOnly = true; this.name = LocaleKeys.local_manager_default_scheme_label.tr(); this.description = LocaleKeys.local_manager_default_scheme_description.tr(); this.gestures = []; @@ -157,6 +162,7 @@ class Scheme { Map toJson() => { 'id': id, + 'readOnly': readOnly, 'name': name, 'desc': description, 'gestures': gestures, diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index 21fb172..57a350b 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -126,27 +126,32 @@ class GestureEditor extends StatelessWidget { controller: horizontalCtrl, child: ConstrainedBox( constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: DDataTable( - showBottomBorder: true, - headingRowHeight: _headingRowHeight, - showCheckboxColumn: true, - headerBackgroundColor: context.t.dialogBackgroundColor, - verticalScrollController: verticalCtrl, - dataRowColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.hovered)) return context.t.dialogBackgroundColor; - if (states.contains(MaterialState.selected)) - return context.read().currentActiveColor; - return null; - }), - columns: [ - DDataColumn(label: Text(LocaleKeys.gesture_editor_fingers.tr()), center: true), - 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_type.tr()), center: true), - DDataColumn(label: Text(LocaleKeys.gesture_editor_command.tr())), - DDataColumn(label: Text(LocaleKeys.gesture_editor_remark.tr())), - ], - rows: _buildDataRows(schemeProvider.gestures, context), + child: IgnorePointer( + ignoring: schemeProvider.readOnly ?? false, + child: DDataTable( + showBottomBorder: true, + headingRowHeight: _headingRowHeight, + showCheckboxColumn: true, + headerBackgroundColor: context.t.dialogBackgroundColor, + verticalScrollController: verticalCtrl, + dataRowColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.hovered)) + return context.t.dialogBackgroundColor; + if (states.contains(MaterialState.selected)) + return context.read().currentActiveColor; + return null; + }), + columns: [ + DDataColumn(label: Text(LocaleKeys.gesture_editor_fingers.tr()), center: true), + 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_type.tr()), center: true), + DDataColumn(label: Text(LocaleKeys.gesture_editor_command.tr())), + DDataColumn(label: Text(LocaleKeys.gesture_editor_remark.tr())), + ], + rows: _buildDataRows(schemeProvider.gestures, context), + ), ), ), ), @@ -164,18 +169,23 @@ class GestureEditor extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ DButton.add( - enabled: !gesturePropProvider.editMode! && !schemeTree.fullFiled, + enabled: !(schemeProvider.readOnly ?? false) && + !gesturePropProvider.editMode! && + !schemeTree.fullFiled, onTap: () { var schemeProvider = context.read(); - context.read().setProps( - gestures: [ - ...?schemeProvider.gestures, - H.getNextAvailableGestureProp(schemeProvider.buildSchemeTree())!, - ]..sort()); + var newGestures = [ + ...?schemeProvider.gestures, + H.getNextAvailableGestureProp(schemeProvider.buildSchemeTree())!, + ]..sort(); + context.read().setProps(gestures: newGestures); + saveGesturesToLocal(context, schemeProvider, newGestures); }, ), DButton.delete( - enabled: gesturePropProvider != GestureProp.empty() && !gesturePropProvider.editMode!, + enabled: !(schemeProvider.readOnly ?? false) && + gesturePropProvider != GestureProp.empty() && + !gesturePropProvider.editMode!, onTap: () { var schemeProvider = context.read(); var index = schemeProvider.gestures?.indexWhere((e) => e.id == gesturePropProvider.id); @@ -187,10 +197,13 @@ class GestureEditor extends StatelessWidget { gesturePropProvider.copyFrom( newGestures[(index ?? 0) > newGestures.length - 1 ? newGestures.length - 1 : index ?? 0] ..editMode = false); + saveGesturesToLocal(context, schemeProvider, newGestures); }, ), DButton.duplicate( - enabled: gesturePropProvider != GestureProp.empty() && !gesturePropProvider.editMode!, + enabled: !(schemeProvider.readOnly ?? false) && + gesturePropProvider != GestureProp.empty() && + !gesturePropProvider.editMode!, onTap: () { var schemeProvider = context.read(); context.read().copyFrom( @@ -200,7 +213,8 @@ class GestureEditor extends StatelessWidget { }, ), DButton.paste( - enabled: copiedGesturePropProvider != CopiedGesturePropProvider.empty() && + enabled: !(schemeProvider.readOnly ?? false) && + copiedGesturePropProvider != CopiedGesturePropProvider.empty() && !gesturePropProvider.editMode! && !schemeTree.fullFiled, onTap: () { @@ -221,11 +235,12 @@ class GestureEditor extends StatelessWidget { newGestureProp.remark = copiedGesturePropProvider.remark; } newGestureProp.id = Uuid().v1(); - context.read().setProps( - gestures: [ - ...?schemeProvider.gestures, - newGestureProp, - ]..sort()); + var newGestures = [ + ...?schemeProvider.gestures, + newGestureProp, + ]..sort(); + context.read().setProps(gestures: newGestures); + saveGesturesToLocal(context, schemeProvider, newGestures); }, ), ] @@ -258,6 +273,7 @@ class GestureEditor extends StatelessWidget { Expanded( child: DTextField( initText: schemeProvider.name, + readOnly: schemeProvider.readOnly ?? false, onComplete: (val) { val = val.trim(); schemeProvider.setProps(name: val); @@ -271,11 +287,7 @@ class GestureEditor extends StatelessWidget { var localSchemeEntry = localSchemesProvider.schemes! .firstWhere((ele) => ele.scheme.id == schemeProvider.id); localSchemeEntry.scheme.name = val; - localSchemeEntry.save(); - localSchemesProvider.schemeEntries.then((value) { - localSchemesProvider - .setProps(schemes: [localSchemesProvider.schemes!.first, ...value..sort()]); - }); + localSchemeEntry.save(localSchemesProvider); }, ), ), @@ -314,9 +326,11 @@ List _buildDataRows(List? gestures, BuildContext context) var newGestures = List.of(schemeProvider.gestures!); var index = newGestures.indexWhere((element) => element == prop); newGestures[index].copyFrom(prop); + newGestures.sort(); context.read().setProps( - gestures: newGestures..sort(), + gestures: newGestures, ); + saveGesturesToLocal(context, schemeProvider, newGestures); }; provider.copyFrom( gesture..editMode = true, @@ -328,6 +342,13 @@ List _buildDataRows(List? gestures, BuildContext context) ); }).toList(); +void saveGesturesToLocal(BuildContext context, SchemeProvider schemeProvider, List newGestures) { + var localSchemesProvider = context.read(); + var localSchemeEntry = localSchemesProvider.schemes!.firstWhere((ele) => ele.scheme.id == schemeProvider.id); + localSchemeEntry.scheme.gestures = newGestures; + localSchemeEntry.save(localSchemesProvider); +} + List _buildRowCellsEditing(BuildContext context) { var gesture = context.read(); var schemeTree = context.read().buildSchemeTree(); diff --git a/app/lib/pages/home.dart b/app/lib/pages/home.dart index f425962..8efcf89 100644 --- a/app/lib/pages/home.dart +++ b/app/lib/pages/home.dart @@ -18,55 +18,9 @@ class _HomePageState extends State { return Scaffold( body: MultiProvider( providers: [ - ChangeNotifierProvider(create: (context) => SchemeProvider.parse(''' - { - "name": "test", - "desc": "some desc", - "gestures": [ - { - "gesture": "swipe", - "direction": "down", - "fingers": 3, - "type": "shortcut", - "command": "Control_L+w", - "remark": "close current page." - }, - { - "gesture": "swipe", - "direction": "up", - "fingers": 3, - "type": "shortcut", - "command": "Control_L+Alt_L+t", - "remark": "reopen last closed page." - }, - { - "gesture": "pinch", - "direction": "in", - "fingers": 4, - "type": "shortcut", - "command": "Control_L+Alt_L+f", - "remark": "search files." - }, - { - "gesture": "tap", - "fingers": 4, - "type": "built_in", - "command": "handle4FingersTap", - "remark": "handle4FingersTap." - }, - { - "gesture": "swipe", - "direction": "down", - "fingers": 5, - "type": "commandline", - "command": "dbus-send --type=method_call --dest=com.deepin.dde.Launcher /com/deepin/dde/Launcher com.deepin.dde.Launcher.Toggle", - "remark": "toggle launcher." - } - ] - } - ''')), + ChangeNotifierProvider(create: (context) => SchemeProvider.systemDefault()), ChangeNotifierProvider(create: (context) => GesturePropProvider.empty()), - ChangeNotifierProvider(create: (context) => LocalSchemesProvider(),lazy: false), + ChangeNotifierProvider(create: (context) => LocalSchemesProvider(), lazy: false), ], child: Column( mainAxisSize: MainAxisSize.max, diff --git a/app/lib/widgets/dde_button.dart b/app/lib/widgets/dde_button.dart index 38bcbc0..7aad5ad 100644 --- a/app/lib/widgets/dde_button.dart +++ b/app/lib/widgets/dde_button.dart @@ -143,7 +143,7 @@ class _DButtonState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, ), - borderColor: _hovering + borderColor: _hovering && widget.onTap != null ? (widget.activeBorderColor ?? context.watch().currentActiveColor) : Color(0xff565656), borderWidth: 2, diff --git a/app/lib/widgets/dde_text_field.dart b/app/lib/widgets/dde_text_field.dart index 1dd1724..17fd26d 100644 --- a/app/lib/widgets/dde_text_field.dart +++ b/app/lib/widgets/dde_text_field.dart @@ -7,12 +7,14 @@ import 'package:provider/provider.dart'; class DTextField extends StatefulWidget { final String? initText; final String? hint; + final bool readOnly; final Function(String value) onComplete; const DTextField({ Key? key, this.initText, this.hint, + this.readOnly = false, required this.onComplete, }) : super(key: key); @@ -62,7 +64,7 @@ class _DTextFieldState extends State { color: Colors.grey.withOpacity(.3), border: Border.all( width: 2, - color: Focus.of(context).hasFocus + color: Focus.of(context).hasFocus && !widget.readOnly ? context.watch().activeColor ?? Color(0xff565656) : Color(0xff565656)), ), @@ -71,6 +73,7 @@ class _DTextFieldState extends State { child: Padding( padding: const EdgeInsets.only(left: 15), child: TextField( + readOnly: widget.readOnly, focusNode: _focusNode, cursorColor: context.watch().activeColor, decoration: InputDecoration.collapsed(hintText: widget.hint), -- 2.11.0 From 6dfec40c3fbf68161f4318d96180817d412a10a1 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Thu, 2 Dec 2021 19:11:43 +0800 Subject: [PATCH 04/14] feat: create and remove local scheme file logic. --- app/lib/models/local_schemes.dart | 4 +++ app/lib/models/local_schemes_linux.dart | 16 +++++++++++ app/lib/models/local_schemes_web.dart | 18 +++++++++++-- app/lib/models/scheme.dart | 3 +++ app/lib/pages/local_manager.dart | 47 ++++++++++++++++++++++++--------- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/app/lib/models/local_schemes.dart b/app/lib/models/local_schemes.dart index 9a79be2..2f6684c 100644 --- a/app/lib/models/local_schemes.dart +++ b/app/lib/models/local_schemes.dart @@ -27,4 +27,8 @@ abstract class LocalSchemeEntry implements Comparable { abstract class LocalSchemesInterface { Future> get schemeEntries; + + Future create(); + + void remove(String path); } diff --git a/app/lib/models/local_schemes_linux.dart b/app/lib/models/local_schemes_linux.dart index e74d98e..aefe39b 100644 --- a/app/lib/models/local_schemes_linux.dart +++ b/app/lib/models/local_schemes_linux.dart @@ -6,6 +6,7 @@ import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/scheme.dart'; import 'package:path/path.dart' show join; import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; import 'local_schemes.dart'; import 'local_schemes_provider.dart'; @@ -43,6 +44,21 @@ class LocalSchemes implements LocalSchemesInterface { @ProviderModelProp() List? schemes; + + @override + Future create() async { + var _supportDirectory = await getApplicationSupportDirectory(); + return LocalSchemeEntryLinux( + path: join(_supportDirectory.path, 'schemes', '${Uuid().v1()}.json'), + scheme: Scheme.create(), + lastModifyTime: DateTime.now(), + ); + } + + @override + void remove(String path) { + File(path).delete(); + } } class LocalSchemeEntryLinux implements LocalSchemeEntry { diff --git a/app/lib/models/local_schemes_web.dart b/app/lib/models/local_schemes_web.dart index 582141c..efa4cbf 100644 --- a/app/lib/models/local_schemes_web.dart +++ b/app/lib/models/local_schemes_web.dart @@ -5,6 +5,7 @@ import 'package:dde_gesture_manager/builder/provider_annotation.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/local_schemes_provider.dart'; import 'package:dde_gesture_manager/models/scheme.dart'; +import 'package:uuid/uuid.dart'; import 'local_schemes.dart'; @@ -43,6 +44,18 @@ class LocalSchemes implements LocalSchemesInterface { @ProviderModelProp() List? schemes; + + @override + Future create() => Future.value( + LocalSchemeEntryWeb( + path: Uuid().v1(), + scheme: Scheme.create(), + lastModifyTime: DateTime.now(), + ), + ); + + @override + remove(String path) => window.localStorage.remove(path); } class LocalSchemeEntryWeb implements LocalSchemeEntry { @@ -70,8 +83,9 @@ class LocalSchemeEntryWeb implements LocalSchemeEntry { @override save(LocalSchemesProvider provider) { - // TODO: implement save - throw UnimplementedError(); + window.localStorage[path] = JsonEncoder.withIndent(' ' * 4).convert(scheme); + provider.schemes!.firstWhere((ele) => ele.scheme.id == scheme.id).lastModifyTime = DateTime.now(); + provider.setProps(schemes: [...provider.schemes!]..sort()); } @override diff --git a/app/lib/models/scheme.dart b/app/lib/models/scheme.dart index d6cfde6..7482b83 100644 --- a/app/lib/models/scheme.dart +++ b/app/lib/models/scheme.dart @@ -148,6 +148,9 @@ class Scheme { Scheme.create({this.name, this.description, this.gestures}) { this.id = Uuid().v1(); + this.gestures = []; + this.readOnly = false; + this.name = 'new xxx'; } SchemeTree buildSchemeTree() { diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index da3fbe9..e8dcb72 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -8,6 +8,7 @@ import 'package:dde_gesture_manager/models/settings.provider.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:flutter/animation.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; import 'package:uuid/uuid.dart'; @@ -24,21 +25,21 @@ class LocalManager extends StatefulWidget { class _LocalManagerState extends State { late ScrollController _scrollController; String? _hoveringItem; - late String _selectedItem; + late String _selectedItemPath; @override void initState() { super.initState(); /// todo: load from sp - _selectedItem = Uuid.NAMESPACE_NIL; + _selectedItemPath = ''; _scrollController = ScrollController(); } - Color _getItemBackgroundColor(int index, String itemId) { + Color _getItemBackgroundColor(int index, String itemPath) { Color _color = index % 2 == 0 ? context.t.scaffoldBackgroundColor : context.t.backgroundColor; - if (itemId == _hoveringItem) _color = context.t.scaffoldBackgroundColor; - if (itemId == _selectedItem) _color = context.read().currentActiveColor; + if (itemPath == _hoveringItem) _color = context.t.scaffoldBackgroundColor; + if (itemPath == _selectedItemPath) _color = context.read().currentActiveColor; return _color; } @@ -111,7 +112,7 @@ class _LocalManagerState extends State { onTap: () { context.read().copyFrom(localSchemes[index].scheme); setState(() { - _selectedItem = localSchemes[index].scheme.id!; + _selectedItemPath = localSchemes[index].path; }); context.read().copyFrom(GestureProp.empty()); }, @@ -123,13 +124,12 @@ class _LocalManagerState extends State { }); }, child: Container( - color: _getItemBackgroundColor(index, localSchemes[index].scheme.id!), + color: _getItemBackgroundColor(index, localSchemes[index].path), child: Padding( padding: const EdgeInsets.only(left: 6, right: 12.0), child: DefaultTextStyle( style: context.t.textTheme.bodyText2!.copyWith( - color: - _selectedItem == localSchemes[index].scheme.id! ? Colors.white : null, + color: _selectedItemPath == localSchemes[index].path ? Colors.white : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -153,10 +153,31 @@ class _LocalManagerState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - DButton.add(enabled: true), - DButton.delete(enabled: _selectedItem != Uuid.NAMESPACE_NIL), - DButton.duplicate(enabled: _selectedItem != Uuid.NAMESPACE_NIL), - DButton.apply(enabled: _selectedItem != Uuid.NAMESPACE_NIL), + DButton.add( + enabled: true, + onTap: () async { + var localSchemesProvider = context.read(); + var newSchemes = [...?localSchemesProvider.schemes]; + newSchemes.add(await localSchemesProvider.create()); + localSchemesProvider.setProps(schemes: newSchemes..sort()); + }, + ), + DButton.delete( + enabled: _selectedItemPath.notNull, + onTap: () { + var localSchemesProvider = context.read(); + var newSchemes = [...?localSchemesProvider.schemes]; + var index = newSchemes.indexWhere((element) => element.path == _selectedItemPath); + newSchemes.removeAt(index); + localSchemesProvider.setProps(schemes: newSchemes); + localSchemesProvider.remove(_selectedItemPath); + setState(() { + _selectedItemPath = newSchemes[(index - 1).clamp(1, newSchemes.length)].path; + }); + }, + ), + DButton.duplicate(enabled: _selectedItemPath.notNull), + DButton.apply(enabled: _selectedItemPath.notNull), ] .map((e) => Padding( padding: const EdgeInsets.only(right: 4), -- 2.11.0 From 9a217662911e87c99b068a8db206870b7204c973 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Thu, 9 Dec 2021 17:47:22 +0800 Subject: [PATCH 05/14] feat: use 'fromMarket' and 'uploaded' flag instead of 'readOnly'; code cleanup. --- app/lib/extensions/context_extension.dart | 1 - app/lib/main.dart | 3 --- app/lib/models/scheme.dart | 17 ++++++++++++----- app/lib/pages/gesture_editor.dart | 12 ++++++------ app/lib/pages/local_manager.dart | 4 ---- app/lib/utils/init_linux.dart | 2 -- app/lib/widgets/dde_data_table.dart | 13 +++++-------- app/lib/widgets/language_switcher.dart | 3 +-- app/lib/widgets/theme_switcher.dart | 2 -- app/lib/widgets/version_checker.dart | 1 - 10 files changed, 24 insertions(+), 34 deletions(-) diff --git a/app/lib/extensions/context_extension.dart b/app/lib/extensions/context_extension.dart index 5607fb6..d887823 100644 --- a/app/lib/extensions/context_extension.dart +++ b/app/lib/extensions/context_extension.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; extension ContextExtension on BuildContext { ThemeData get t => Theme.of(this); diff --git a/app/lib/main.dart b/app/lib/main.dart index 30302f1..eeca30d 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -3,7 +3,6 @@ import 'package:dde_gesture_manager/constants/sp_keys.dart'; import 'package:dde_gesture_manager/constants/supported_locales.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/generated/codegen_loader.g.dart'; -import 'package:dde_gesture_manager/generated/locale_keys.g.dart'; import 'package:dde_gesture_manager/models/configs.dart'; import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; @@ -11,9 +10,7 @@ import 'package:dde_gesture_manager/themes/dark.dart'; import 'package:dde_gesture_manager/themes/light.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:dde_gesture_manager/utils/init.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'pages/home.dart'; diff --git a/app/lib/models/scheme.dart b/app/lib/models/scheme.dart index 7482b83..b09be41 100644 --- a/app/lib/models/scheme.dart +++ b/app/lib/models/scheme.dart @@ -117,7 +117,10 @@ class Scheme { String? id; @ProviderModelProp() - bool? readOnly; + bool? fromMarket; + + @ProviderModelProp() + bool? uploaded; @ProviderModelProp() String? name; @@ -128,11 +131,14 @@ class Scheme { @ProviderModelProp() List? gestures; + bool get readOnly => uploaded == true || fromMarket == true || id == Uuid.NAMESPACE_NIL; + Scheme.parse(scheme) { if (scheme is String) scheme = json.decode(scheme); assert(scheme is Map); id = scheme['id'] ?? Uuid().v1(); - readOnly = scheme['readOnly'] ?? false; + fromMarket = scheme['fromMarket'] ?? false; + uploaded = scheme['uploaded'] ?? false; name = scheme['name']; description = scheme['desc']; gestures = (scheme['gestures'] as List? ?? []).map((ele) => GestureProp.parse(ele)).toList()..sort(); @@ -140,7 +146,6 @@ class Scheme { Scheme.systemDefault() { this.id = Uuid.NAMESPACE_NIL; - this.readOnly = true; this.name = LocaleKeys.local_manager_default_scheme_label.tr(); this.description = LocaleKeys.local_manager_default_scheme_description.tr(); this.gestures = []; @@ -149,7 +154,8 @@ class Scheme { Scheme.create({this.name, this.description, this.gestures}) { this.id = Uuid().v1(); this.gestures = []; - this.readOnly = false; + this.fromMarket = false; + this.uploaded = false; this.name = 'new xxx'; } @@ -165,7 +171,8 @@ class Scheme { Map toJson() => { 'id': id, - 'readOnly': readOnly, + 'fromMarket': fromMarket, + 'uploaded': uploaded, 'name': name, 'desc': description, 'gestures': gestures, diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index 57a350b..92a9852 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -127,7 +127,7 @@ class GestureEditor extends StatelessWidget { child: ConstrainedBox( constraints: BoxConstraints(minWidth: constraints.maxWidth), child: IgnorePointer( - ignoring: schemeProvider.readOnly ?? false, + ignoring: schemeProvider.readOnly, child: DDataTable( showBottomBorder: true, headingRowHeight: _headingRowHeight, @@ -169,7 +169,7 @@ class GestureEditor extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ DButton.add( - enabled: !(schemeProvider.readOnly ?? false) && + enabled: !schemeProvider.readOnly && !gesturePropProvider.editMode! && !schemeTree.fullFiled, onTap: () { @@ -183,7 +183,7 @@ class GestureEditor extends StatelessWidget { }, ), DButton.delete( - enabled: !(schemeProvider.readOnly ?? false) && + enabled: !schemeProvider.readOnly && gesturePropProvider != GestureProp.empty() && !gesturePropProvider.editMode!, onTap: () { @@ -201,7 +201,7 @@ class GestureEditor extends StatelessWidget { }, ), DButton.duplicate( - enabled: !(schemeProvider.readOnly ?? false) && + enabled: !schemeProvider.readOnly && gesturePropProvider != GestureProp.empty() && !gesturePropProvider.editMode!, onTap: () { @@ -213,7 +213,7 @@ class GestureEditor extends StatelessWidget { }, ), DButton.paste( - enabled: !(schemeProvider.readOnly ?? false) && + enabled: !schemeProvider.readOnly && copiedGesturePropProvider != CopiedGesturePropProvider.empty() && !gesturePropProvider.editMode! && !schemeTree.fullFiled, @@ -273,7 +273,7 @@ class GestureEditor extends StatelessWidget { Expanded( child: DTextField( initText: schemeProvider.name, - readOnly: schemeProvider.readOnly ?? false, + readOnly: schemeProvider.readOnly, onComplete: (val) { val = val.trim(); schemeProvider.setProps(name: val); diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index e8dcb72..aded155 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -6,12 +6,8 @@ import 'package:dde_gesture_manager/models/scheme.dart'; import 'package:dde_gesture_manager/models/scheme.provider.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; -import 'package:flutter/animation.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/painting.dart'; -import 'package:uuid/uuid.dart'; class LocalManager extends StatefulWidget { const LocalManager({ diff --git a/app/lib/utils/init_linux.dart b/app/lib/utils/init_linux.dart index 3544dfd..7a553fe 100644 --- a/app/lib/utils/init_linux.dart +++ b/app/lib/utils/init_linux.dart @@ -3,12 +3,10 @@ import 'package:dde_gesture_manager/constants/sp_keys.dart'; import 'package:dde_gesture_manager/constants/supported_locales.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/generated/codegen_loader.g.dart'; -import 'package:dde_gesture_manager/generated/locale_keys.g.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:flutter/material.dart'; import 'package:gsettings/gsettings.dart'; -import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; Future initEvents(BuildContext context) async { diff --git a/app/lib/widgets/dde_data_table.dart b/app/lib/widgets/dde_data_table.dart index 5a7b90a..b4e6e96 100644 --- a/app/lib/widgets/dde_data_table.dart +++ b/app/lib/widgets/dde_data_table.dart @@ -6,10 +6,8 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:rect_getter/rect_getter.dart'; /// Signature for [DataColumn.onSort] callback. @@ -885,8 +883,8 @@ class _DDataTableState extends State { final double effectiveColumnSpacing = widget.columnSpacing ?? theme.dataTableTheme.columnSpacing ?? DDataTable._columnSpacing; - final List tableColumns = List.filled( - widget.columns.length, const _NullTableColumnWidth()); + final List tableColumns = + List.filled(widget.columns.length, const _NullTableColumnWidth()); final List tableRows = List.generate( widget.rows.length + 1, // the +1 is for the header row (int index) { @@ -1007,7 +1005,7 @@ class _DDataTableState extends State { colors: [ _headerBackgroundHSLColor .withLightness( - _headerBackgroundHSLColor.lightness - 0.1 < 0 ? 0 : _headerBackgroundHSLColor.lightness - 0.1) + _headerBackgroundHSLColor.lightness - 0.1 < 0 ? 0 : _headerBackgroundHSLColor.lightness - 0.1) .toColor(), widget.headerBackgroundColor, ], @@ -1067,8 +1065,7 @@ class _DDataTableState extends State { _headersRect = _rects; }); } - if (_rects == null || _rects.isEmpty || _rects.first == null) - Future.microtask(() => _buildHeaderStack(tableRows)); + if (_rects == null || _rects.isEmpty || _rects.first == null) Future.microtask(() => _buildHeaderStack(tableRows)); } } diff --git a/app/lib/widgets/language_switcher.dart b/app/lib/widgets/language_switcher.dart index e98317d..d0ccfb5 100644 --- a/app/lib/widgets/language_switcher.dart +++ b/app/lib/widgets/language_switcher.dart @@ -2,13 +2,12 @@ import 'package:dde_gesture_manager/constants/sp_keys.dart'; import 'package:dde_gesture_manager/constants/supported_locales.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/generated/codegen_loader.g.dart'; -import 'package:dde_gesture_manager/generated/locale_keys.g.dart'; import 'package:dde_gesture_manager/models/local_schemes_provider.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; +import 'package:easy_localization/src/translations.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:easy_localization/src/translations.dart'; class LanguageSwitcher extends StatelessWidget { const LanguageSwitcher({Key? key}) : super(key: key); diff --git a/app/lib/widgets/theme_switcher.dart b/app/lib/widgets/theme_switcher.dart index 3ad2537..95b1748 100644 --- a/app/lib/widgets/theme_switcher.dart +++ b/app/lib/widgets/theme_switcher.dart @@ -1,8 +1,6 @@ import 'package:dde_gesture_manager/extensions.dart'; -import 'package:dde_gesture_manager/generated/locale_keys.g.dart'; import 'package:dde_gesture_manager/models/configs.dart'; import 'package:dde_gesture_manager/models/configs.provider.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class ThemeSwitcher extends StatelessWidget { diff --git a/app/lib/widgets/version_checker.dart b/app/lib/widgets/version_checker.dart index 763a021..7aeb7d3 100644 --- a/app/lib/widgets/version_checker.dart +++ b/app/lib/widgets/version_checker.dart @@ -1,7 +1,6 @@ import 'package:dde_gesture_manager/extensions.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:dde_gesture_manager/generated/locale_keys.g.dart'; class VersionChecker extends StatelessWidget { const VersionChecker({Key? key}) : super(key: key); -- 2.11.0 From 1f542cb585c5d07900fd1212bb7efb124695b562 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Thu, 9 Dec 2021 18:47:35 +0800 Subject: [PATCH 06/14] wip: apply scheme logic. --- app/lib/constants/sp_keys.dart | 1 + app/lib/models/configs.dart | 4 +++ app/lib/pages/local_manager.dart | 55 ++++++++++++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/app/lib/constants/sp_keys.dart b/app/lib/constants/sp_keys.dart index 846f957..e3140a4 100644 --- a/app/lib/constants/sp_keys.dart +++ b/app/lib/constants/sp_keys.dart @@ -1,4 +1,5 @@ class SPKeys { static final String brightnessMode = 'BRIGHTNESS_MODE'; + static final String appliedSchemeId = 'APPLIED_SCHEME_ID'; static final String userLanguage = 'USER_LANGUAGE'; } diff --git a/app/lib/models/configs.dart b/app/lib/models/configs.dart index 7df082f..691756b 100644 --- a/app/lib/models/configs.dart +++ b/app/lib/models/configs.dart @@ -13,8 +13,12 @@ class Configs { @ProviderModelProp() BrightnessMode? brightnessMode; + @ProviderModelProp() + String? appliedSchemeId; + Configs() { this.brightnessMode = BrightnessMode.values[H().sp.getInt(SPKeys.brightnessMode)?.clamp(0, BrightnessMode.values.length - 1) ?? 0]; + this.appliedSchemeId = H().sp.getString(SPKeys.appliedSchemeId); } } diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index aded155..5e7dc57 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -1,13 +1,19 @@ +import 'package:collection/collection.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/constants/sp_keys.dart'; import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/content_layout.provider.dart'; +import 'package:dde_gesture_manager/models/local_schemes.dart'; import 'package:dde_gesture_manager/models/local_schemes_provider.dart'; import 'package:dde_gesture_manager/models/scheme.dart'; import 'package:dde_gesture_manager/models/scheme.provider.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; +import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:uuid/uuid.dart'; class LocalManager extends StatefulWidget { const LocalManager({ @@ -27,9 +33,20 @@ class _LocalManagerState extends State { void initState() { super.initState(); - /// todo: load from sp _selectedItemPath = ''; _scrollController = ScrollController(); + + context.read().schemeEntries.then((_) { + var localSchemes = context.read().schemes ?? []; + var appliedSchemeId = context.read().appliedSchemeId; + var appliedScheme = localSchemes.firstWhereOrNull((ele) => ele.scheme.id == appliedSchemeId); + if (appliedScheme != null) { + setState(() { + _selectedItemPath = appliedScheme.path; + }); + _handleItemClick(context, appliedScheme); + } + }); } Color _getItemBackgroundColor(int index, String itemPath) { @@ -39,6 +56,22 @@ class _LocalManagerState extends State { return _color; } + Icon _getItemIcon(Scheme scheme, String? appliedId) { + if (scheme.id == appliedId) return Icon(Icons.done_rounded, size: 22); + if (scheme.id == Uuid.NAMESPACE_NIL) return Icon(Icons.restore_rounded, size: 22); + if (scheme.fromMarket == true) return Icon(Icons.local_grocery_store_rounded, size: 20); + if (scheme.uploaded == true) return Icon(Icons.cloud_done_rounded, size: 18); + return Icon(Icons.person_rounded, size: 22); + } + + void _handleItemClick(BuildContext context, LocalSchemeEntry localScheme) { + context.read().copyFrom(localScheme.scheme); + setState(() { + _selectedItemPath = localScheme.path; + }); + context.read().copyFrom(GestureProp.empty()); + } + @override Widget build(BuildContext context) { var isOpen = context.watch().localManagerOpened == true; @@ -106,11 +139,7 @@ class _LocalManagerState extends State { controller: _scrollController, itemBuilder: (context, index) => GestureDetector( onTap: () { - context.read().copyFrom(localSchemes[index].scheme); - setState(() { - _selectedItemPath = localSchemes[index].path; - }); - context.read().copyFrom(GestureProp.empty()); + _handleItemClick(context, localSchemes[index]); }, child: MouseRegion( cursor: SystemMouseCursors.click, @@ -131,7 +160,8 @@ class _LocalManagerState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(localSchemes[index].scheme.name ?? ''), - Text('456'), + _getItemIcon(localSchemes[index].scheme, + context.watch().appliedSchemeId), ], ), ), @@ -173,7 +203,16 @@ class _LocalManagerState extends State { }, ), DButton.duplicate(enabled: _selectedItemPath.notNull), - DButton.apply(enabled: _selectedItemPath.notNull), + DButton.apply( + enabled: _selectedItemPath.notNull, + onTap: () { + var appliedId = + localSchemes.firstWhere((ele) => ele.path == _selectedItemPath).scheme.id!; + appliedId.sout(); + H().sp.updateString(SPKeys.appliedSchemeId, appliedId); + context.read().setProps(appliedSchemeId: appliedId); + }, + ), ] .map((e) => Padding( padding: const EdgeInsets.only(right: 4), -- 2.11.0 From cbaf61f70b85950b0e161f91a8e584dfb15c7f34 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Tue, 14 Dec 2021 17:54:07 +0800 Subject: [PATCH 07/14] wip: add notificator util. --- app/3rd_party/cherry_toast/CHANGELOG.md | 19 + app/3rd_party/cherry_toast/LICENSE | 674 +++++++++++++++++++++ app/3rd_party/cherry_toast/README.md | 202 ++++++ app/3rd_party/cherry_toast/lib/cherry_toast.dart | 515 ++++++++++++++++ .../cherry_toast/lib/cherry_toast_icon.dart | 90 +++ .../cherry_toast/lib/resources/arrays.dart | 7 + .../cherry_toast/lib/resources/colors.dart | 6 + .../cherry_toast/lib/resources/constants.dart | 19 + app/3rd_party/cherry_toast/pubspec.yaml | 18 + app/lib/models/scheme.dart | 4 +- app/lib/pages/gesture_editor.dart | 76 ++- app/lib/pages/local_manager.dart | 4 +- app/lib/utils/alert_interface.dart | 18 + app/lib/utils/alert_platform.dart | 30 + app/lib/utils/alert_web.dart | 30 + app/lib/utils/helper.dart | 36 +- app/lib/utils/notificator.dart | 127 ++++ app/lib/widgets/dde_data_table.dart | 6 +- app/linux/flutter/generated_plugin_registrant.cc | 4 + app/linux/flutter/generated_plugins.cmake | 1 + app/pubspec.yaml | 5 +- app/resources/langs/en.json | 6 + app/resources/langs/zh-CN.json | 6 + 23 files changed, 1830 insertions(+), 73 deletions(-) create mode 100755 app/3rd_party/cherry_toast/CHANGELOG.md create mode 100755 app/3rd_party/cherry_toast/LICENSE create mode 100755 app/3rd_party/cherry_toast/README.md create mode 100755 app/3rd_party/cherry_toast/lib/cherry_toast.dart create mode 100755 app/3rd_party/cherry_toast/lib/cherry_toast_icon.dart create mode 100755 app/3rd_party/cherry_toast/lib/resources/arrays.dart create mode 100755 app/3rd_party/cherry_toast/lib/resources/colors.dart create mode 100755 app/3rd_party/cherry_toast/lib/resources/constants.dart create mode 100755 app/3rd_party/cherry_toast/pubspec.yaml create mode 100644 app/lib/utils/alert_interface.dart create mode 100644 app/lib/utils/alert_platform.dart create mode 100644 app/lib/utils/alert_web.dart create mode 100644 app/lib/utils/notificator.dart diff --git a/app/3rd_party/cherry_toast/CHANGELOG.md b/app/3rd_party/cherry_toast/CHANGELOG.md new file mode 100755 index 0000000..772320d --- /dev/null +++ b/app/3rd_party/cherry_toast/CHANGELOG.md @@ -0,0 +1,19 @@ +## [1.0.2] - 19/08/2021 + +* Add heartbeat animation on icon +* Customizable icon size and color +* Bug fixes + +## [1.0.1] - 24/07/2021 + +* Add customizable border radius + +## [1.0.0] - 20/07/2021 + +* Add support to all platforms +* Add support to null safety +* Add multiple built-in themes +* Add built-in animations +* Support LTR and RTL layout rendering +* Support Top and Bottom toast display +* Full customization to title, description and action components diff --git a/app/3rd_party/cherry_toast/LICENSE b/app/3rd_party/cherry_toast/LICENSE new file mode 100755 index 0000000..e72bfdd --- /dev/null +++ b/app/3rd_party/cherry_toast/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/app/3rd_party/cherry_toast/README.md b/app/3rd_party/cherry_toast/README.md new file mode 100755 index 0000000..02d2aef --- /dev/null +++ b/app/3rd_party/cherry_toast/README.md @@ -0,0 +1,202 @@ + + + + + +# Cherry Toast + +

+ +
+A new way to display toasts in Flutter in an elegant design and animations +

+ + +| ![info_cherry_toast.gif](https://github.com/koukibadr/Cherry-Toast/blob/main/example/info_cherry_toast.gif?raw=true) | ![error_cherry_toast.gif](https://github.com/koukibadr/Cherry-Toast/blob/main/example/error_cherry_toast.gif?raw=true) | ![bottom_cherry_toast.gif](https://github.com/koukibadr/Cherry-Toast/blob/main/example/bottom_cherry_toast.gif?raw=true) | +|--|--|--| +| ![warning_cherry_toast.gif](https://github.com/koukibadr/Cherry-Toast/blob/main/example/warning_cherry_toast.gif?raw=true) | ![success_cherry_toast.gif](https://github.com/koukibadr/Cherry-Toast/blob/main/example/success_cherry_toast.gif?raw=true) | ![right_layout_cherry_toast.gif](https://github.com/koukibadr/Cherry-Toast/blob/main/example/right_layout_cherry_toast.gif?raw=true) | + + + + + +## Cherry Toast v1.0.2 + + +- Support all platforms +- Top and Bottom display position +- Multiple built-in themes +- Built-in animations +- Support null safety +- Elegant design +- Full customizable +- Heartbeat animation on icons +- Customizable icon size and color and display + + + + +## Installation + + +To add cherry toast to your project add this line to your `pubspec.yaml` file + +```yaml +dependencies: + cherry_toast: ^1.0.2 +``` + + + + + +## Parameters + + +| Name | Description | Required | Default Value | +|--|--|--|--| +| title | The toast title `String` | true | N/A | +| description | the toast description text (nullable) | false | null | +| action | the toast action text (clickable text) | false | null | +| titleStyle | the title text style | false | `TextStyle(color: Colors.black, fontWeight: FontWeight.bold)` | +| descriptionStyle | the description text style | false | `TextStyle(color: Colors.black)` | +| actionStyle | the action text style | false | `TextStyle(color: Colors.black, fontWeight: FontWeight.bold)` | +| displayTitle | indicates whether the title will be rendered or not | false | true | +| icon | the toast displayed icon (IconData) | required when using the default constructor otherwise it's not required | N/A | +| toastPosition | the position of the toast (Top/Bottom) | false | `POSITION.TOP` | +| themeColor | the color that will be applied on the icon back circle (for built-in themes it will match the action text color | required when using the default constructor otherwise it's not required | N/A | +| actionHandler | Function that will be invoked when clicking on the action text | false | null | +| animationDuration | the duration of the animation display and hide | false | `Duration(milliseconds: 1500)` | +| animationCurve | the display animation curve | false | `Curves.ease` | +| animationType | the type of the animation that will be applied on the toast (From left, From right, From top) | false | `ANIMATION_TYPE.FROM_LEFT` | +| autoDismiss | indicates whether the toast will be dismissed automatically or not | false | false | +| toastDuration | the duration of the toast when `autoDismiss` is true | false | `Duration(milliseconds: 3000)` | +| layout | the taost's layout rendering (LTR, RTL) | false | `TOAST_LAYOUT.LTR` | +| displayCloseButton | indicates whether display or not the close button | false | true | +| borderRadius| define the toast border radius | false | 20 | +| iconColor| define the icon color | false | `Colors.black`| +| displayIcon| hide or show the icon on the toast | false | true | +| enableIconAnimation | define wether apply an animation on the icon or not | false | true | +| iconSize | define the icon size | false | 20 | + + + +## Usage + +- Simple cherry toast with only title + +```dart + +CherryToast.success( + title: "The simplest cherry toast" +).show(context); + +``` + + + + + +- Simple cherry toast with action button + +```dart + +CherryToast.info( + title: "User added", + action: "Display information", + actionHandler: (){ + print("Action button pressed"); + }, +).show(context); + +``` + +

+ +
+A new way to display toasts in Flutter in an elegant design and animations +

+ + + +- Toast with description without title + +```dart + +CherryToast.warning( + title: "", + displayTitle: false, + description: "All information may be deleted after this action", + animationType: ANIMATION_TYPE.FROM_TOP, + action: "Backup data", + actionHandler: (){ + print("Hello World!!"); + }, +).show(context); + +``` + + + + + +- Toast with nothing but description with different animation type and auto dismiss + +```dart +CherryToast.error( + title: "", + displayTitle: false, + description: "Invalid account information", + animationType: ANIMATION_TYPE.FROM_RIGHT, + animationDuration: Duration(milliseconds: 1000), + autoDismiss: true) +.show(context); + +``` + +- Bottom displayed cherry toast + +```dart +CherryToast( + icon: Icons.alarm_add, + themeColor: Colors.pink, + title: "", + displayTitle: false, + description: "A bottom cherry toast example", + toastPosition: POSITION.BOTTOM, + animationDuration: Duration(milliseconds: 1000), + autoDismiss: true) +.show(context); + + + +``` + + +- Right layout rendered cherry toast + +```dart +CherryToast( + icon: Icon(Icons.car_repair), + themeColor: Colors.green, + title: "", + displayTitle: false, + description: "هذا مثال تصميم من اليمين", + toastPosition: POSITION.BOTTOM, + layout: TOAST_LAYOUT.RTL, + animationType: ANIMATION_TYPE.FROM_RIGHT, + action: "انقر هنا", + actionStyle: TextStyle(color: Colors.green), + animationDuration: Duration(milliseconds: 1000), + autoDismiss: true) +.show(context); + +``` + +## Contribution + +Of course the project is open source, and you can contribute to it [repository link](https://github.com/koukibadr/Cherry-Toast) + +- If you **found a bug**, open an issue. +- If you **have a feature request**, open an issue. +- If you **want to contribute**, submit a pull request. \ No newline at end of file diff --git a/app/3rd_party/cherry_toast/lib/cherry_toast.dart b/app/3rd_party/cherry_toast/lib/cherry_toast.dart new file mode 100755 index 0000000..58e1d9f --- /dev/null +++ b/app/3rd_party/cherry_toast/lib/cherry_toast.dart @@ -0,0 +1,515 @@ +import 'dart:async'; +import 'package:cherry_toast/cherry_toast_icon.dart'; +import 'package:cherry_toast/resources/arrays.dart'; +import 'package:cherry_toast/resources/colors.dart'; +import 'package:cherry_toast/resources/constants.dart'; +import 'package:flutter/material.dart'; + +// ignore: must_be_immutable +class CherryToast extends StatefulWidget { + CherryToast({ + required this.title, + required this.icon, + required this.themeColor, + this.iconColor = Colors.black, + this.action, + this.actionHandler, + this.description, + this.descriptionStyle = DEFAULT_DESCRIPTION_STYLE, + this.titleStyle = DEFAULT_TITLTE_STYLE, + this.actionStyle = const TextStyle(color: Colors.black, fontWeight: FontWeight.bold), + this.displayTitle = true, + this.toastPosition = POSITION.TOP, + this.animationDuration = DEFAULT_ANIMATION_DURATION, + this.animationCurve = DEFAULT_ANIMATION_CURVE, + this.animationType = ANIMATION_TYPE.FROM_LEFT, + this.autoDismiss = false, + this.toastDuration = DEFAULT_TOAST_DURATION, + this.layout = TOAST_LAYOUT.LTR, + this.displayCloseButton = true, + this.borderRadius = DEFAULT_RADIUS, + this.displayIcon = true, + this.enableIconAnimation = true, + this.iconSize = DEFAULT_ICON_SIZE, + this.backgroundColor, + }); + + CherryToast.success({ + required this.title, + this.action, + this.actionHandler, + this.description, + this.descriptionStyle = DEFAULT_DESCRIPTION_STYLE, + this.titleStyle = DEFAULT_TITLTE_STYLE, + this.actionStyle = const TextStyle(color: SUCCESS_COLOR, fontWeight: FontWeight.bold), + this.displayTitle = true, + this.toastPosition = POSITION.TOP, + this.animationDuration = DEFAULT_ANIMATION_DURATION, + this.animationCurve = DEFAULT_ANIMATION_CURVE, + this.animationType = ANIMATION_TYPE.FROM_LEFT, + this.autoDismiss = false, + this.toastDuration = DEFAULT_TOAST_DURATION, + this.layout = TOAST_LAYOUT.LTR, + this.displayCloseButton = true, + this.borderRadius = DEFAULT_RADIUS, + this.displayIcon = true, + this.enableIconAnimation = true, + this.backgroundColor, + }) { + this.icon = Icons.check_circle; + this.themeColor = SUCCESS_COLOR; + this.iconColor = SUCCESS_COLOR; + this.iconSize = DEFAULT_ICON_SIZE; + } + + CherryToast.error({ + required this.title, + this.action, + this.actionHandler, + this.description, + this.descriptionStyle = DEFAULT_DESCRIPTION_STYLE, + this.titleStyle = DEFAULT_TITLTE_STYLE, + this.actionStyle = const TextStyle(color: ERROR_COLOR, fontWeight: FontWeight.bold), + this.displayTitle = true, + this.toastPosition = POSITION.TOP, + this.animationDuration = DEFAULT_ANIMATION_DURATION, + this.animationCurve = DEFAULT_ANIMATION_CURVE, + this.animationType = ANIMATION_TYPE.FROM_LEFT, + this.autoDismiss = false, + this.toastDuration = DEFAULT_TOAST_DURATION, + this.layout = TOAST_LAYOUT.LTR, + this.displayCloseButton = true, + this.borderRadius = DEFAULT_RADIUS, + this.displayIcon = true, + this.enableIconAnimation = true, + this.backgroundColor, + }) { + this.icon = Icons.error_rounded; + this.themeColor = ERROR_COLOR; + this.iconColor = ERROR_COLOR; + this.iconSize = DEFAULT_ICON_SIZE; + } + + CherryToast.warning({ + required this.title, + this.action, + this.actionHandler, + this.description, + this.descriptionStyle = DEFAULT_DESCRIPTION_STYLE, + this.titleStyle = DEFAULT_TITLTE_STYLE, + this.actionStyle = const TextStyle(color: WARINING_COLOR, fontWeight: FontWeight.bold), + this.displayTitle = true, + this.toastPosition = POSITION.TOP, + this.animationDuration = DEFAULT_ANIMATION_DURATION, + this.animationCurve = DEFAULT_ANIMATION_CURVE, + this.animationType = ANIMATION_TYPE.FROM_LEFT, + this.autoDismiss = false, + this.toastDuration = DEFAULT_TOAST_DURATION, + this.layout = TOAST_LAYOUT.LTR, + this.displayCloseButton = true, + this.borderRadius = DEFAULT_RADIUS, + this.displayIcon = true, + this.enableIconAnimation = true, + this.backgroundColor, + }) { + this.icon = Icons.warning_rounded; + this.themeColor = WARINING_COLOR; + this.iconColor = WARINING_COLOR; + this.iconSize = DEFAULT_ICON_SIZE; + } + + CherryToast.info({ + required this.title, + this.action, + this.actionHandler, + this.description, + this.descriptionStyle = DEFAULT_DESCRIPTION_STYLE, + this.titleStyle = DEFAULT_TITLTE_STYLE, + this.actionStyle = const TextStyle(color: INFO_COLOR, fontWeight: FontWeight.bold), + this.displayTitle = true, + this.toastPosition = POSITION.TOP, + this.animationDuration = DEFAULT_ANIMATION_DURATION, + this.animationCurve = DEFAULT_ANIMATION_CURVE, + this.animationType = ANIMATION_TYPE.FROM_LEFT, + this.autoDismiss = false, + this.toastDuration = DEFAULT_TOAST_DURATION, + this.layout = TOAST_LAYOUT.LTR, + this.displayCloseButton = true, + this.borderRadius = DEFAULT_RADIUS, + this.displayIcon = true, + this.enableIconAnimation = true, + this.backgroundColor, + }) { + this.icon = Icons.info_rounded; + this.themeColor = INFO_COLOR; + this.iconColor = INFO_COLOR; + this.iconSize = DEFAULT_ICON_SIZE; + } + + ///the toast title string + /// + final String title; + + ///The toast description text + /// + final String? description; + + ///The toast action button text + /// + final String? action; + + ///the text style that will be applied on the title + ///by default it's `TextStyle(color: Colors.black, fontWeight: FontWeight.bold)` + /// + final TextStyle titleStyle; + + ///the text style that will be applied on the description + /// + final TextStyle descriptionStyle; + + ///the action button text style + /// + final TextStyle actionStyle; + + ///indicates whether display or not the title + /// + final bool displayTitle; + + ///the toast icon, it's required when using the default constructor + /// + late IconData icon; + + ///the Icon color + ///this parameter is only available on the default constructor + ///for the built-in themes the color will be set automatically + late Color iconColor; + + ///the icon size + ///by default is 20 + ///this parameter is available in default constructor + late double iconSize; + + ///the toast display postion, possible values + ///```dart + ///{ + ///TOP, + ///BOTTOM + ///} + ///``` + final POSITION toastPosition; + + ///The color that will be applied on the circle behind the icon + ///for better rendering the action button must have the same color + /// + late Color themeColor; + + ///the function invoked when clicking on the action button + /// + final Function? actionHandler; + + ///The duration of the animation by default it's 1.5 seconds + /// + final Duration animationDuration; + + ///the animation curve by default it's set to `Curves.ease` + /// + final Cubic animationCurve; + + ///The animation type applied on the toast + ///```dart + ///{ + ///FROM_TOP, + ///FROM_LEFT, + ///FROM_RIGHT + ///} + ///``` + final ANIMATION_TYPE animationType; + + ///indicates whether the toast will be hidden automatically or not + /// + final bool autoDismiss; + + ///the duration of the toast if [autoDismiss] is true + ///by default it's 3 seconds + /// + final Duration toastDuration; + + ///the layout of the toast + ///```dart + ///{ + ///LTR, + ///RTL + ///} + ///``` + final TOAST_LAYOUT layout; + + ///Display / Hide the close button icon + ///by default it's true + final bool displayCloseButton; + + ///define the border radius applied on the toast + ///by default it's 20 + /// + final double borderRadius; + + ///Define whether the icon will be rendered or not + /// + final bool displayIcon; + + ///Define wether the animation on the icon will be rendered or not + /// + final bool enableIconAnimation; + + final Color? backgroundColor; + + ///Display the created cherry toast + ///[context] the context of the application + /// + show(BuildContext context) { + Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (context, _, __) => AlertDialog( + backgroundColor: Colors.transparent, + contentPadding: EdgeInsets.all(0), + insetPadding: EdgeInsets.all(70), + elevation: 0, + content: this, + ), + opaque: false), + ); + } + + @override + _CherryToastState createState() => _CherryToastState(); +} + +class _CherryToastState extends State with TickerProviderStateMixin { + late Animation offsetAnimation; + late AnimationController slideController; + late BoxDecoration toastDecoration; + + @override + void initState() { + super.initState(); + _initAnimation(); + var _shadowHSLColor = HSLColor.fromColor(widget.backgroundColor ?? Colors.white); + toastDecoration = BoxDecoration( + color: widget.backgroundColor ?? Colors.white, + borderRadius: BorderRadius.circular(this.widget.borderRadius), + boxShadow: [ + BoxShadow( + color: _shadowHSLColor + .withLightness(_shadowHSLColor.lightness - 0.1 < 0 ? 0 : _shadowHSLColor.lightness - 0.1) + .toColor(), + spreadRadius: 2, + blurRadius: 3, + offset: Offset(0, 1), // changes position of shadow + ), + ], + ); + if (this.widget.autoDismiss) { + Timer(this.widget.toastDuration, () { + slideController.reverse(); + Timer(this.widget.animationDuration, () { + if (mounted) Navigator.pop(context); + }); + }); + } + } + + ///Initialize animation parameters [slideController] and [offsetAnimation] + _initAnimation() { + slideController = AnimationController( + duration: this.widget.animationDuration, + vsync: this, + ); + switch (this.widget.animationType) { + case ANIMATION_TYPE.FROM_LEFT: + offsetAnimation = Tween( + begin: const Offset(-2, 0), + end: const Offset(0, 0), + ).animate(CurvedAnimation(parent: slideController, curve: this.widget.animationCurve)); + break; + case ANIMATION_TYPE.FROM_RIGHT: + offsetAnimation = Tween( + begin: const Offset(2, 0), + end: const Offset(0, 0), + ).animate(CurvedAnimation(parent: slideController, curve: this.widget.animationCurve)); + break; + case ANIMATION_TYPE.FROM_TOP: + offsetAnimation = Tween( + begin: const Offset(0, -2), + end: const Offset(0, 0), + ).animate(CurvedAnimation(parent: slideController, curve: this.widget.animationCurve)); + break; + default: + } + WidgetsBinding.instance!.addPostFrameCallback((_) { + slideController.forward(); + }); + } + + @override + Widget build(BuildContext context) { + if (this.widget.layout == TOAST_LAYOUT.LTR) { + return _renderLeftLayoutToast(context); + } else { + return _renderRightLayoutToast(context); + } + } + + ///render a left layout toast if [this.widget.layout] set to LTR + /// + Widget _renderLeftLayoutToast(BuildContext context) { + return Column( + mainAxisAlignment: this.widget.toastPosition == POSITION.TOP ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + SlideTransition( + position: offsetAnimation, + child: Container( + decoration: toastDecoration, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + this.widget.displayIcon + ? CherryToatIcon( + color: this.widget.themeColor, + icon: this.widget.icon, + iconSize: this.widget.iconSize, + iconColor: this.widget.iconColor, + enableAnimation: this.widget.enableIconAnimation, + ) + : Container(), + _renderToastContent(), + ], + ), + ), + this.widget.displayCloseButton + ? Padding( + padding: const EdgeInsets.only(top: 10, right: 10), + child: _renderCloseButton(context), + ) + : Container(), + ], + ), + ), + ), + ), + ], + ); + } + + ///render a right layout toast if [this.widget.layout] set to RTL + /// + Column _renderRightLayoutToast(BuildContext context) { + return Column( + mainAxisAlignment: this.widget.toastPosition == POSITION.TOP ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + SlideTransition( + position: offsetAnimation, + child: Container( + decoration: toastDecoration, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + this.widget.displayCloseButton + ? Padding( + padding: const EdgeInsets.only(top: 10, left: 10), + child: _renderCloseButton(context), + ) + : Container(), + Expanded( + flex: 2, + child: Row( + crossAxisAlignment: this.widget.description == null && this.widget.action == null + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + _renderToastContent(), + CherryToatIcon( + color: this.widget.themeColor, + icon: this.widget.icon, + iconSize: this.widget.iconSize, + iconColor: this.widget.iconColor, + enableAnimation: this.widget.enableIconAnimation), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + + /// render the close button icon with a clickable widget that + /// will hide the toast + /// + InkWell _renderCloseButton(BuildContext context) { + return InkWell( + onTap: () { + slideController.reverse(); + Timer(this.widget.animationDuration, () { + if (mounted) Navigator.pop(context); + }); + }, + child: Icon(Icons.close, color: Colors.grey[500], size: CLOSE_BUTTON_SIZE), + ); + } + + ///render the toast content (Title, Description and Action) + /// + Expanded _renderToastContent() { + return Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + this.widget.layout == TOAST_LAYOUT.LTR ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: [ + this.widget.displayTitle ? Text(this.widget.title, style: this.widget.titleStyle) : Container(), + this.widget.description == null + ? Container() + : Column( + children: [ + SizedBox( + height: 5, + ), + Text(this.widget.description ?? "", style: this.widget.descriptionStyle) + ], + ), + this.widget.action != null + ? Column( + children: [ + SizedBox( + height: 5, + ), + InkWell( + onTap: () { + this.widget.actionHandler?.call(); + }, + child: Text(this.widget.action ?? "", style: this.widget.actionStyle)) + ], + ) + : Container() + ], + ), + ), + ); + } +} diff --git a/app/3rd_party/cherry_toast/lib/cherry_toast_icon.dart b/app/3rd_party/cherry_toast/lib/cherry_toast_icon.dart new file mode 100755 index 0000000..9762b1f --- /dev/null +++ b/app/3rd_party/cherry_toast/lib/cherry_toast_icon.dart @@ -0,0 +1,90 @@ +import 'package:cherry_toast/resources/constants.dart'; +import 'package:flutter/material.dart'; + +class CherryToatIcon extends StatefulWidget { + ///the color that will be applied on the circle behind the icon + ///(required) + final Color color; + + ///The toast icon widget (required) + /// + final IconData icon; + + ///the size of the icon (required) + /// + final double iconSize; + + ///the icon color (required) + final Color iconColor; + + ///define wether the animation will be applied on the icon or not + /// + final bool enableAnimation; + + CherryToatIcon( + {required this.color, + required this.icon, + required this.iconSize, + required this.iconColor, + required this.enableAnimation}); + + @override + _CherryToatIconState createState() => _CherryToatIconState(); +} + +class _CherryToatIconState extends State + with TickerProviderStateMixin { + late Animation _heartAnimation; + late AnimationController _heartAnimationController; + + @override + void initState() { + super.initState(); + if (this.widget.enableAnimation) { + _heartAnimationController = AnimationController( + vsync: this, duration: Duration(milliseconds: 1200)); + _heartAnimation = Tween( + begin: this.widget.iconSize * 0.7, + end: this.widget.iconSize * 0.95) + .animate(CurvedAnimation( + curve: Curves.bounceOut, parent: _heartAnimationController)); + + _heartAnimationController.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + _heartAnimationController.repeat(); + } + }); + _heartAnimationController.forward(); + } + } + + @override + Widget build(BuildContext context) { + return Container( + width: DEFAULT_ICON_LAYOUT_SIZE, + height: DEFAULT_ICON_LAYOUT_SIZE, + decoration: BoxDecoration( + shape: BoxShape.circle, color: this.widget.color.withAlpha(20)), + child: Center( + child: this.widget.enableAnimation + ? AnimatedBuilder( + builder: (context, child) { + return Icon(this.widget.icon, + size: this._heartAnimation.value, + color: this.widget.iconColor); + }, + animation: this._heartAnimationController, + ) + : Icon(this.widget.icon, + size: this.widget.iconSize, color: this.widget.iconColor)), + ); + } + + @override + void dispose() { + if (this.widget.enableAnimation) { + _heartAnimationController.dispose(); + } + super.dispose(); + } +} diff --git a/app/3rd_party/cherry_toast/lib/resources/arrays.dart b/app/3rd_party/cherry_toast/lib/resources/arrays.dart new file mode 100755 index 0000000..d21492d --- /dev/null +++ b/app/3rd_party/cherry_toast/lib/resources/arrays.dart @@ -0,0 +1,7 @@ +enum CHERRY_TYPE { SUCCESS, WARINING, ERROR, INFO, CUSTOM } + +enum POSITION { TOP, BOTTOM } + +enum ANIMATION_TYPE { FROM_TOP, FROM_LEFT, FROM_RIGHT } + +enum TOAST_LAYOUT { LTR, RTL } diff --git a/app/3rd_party/cherry_toast/lib/resources/colors.dart b/app/3rd_party/cherry_toast/lib/resources/colors.dart new file mode 100755 index 0000000..4ea81f3 --- /dev/null +++ b/app/3rd_party/cherry_toast/lib/resources/colors.dart @@ -0,0 +1,6 @@ +import 'dart:ui'; + +const Color ERROR_COLOR = Color(0xffE43837); +const Color SUCCESS_COLOR = Color(0xFF2F9449); +const Color INFO_COLOR = Color(0xFF4E5CB9); +const Color WARINING_COLOR = Color(0xffFC9F00); diff --git a/app/3rd_party/cherry_toast/lib/resources/constants.dart b/app/3rd_party/cherry_toast/lib/resources/constants.dart new file mode 100755 index 0000000..de2c168 --- /dev/null +++ b/app/3rd_party/cherry_toast/lib/resources/constants.dart @@ -0,0 +1,19 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +const String PACKAGE_NAME = "cherry_toast"; + +const TextStyle DEFAULT_TITLTE_STYLE = + const TextStyle(color: Colors.black, fontWeight: FontWeight.bold); +const TextStyle DEFAULT_DESCRIPTION_STYLE = + const TextStyle(color: Colors.black); + +const Duration DEFAULT_ANIMATION_DURATION = Duration(milliseconds: 1500); +const Duration DEFAULT_TOAST_DURATION = Duration(milliseconds: 3000); +const Cubic DEFAULT_ANIMATION_CURVE = Curves.ease; + +const double CLOSE_BUTTON_SIZE = 15; + +const double DEFAULT_ICON_SIZE = 20; +const double DEFAULT_ICON_LAYOUT_SIZE = 40; +const double DEFAULT_RADIUS = 20; diff --git a/app/3rd_party/cherry_toast/pubspec.yaml b/app/3rd_party/cherry_toast/pubspec.yaml new file mode 100755 index 0000000..831892b --- /dev/null +++ b/app/3rd_party/cherry_toast/pubspec.yaml @@ -0,0 +1,18 @@ +name: cherry_toast +description: A new way to display toasts in flutter with elegant design and animations +version: 1.0.2 +homepage: https://github.com/koukibadr/Cherry-Toast + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: diff --git a/app/lib/models/scheme.dart b/app/lib/models/scheme.dart index b09be41..f30deca 100644 --- a/app/lib/models/scheme.dart +++ b/app/lib/models/scheme.dart @@ -246,10 +246,10 @@ class GestureProp implements Comparable { Map toJson() => { 'id': id, - 'gesture': H.getGestureName(gesture), + 'gesture': gesture?.name, 'direction': H.getGestureDirectionName(direction), 'fingers': fingers, - 'type': H.getGestureTypeName(type), + 'type': type?.name, 'command': command, 'remark': remark, }; diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index 92a9852..56aa120 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -9,6 +9,7 @@ import 'package:dde_gesture_manager/models/settings.provider.dart'; import 'package:dde_gesture_manager/pages/content.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:dde_gesture_manager/utils/keyboard_mapper.dart'; +import 'package:dde_gesture_manager/utils/notificator.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:dde_gesture_manager/widgets/dde_data_table.dart'; import 'package:dde_gesture_manager/widgets/dde_text_field.dart'; @@ -126,32 +127,27 @@ class GestureEditor extends StatelessWidget { controller: horizontalCtrl, child: ConstrainedBox( constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: IgnorePointer( - ignoring: schemeProvider.readOnly, - child: DDataTable( - showBottomBorder: true, - headingRowHeight: _headingRowHeight, - showCheckboxColumn: true, - headerBackgroundColor: context.t.dialogBackgroundColor, - verticalScrollController: verticalCtrl, - dataRowColor: - MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.hovered)) - return context.t.dialogBackgroundColor; - if (states.contains(MaterialState.selected)) - return context.read().currentActiveColor; - return null; - }), - columns: [ - DDataColumn(label: Text(LocaleKeys.gesture_editor_fingers.tr()), center: true), - 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_type.tr()), center: true), - DDataColumn(label: Text(LocaleKeys.gesture_editor_command.tr())), - DDataColumn(label: Text(LocaleKeys.gesture_editor_remark.tr())), - ], - rows: _buildDataRows(schemeProvider.gestures, context), - ), + child: DDataTable( + showBottomBorder: true, + headingRowHeight: _headingRowHeight, + showCheckboxColumn: true, + headerBackgroundColor: context.t.dialogBackgroundColor, + verticalScrollController: verticalCtrl, + dataRowColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.hovered)) return context.t.dialogBackgroundColor; + if (states.contains(MaterialState.selected)) + return context.read().currentActiveColor; + return null; + }), + columns: [ + DDataColumn(label: Text(LocaleKeys.gesture_editor_fingers.tr()), center: true), + 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_type.tr()), center: true), + DDataColumn(label: Text(LocaleKeys.gesture_editor_command.tr())), + DDataColumn(label: Text(LocaleKeys.gesture_editor_remark.tr())), + ], + rows: _buildDataRows(context, schemeProvider.gestures, schemeProvider.readOnly), ), ), ), @@ -169,9 +165,7 @@ class GestureEditor extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ DButton.add( - enabled: !schemeProvider.readOnly && - !gesturePropProvider.editMode! && - !schemeTree.fullFiled, + enabled: !schemeProvider.readOnly && !gesturePropProvider.editMode! && !schemeTree.fullFiled, onTap: () { var schemeProvider = context.read(); var newGestures = [ @@ -201,15 +195,16 @@ class GestureEditor extends StatelessWidget { }, ), DButton.duplicate( - enabled: !schemeProvider.readOnly && - gesturePropProvider != GestureProp.empty() && - !gesturePropProvider.editMode!, + enabled: gesturePropProvider != GestureProp.empty() && !gesturePropProvider.editMode!, onTap: () { var schemeProvider = context.read(); context.read().copyFrom( schemeProvider.gestures!.firstWhere((element) => element.id == gesturePropProvider.id)); - - /// todo: give some info to UI. + Notificator.success( + context, + title: LocaleKeys.info_gesture_prop_duplicated_title.tr(), + description: LocaleKeys.info_gesture_prop_duplicated_description.tr(), + ); }, ), DButton.paste( @@ -306,7 +301,8 @@ class GestureEditor extends StatelessWidget { } } -List _buildDataRows(List? gestures, BuildContext context) => (gestures ?? []).map((gesture) { +List _buildDataRows(BuildContext context, List? gestures, bool readOnly) => + (gestures ?? []).map((gesture) { var gesturePropProvider = context.watch(); bool editing = gesturePropProvider == gesture && gesturePropProvider.editMode == true; bool selected = gesturePropProvider == gesture && !editing; @@ -320,7 +316,7 @@ List _buildDataRows(List? gestures, BuildContext context) Future.microtask(() => provider.setProps( id: gesture.id, )); - } else if (selected == false) { + } else if (selected == false && !readOnly) { provider.onEditEnd = (prop) { var schemeProvider = context.read(); var newGestures = List.of(schemeProvider.gestures!); @@ -408,7 +404,7 @@ List _buildRowCellsEditing(BuildContext context) { .map( (e) => DropdownMenuItem( child: Text( - '${LocaleKeys.gesture_editor_gestures}.${H.getGestureName(e)}', + '${LocaleKeys.gesture_editor_gestures}.${e.name}', textScaleFactor: .8, ).tr(), value: e, @@ -456,7 +452,7 @@ List _buildRowCellsEditing(BuildContext context) { .map( (e) => DropdownMenuItem( child: Text( - '${LocaleKeys.gesture_editor_types}.${H.getGestureTypeName(e)}', + '${LocaleKeys.gesture_editor_types}.${e.name}', textScaleFactor: .8, ).tr(), value: e, @@ -545,7 +541,7 @@ List _buildRowCellsNormal(BuildContext context, bool selected, Gestur ), Center( child: Text( - '${LocaleKeys.gesture_editor_gestures}.${H.getGestureName(gesture.gesture)}', + '${LocaleKeys.gesture_editor_gestures}.${gesture.gesture?.name}', ).tr(), ), Center( @@ -554,7 +550,7 @@ List _buildRowCellsNormal(BuildContext context, bool selected, Gestur ).tr()), Center( child: Text( - '${LocaleKeys.gesture_editor_types}.${H.getGestureTypeName(gesture.type)}', + '${LocaleKeys.gesture_editor_types}.${gesture.type?.name}', ).tr()), gesture.type == GestureType.shortcut ? Row( diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index 5e7dc57..3e3723f 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -51,7 +51,7 @@ class _LocalManagerState extends State { Color _getItemBackgroundColor(int index, String itemPath) { Color _color = index % 2 == 0 ? context.t.scaffoldBackgroundColor : context.t.backgroundColor; - if (itemPath == _hoveringItem) _color = context.t.scaffoldBackgroundColor; + if (itemPath == _hoveringItem) _color = context.t.dialogBackgroundColor; if (itemPath == _selectedItemPath) _color = context.read().currentActiveColor; return _color; } @@ -145,7 +145,7 @@ class _LocalManagerState extends State { cursor: SystemMouseCursors.click, onEnter: (_) { setState(() { - _hoveringItem = localSchemes[index].scheme.id!; + _hoveringItem = localSchemes[index].path; }); }, child: Container( diff --git a/app/lib/utils/alert_interface.dart b/app/lib/utils/alert_interface.dart new file mode 100644 index 0000000..fa7a3d9 --- /dev/null +++ b/app/lib/utils/alert_interface.dart @@ -0,0 +1,18 @@ +export 'alert_web.dart' if (dart.library.io) 'alert_platform.dart'; + +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; + +abstract class Alert { + Future showAlert({ + required String windowTitle, + required String text, + String? positiveButtonTitle, + }); + + Future showConfirm({ + required String windowTitle, + required String text, + String? positiveButtonTitle, + String? negativeButtonTitle, + }); +} \ No newline at end of file diff --git a/app/lib/utils/alert_platform.dart b/app/lib/utils/alert_platform.dart new file mode 100644 index 0000000..e9eaead --- /dev/null +++ b/app/lib/utils/alert_platform.dart @@ -0,0 +1,30 @@ +import 'alert_interface.dart'; +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; + +class AlertImpl implements Alert { + @override + Future showAlert({ + required String windowTitle, + required String text, + String? positiveButtonTitle, + }) => + FlutterPlatformAlert.showCustomAlert( + windowTitle: windowTitle, + text: text, + positiveButtonTitle: positiveButtonTitle, + ); + + @override + Future showConfirm({ + required String windowTitle, + required String text, + String? positiveButtonTitle, + String? negativeButtonTitle, + }) => + FlutterPlatformAlert.showCustomAlert( + windowTitle: windowTitle, + text: text, + positiveButtonTitle: positiveButtonTitle, + negativeButtonTitle: negativeButtonTitle, + ); +} diff --git a/app/lib/utils/alert_web.dart b/app/lib/utils/alert_web.dart new file mode 100644 index 0000000..c5bf038 --- /dev/null +++ b/app/lib/utils/alert_web.dart @@ -0,0 +1,30 @@ +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; + +import 'alert_interface.dart'; + +import 'dart:html' as html; + +class AlertImpl implements Alert { + @override + Future showAlert({ + required String windowTitle, + required String text, + String? positiveButtonTitle, + }) { + html.window.alert([windowTitle, text].join('\n')); + return Future.value(CustomButton.positiveButton); + } + + @override + Future showConfirm({ + required String windowTitle, + required String text, + String? positiveButtonTitle, + String? negativeButtonTitle, + }) { + var confirmed = html.window.confirm([windowTitle, text].join('\n')); + return Future.value( + confirmed ? CustomButton.positiveButton : CustomButton.negativeButton, + ); + } +} diff --git a/app/lib/utils/helper.dart b/app/lib/utils/helper.dart index 6752dcb..61350dd 100644 --- a/app/lib/utils/helper.dart +++ b/app/lib/utils/helper.dart @@ -6,6 +6,15 @@ import 'package:flutter/cupertino.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; +extension EnumByName on Iterable { + T? findByName(String name) { + for (var value in this) { + if (value.name == name) return value; + } + return null; + } +} + class H { H._(); @@ -50,19 +59,7 @@ class H { return preferredPanelsStatus..marketPanelOpened = false; } - static String? getGestureName(Gesture? gesture) => const { - Gesture.swipe: 'swipe', - Gesture.tap: 'tap', - Gesture.pinch: 'pinch', - }[gesture]; - - static Gesture getGestureByName(String gestureName) => - const { - 'swipe': Gesture.swipe, - 'tap': Gesture.tap, - 'pinch': Gesture.pinch, - }[gestureName] ?? - Gesture.swipe; + static Gesture getGestureByName(String gestureName) => Gesture.values.findByName(gestureName) ?? Gesture.swipe; static String? getGestureDirectionName(GestureDirection? direction) => const { GestureDirection.up: 'up', @@ -85,19 +82,8 @@ class H { }[directionName] ?? GestureDirection.none; - static String? getGestureTypeName(GestureType? type) => const { - GestureType.built_in: 'built_in', - GestureType.shortcut: 'shortcut', - GestureType.commandline: 'commandline', - }[type]; - static GestureType getGestureTypeByName(String typeName) => - const { - 'built_in': GestureType.built_in, - 'shortcut': GestureType.shortcut, - 'commandline': GestureType.commandline, - }[typeName] ?? - GestureType.built_in; + GestureType.values.findByName(typeName) ?? GestureType.built_in; static Color? parseQtActiveColor(String? inp) { if (inp == null) return null; diff --git a/app/lib/utils/notificator.dart b/app/lib/utils/notificator.dart new file mode 100644 index 0000000..3ef808d --- /dev/null +++ b/app/lib/utils/notificator.dart @@ -0,0 +1,127 @@ +import 'package:cherry_toast/cherry_toast.dart'; +import 'package:cherry_toast/resources/arrays.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; + +import 'alert_interface.dart'; + +class Notificator { + static Future showAlert({ + required String title, + required String description, + String? positiveButtonTitle, + }) { + return AlertImpl().showAlert( + windowTitle: title, + text: description, + positiveButtonTitle: positiveButtonTitle, + ); + } + + static Future showConfirm({ + required String title, + required String description, + String? positiveButtonTitle, + String? negativeButtonTitle, + }) { + return AlertImpl().showConfirm( + windowTitle: title, + text: description, + positiveButtonTitle: positiveButtonTitle, + negativeButtonTitle: negativeButtonTitle, + ); + } + + static _setToastIconBackgroundColor(CherryToast toast, bool isDarkMode) { + var hslColor = HSLColor.fromColor(toast.themeColor); + toast.themeColor = hslColor.withLightness((hslColor.lightness + (isDarkMode ? 0.4 : 0.1)).clamp(0, 1)).toColor(); + } + + static CherryToast info( + BuildContext context, { + required String title, + String? description, + }) { + var themeData = Theme.of(context); + var toast = CherryToast.info( + title: title, + description: description, + autoDismiss: true, + animationType: ANIMATION_TYPE.FROM_TOP, + animationDuration: Duration(milliseconds: 300), + toastDuration: Duration(seconds: 3), + backgroundColor: themeData.backgroundColor, + titleStyle: themeData.textTheme.bodyText1!.copyWith(fontWeight: FontWeight.bold), + descriptionStyle: themeData.textTheme.bodyText1!, + ); + _setToastIconBackgroundColor(toast, themeData.brightness == Brightness.dark); + toast.show(context); + return toast; + } + + static CherryToast warning( + BuildContext context, { + required String title, + String? description, + }) { + var themeData = Theme.of(context); + var toast = CherryToast.warning( + title: title, + description: description, + autoDismiss: true, + animationType: ANIMATION_TYPE.FROM_TOP, + animationDuration: Duration(milliseconds: 300), + toastDuration: Duration(seconds: 3), + backgroundColor: themeData.backgroundColor, + titleStyle: themeData.textTheme.bodyText1!.copyWith(fontWeight: FontWeight.bold), + descriptionStyle: themeData.textTheme.bodyText1!, + ); + _setToastIconBackgroundColor(toast, themeData.brightness == Brightness.dark); + toast.show(context); + return toast; + } + + static CherryToast error( + BuildContext context, { + required String title, + String? description, + }) { + var themeData = Theme.of(context); + var toast = CherryToast.error( + title: title, + description: description, + autoDismiss: true, + animationType: ANIMATION_TYPE.FROM_TOP, + animationDuration: Duration(milliseconds: 300), + toastDuration: Duration(seconds: 3), + backgroundColor: themeData.backgroundColor, + titleStyle: themeData.textTheme.bodyText1!.copyWith(fontWeight: FontWeight.bold), + descriptionStyle: themeData.textTheme.bodyText1!, + ); + _setToastIconBackgroundColor(toast, themeData.brightness == Brightness.dark); + toast.show(context); + return toast; + } + + static CherryToast success( + BuildContext context, { + required String title, + String? description, + }) { + var themeData = Theme.of(context); + var toast = CherryToast.success( + title: title, + description: description, + autoDismiss: true, + animationType: ANIMATION_TYPE.FROM_TOP, + animationDuration: Duration(milliseconds: 300), + toastDuration: Duration(seconds: 3), + backgroundColor: themeData.backgroundColor, + titleStyle: themeData.textTheme.bodyText1!.copyWith(fontWeight: FontWeight.bold), + descriptionStyle: themeData.textTheme.bodyText1!, + ); + _setToastIconBackgroundColor(toast, themeData.brightness == Brightness.dark); + toast.show(context); + return toast; + } +} diff --git a/app/lib/widgets/dde_data_table.dart b/app/lib/widgets/dde_data_table.dart index b4e6e96..d145abd 100644 --- a/app/lib/widgets/dde_data_table.dart +++ b/app/lib/widgets/dde_data_table.dart @@ -17,7 +17,7 @@ typedef DataColumnSortCallback = void Function(int columnIndex, bool ascending); /// /// One column configuration must be provided for each column to /// display in the table. The list of [DataColumn] objects is passed -/// as the `columns` argument to the [new DataTable] constructor. +/// as the `columns` argument to the [DataTable] constructor. @immutable class DDataColumn { /// Creates the configuration for a column of a [DataTable]. @@ -73,7 +73,7 @@ class DDataColumn { /// /// One row configuration must be provided for each row to /// display in the table. The list of [DataRow] objects is passed -/// as the `rows` argument to the [new DataTable] constructor. +/// as the `rows` argument to the [DataTable] constructor. /// /// The data for this row of the table is provided in the [cells] /// property of the [DataRow] object. @@ -995,7 +995,7 @@ class _DDataTableState extends State { List _skickyHeaders = []; var _headerBackgroundHSLColor = HSLColor.fromColor(widget.headerBackgroundColor); - HSLColor.fromColor(widget.headerBackgroundColor).withSaturation(.1).toColor(); + // HSLColor.fromColor(widget.headerBackgroundColor).withSaturation(.1).toColor(); if (_headersRect != null && _headersRect!.length > 0) { for (var i = 0; i < _headersRect!.length; i++) { _skickyHeaders.add(Positioned( diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc index 5a6c006..da1524f 100644 --- a/app/linux/flutter/generated_plugin_registrant.cc +++ b/app/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_platform_alert_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin"); + flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/app/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake index 567db6d..5a9d113 100644 --- a/app/linux/flutter/generated_plugins.cmake +++ b/app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_platform_alert window_manager ) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 7056b83..42a6021 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.15.0 <3.0.0" dependencies: flutter: @@ -37,6 +37,9 @@ dependencies: path_provider: ^2.0.5 uuid: ^3.0.5 adaptive_scrollbar: ^2.1.0 + flutter_platform_alert: ^0.2.1 + cherry_toast: + path: 3rd_party/cherry_toast xdg_directories_web: path: 3rd_party/xdg_directories_web diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index 389276f..c8cb52d 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -82,5 +82,11 @@ "SplitWindowLeft": "SplitWindowLeft", "SplitWindowRight": "SplitWindowRight", "MoveWindow": "MoveWindow" + }, + "info": { + "gesture_prop_duplicated": { + "title": "Duplicated !", + "description": "Now you can go to other scheme and click the paste button ~" + } } } \ No newline at end of file diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index 00e0f2d..d9a46be 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -82,5 +82,11 @@ "SplitWindowLeft": "向左分屏", "SplitWindowRight": "向右分屏", "MoveWindow": "移动窗口" + }, + "info": { + "gesture_prop_duplicated": { + "title": "复制成功!", + "description": "现在可以去其他方案中点击粘贴按钮试试哦~" + } } } \ No newline at end of file -- 2.11.0 From 214b06cac83af65c63a518bec3f153ae15f9bc8e Mon Sep 17 00:00:00 2001 From: debuggerx Date: Tue, 14 Dec 2021 18:47:21 +0800 Subject: [PATCH 08/14] wip: show error when scheme name conflict. --- app/lib/models/scheme.dart | 2 +- app/lib/pages/gesture_editor.dart | 7 +++++-- app/lib/pages/local_manager.dart | 24 ++++++++++++++++++------ app/resources/langs/en.json | 7 ++++++- app/resources/langs/zh-CN.json | 7 ++++++- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/app/lib/models/scheme.dart b/app/lib/models/scheme.dart index f30deca..292b1bb 100644 --- a/app/lib/models/scheme.dart +++ b/app/lib/models/scheme.dart @@ -156,7 +156,7 @@ class Scheme { this.gestures = []; this.fromMarket = false; this.uploaded = false; - this.name = 'new xxx'; + this.name = LocaleKeys.str_new_scheme.tr(); } SchemeTree buildSchemeTree() { diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index 56aa120..deff43b 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -274,8 +274,11 @@ class GestureEditor extends StatelessWidget { schemeProvider.setProps(name: val); var localSchemesProvider = context.read(); if (!localSchemesProvider.schemes!.every((element) => element.scheme.name != val)) { - /// show error info; - 'duplicate name'.sout(); + Notificator.error( + context, + title: LocaleKeys.info_scheme_name_conflict_title.tr(), + description: LocaleKeys.info_scheme_name_conflict_description.tr(), + ); return; } ; diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index 3e3723f..cba1f3c 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -56,8 +56,7 @@ class _LocalManagerState extends State { return _color; } - Icon _getItemIcon(Scheme scheme, String? appliedId) { - if (scheme.id == appliedId) return Icon(Icons.done_rounded, size: 22); + Icon _getItemIcon(Scheme scheme) { if (scheme.id == Uuid.NAMESPACE_NIL) return Icon(Icons.restore_rounded, size: 22); if (scheme.fromMarket == true) return Icon(Icons.local_grocery_store_rounded, size: 20); if (scheme.uploaded == true) return Icon(Icons.cloud_done_rounded, size: 18); @@ -159,9 +158,22 @@ class _LocalManagerState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(localSchemes[index].scheme.name ?? ''), - _getItemIcon(localSchemes[index].scheme, - context.watch().appliedSchemeId), + Row( + children: [ + Opacity( + opacity: context.watch().appliedSchemeId == + localSchemes[index].scheme.id + ? 1 + : 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0), + child: Icon(Icons.done_rounded, size: 20), + ), + ), + Text(localSchemes[index].scheme.name ?? ''), + ], + ), + _getItemIcon(localSchemes[index].scheme), ], ), ), @@ -204,7 +216,7 @@ class _LocalManagerState extends State { ), DButton.duplicate(enabled: _selectedItemPath.notNull), DButton.apply( - enabled: _selectedItemPath.notNull, + enabled: true, onTap: () { var appliedId = localSchemes.firstWhere((ele) => ele.path == _selectedItemPath).scheme.id!; diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index c8cb52d..f668426 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -65,7 +65,8 @@ "paste": "paste" }, "str": { - "null": "Null" + "null": "Null", + "new_scheme": "New gesture scheme" }, "built_in_commands": { "ShowWorkspace": "ShowWorkspace", @@ -87,6 +88,10 @@ "gesture_prop_duplicated": { "title": "Duplicated !", "description": "Now you can go to other scheme and click the paste button ~" + }, + "scheme_name_conflict": { + "title": "Save failed!", + "description": "Scheme name conflict, please rename it!" } } } \ No newline at end of file diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index d9a46be..c623dcf 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -65,7 +65,8 @@ "paste": "粘贴" }, "str": { - "null": "无" + "null": "无", + "new_scheme": "新建手势方案" }, "built_in_commands": { "ShowWorkspace": "显示工作区", @@ -87,6 +88,10 @@ "gesture_prop_duplicated": { "title": "复制成功!", "description": "现在可以去其他方案中点击粘贴按钮试试哦~" + }, + "scheme_name_conflict": { + "title": "保存失败!", + "description": "方案名冲突,请重新命名!" } } } \ No newline at end of file -- 2.11.0 From f794515cc7cf374a3da7416786e3f5ee2b4dc006 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Wed, 15 Dec 2021 18:52:31 +0800 Subject: [PATCH 09/14] wip: add md editor for scheme description. --- .../cherry_toast/lib/resources/constants.dart | 1 - app/3rd_party/markdown_editor_ot/.gitignore | 15 + app/3rd_party/markdown_editor_ot/CHANGELOG.md | 5 + app/3rd_party/markdown_editor_ot/LICENSE | 339 ++++++++++ app/3rd_party/markdown_editor_ot/README.md | 23 + app/3rd_party/markdown_editor_ot/icons/demo.css | 539 +++++++++++++++ .../markdown_editor_ot/icons/demo_index.html | 720 +++++++++++++++++++++ .../markdown_editor_ot/icons/iconfont.css | 113 ++++ .../markdown_editor_ot/icons/iconfont.eot | Bin 0 -> 4920 bytes app/3rd_party/markdown_editor_ot/icons/iconfont.js | 1 + .../markdown_editor_ot/icons/iconfont.svg | 98 +++ .../markdown_editor_ot/icons/iconfont.ttf | Bin 0 -> 4752 bytes .../markdown_editor_ot/icons/iconfont.woff | Bin 0 -> 2804 bytes .../markdown_editor_ot/icons/iconfont.woff2 | Bin 0 -> 2296 bytes .../markdown_editor_ot/lib/customize_physics.dart | 29 + .../markdown_editor_ot/lib/fonts/iconfont.ttf | Bin 0 -> 4752 bytes .../markdown_editor_ot/lib/markdown_editor.dart | 4 + .../markdown_editor_ot/lib/src/action.dart | 288 +++++++++ .../markdown_editor_ot/lib/src/edit_perform.dart | 82 +++ .../markdown_editor_ot/lib/src/editor.dart | 352 ++++++++++ .../markdown_editor_ot/lib/src/preview.dart | 60 ++ app/3rd_party/markdown_editor_ot/pubspec.yaml | 61 ++ app/lib/main.dart | 5 +- app/lib/pages/gesture_editor.dart | 42 +- app/lib/widgets/dde_markdown_field.dart | 112 ++++ app/lib/widgets/dde_text_field.dart | 10 +- app/linux/flutter/generated_plugin_registrant.cc | 4 + app/linux/flutter/generated_plugins.cmake | 1 + app/pubspec.yaml | 4 + app/resources/langs/en.json | 4 + app/resources/langs/zh-CN.json | 4 + 31 files changed, 2907 insertions(+), 9 deletions(-) create mode 100644 app/3rd_party/markdown_editor_ot/.gitignore create mode 100644 app/3rd_party/markdown_editor_ot/CHANGELOG.md create mode 100644 app/3rd_party/markdown_editor_ot/LICENSE create mode 100644 app/3rd_party/markdown_editor_ot/README.md create mode 100644 app/3rd_party/markdown_editor_ot/icons/demo.css create mode 100644 app/3rd_party/markdown_editor_ot/icons/demo_index.html create mode 100644 app/3rd_party/markdown_editor_ot/icons/iconfont.css create mode 100644 app/3rd_party/markdown_editor_ot/icons/iconfont.eot create mode 100644 app/3rd_party/markdown_editor_ot/icons/iconfont.js create mode 100644 app/3rd_party/markdown_editor_ot/icons/iconfont.svg create mode 100644 app/3rd_party/markdown_editor_ot/icons/iconfont.ttf create mode 100644 app/3rd_party/markdown_editor_ot/icons/iconfont.woff create mode 100644 app/3rd_party/markdown_editor_ot/icons/iconfont.woff2 create mode 100644 app/3rd_party/markdown_editor_ot/lib/customize_physics.dart create mode 100644 app/3rd_party/markdown_editor_ot/lib/fonts/iconfont.ttf create mode 100644 app/3rd_party/markdown_editor_ot/lib/markdown_editor.dart create mode 100644 app/3rd_party/markdown_editor_ot/lib/src/action.dart create mode 100644 app/3rd_party/markdown_editor_ot/lib/src/edit_perform.dart create mode 100644 app/3rd_party/markdown_editor_ot/lib/src/editor.dart create mode 100644 app/3rd_party/markdown_editor_ot/lib/src/preview.dart create mode 100644 app/3rd_party/markdown_editor_ot/pubspec.yaml create mode 100644 app/lib/widgets/dde_markdown_field.dart diff --git a/app/3rd_party/cherry_toast/lib/resources/constants.dart b/app/3rd_party/cherry_toast/lib/resources/constants.dart index de2c168..8bc76ca 100755 --- a/app/3rd_party/cherry_toast/lib/resources/constants.dart +++ b/app/3rd_party/cherry_toast/lib/resources/constants.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; const String PACKAGE_NAME = "cherry_toast"; diff --git a/app/3rd_party/markdown_editor_ot/.gitignore b/app/3rd_party/markdown_editor_ot/.gitignore new file mode 100644 index 0000000..4272058 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +.dart_tool/ + +.packages +.flutter-plugins +.pub/ + +.idea/ +*.iml +*.lock + + +build/ +ios/ +android/ diff --git a/app/3rd_party/markdown_editor_ot/CHANGELOG.md b/app/3rd_party/markdown_editor_ot/CHANGELOG.md new file mode 100644 index 0000000..9a5c387 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.2 +* Fix icon not display. +## 1.0.1 +* Change directory structure. +## 1.0.0 diff --git a/app/3rd_party/markdown_editor_ot/LICENSE b/app/3rd_party/markdown_editor_ot/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/app/3rd_party/markdown_editor_ot/README.md b/app/3rd_party/markdown_editor_ot/README.md new file mode 100644 index 0000000..53551d9 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/README.md @@ -0,0 +1,23 @@ +# markdown_editor + +Simple and easy to implement your markdown editor, it uses its own parser. + +![show](https://xia-weiyang.github.io/gif/markdown_editor.gif) + +If you only need to render, you can refer to [https://github.com/xia-weiyang/markdown_core](https://github.com/xia-weiyang/markdown_core) + +``` dart +MarkdownEditor( + initText: 'initText', + initTitle: 'initText', + onTapLink: (link){ + print('点击了链接 $link'); + }, + imageWidget: (imageUrl) { + return // Your image widget ; + }, + imageSelect: (){ // Click image select btn + return // selected image link; + }, +) +``` diff --git a/app/3rd_party/markdown_editor_ot/icons/demo.css b/app/3rd_party/markdown_editor_ot/icons/demo.css new file mode 100644 index 0000000..a67054a --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/icons/demo.css @@ -0,0 +1,539 @@ +/* Logo 字体 */ +@font-face { + font-family: "iconfont logo"; + src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); + src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); +} + +.logo { + font-family: "iconfont logo"; + font-size: 160px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* tabs */ +.nav-tabs { + position: relative; +} + +.nav-tabs .nav-more { + position: absolute; + right: 0; + bottom: 0; + height: 42px; + line-height: 42px; + color: #666; +} + +#tabs { + border-bottom: 1px solid #eee; +} + +#tabs li { + cursor: pointer; + width: 100px; + height: 40px; + line-height: 40px; + text-align: center; + font-size: 16px; + border-bottom: 2px solid transparent; + position: relative; + z-index: 1; + margin-bottom: -1px; + color: #666; +} + + +#tabs .active { + border-bottom-color: #f00; + color: #222; +} + +.tab-container .content { + display: none; +} + +/* 页面布局 */ +.main { + padding: 30px 100px; + width: 960px; + margin: 0 auto; +} + +.main .logo { + color: #333; + text-align: left; + margin-bottom: 30px; + line-height: 1; + height: 110px; + margin-top: -50px; + overflow: hidden; + *zoom: 1; +} + +.main .logo a { + font-size: 160px; + color: #333; +} + +.helps { + margin-top: 40px; +} + +.helps pre { + padding: 20px; + margin: 10px 0; + border: solid 1px #e7e1cd; + background-color: #fffdef; + overflow: auto; +} + +.icon_lists { + width: 100% !important; + overflow: hidden; + *zoom: 1; +} + +.icon_lists li { + width: 100px; + margin-bottom: 10px; + margin-right: 20px; + text-align: center; + list-style: none !important; + cursor: default; +} + +.icon_lists li .code-name { + line-height: 1.2; +} + +.icon_lists .icon { + display: block; + height: 100px; + line-height: 100px; + font-size: 42px; + margin: 10px auto; + color: #333; + -webkit-transition: font-size 0.25s linear, width 0.25s linear; + -moz-transition: font-size 0.25s linear, width 0.25s linear; + transition: font-size 0.25s linear, width 0.25s linear; +} + +.icon_lists .icon:hover { + font-size: 100px; +} + +.icon_lists .svg-icon { + /* 通过设置 font-size 来改变图标大小 */ + width: 1em; + /* 图标和文字相邻时,垂直对齐 */ + vertical-align: -0.15em; + /* 通过设置 color 来改变 SVG 的颜色/fill */ + fill: currentColor; + /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 + normalize.css 中也包含这行 */ + overflow: hidden; +} + +.icon_lists li .name, +.icon_lists li .code-name { + color: #666; +} + +/* markdown 样式 */ +.markdown { + color: #666; + font-size: 14px; + line-height: 1.8; +} + +.highlight { + line-height: 1.5; +} + +.markdown img { + vertical-align: middle; + max-width: 100%; +} + +.markdown h1 { + color: #404040; + font-weight: 500; + line-height: 40px; + margin-bottom: 24px; +} + +.markdown h2, +.markdown h3, +.markdown h4, +.markdown h5, +.markdown h6 { + color: #404040; + margin: 1.6em 0 0.6em 0; + font-weight: 500; + clear: both; +} + +.markdown h1 { + font-size: 28px; +} + +.markdown h2 { + font-size: 22px; +} + +.markdown h3 { + font-size: 16px; +} + +.markdown h4 { + font-size: 14px; +} + +.markdown h5 { + font-size: 12px; +} + +.markdown h6 { + font-size: 12px; +} + +.markdown hr { + height: 1px; + border: 0; + background: #e9e9e9; + margin: 16px 0; + clear: both; +} + +.markdown p { + margin: 1em 0; +} + +.markdown>p, +.markdown>blockquote, +.markdown>.highlight, +.markdown>ol, +.markdown>ul { + width: 80%; +} + +.markdown ul>li { + list-style: circle; +} + +.markdown>ul li, +.markdown blockquote ul>li { + margin-left: 20px; + padding-left: 4px; +} + +.markdown>ul li p, +.markdown>ol li p { + margin: 0.6em 0; +} + +.markdown ol>li { + list-style: decimal; +} + +.markdown>ol li, +.markdown blockquote ol>li { + margin-left: 20px; + padding-left: 4px; +} + +.markdown code { + margin: 0 3px; + padding: 0 5px; + background: #eee; + border-radius: 3px; +} + +.markdown strong, +.markdown b { + font-weight: 600; +} + +.markdown>table { + border-collapse: collapse; + border-spacing: 0px; + empty-cells: show; + border: 1px solid #e9e9e9; + width: 95%; + margin-bottom: 24px; +} + +.markdown>table th { + white-space: nowrap; + color: #333; + font-weight: 600; +} + +.markdown>table th, +.markdown>table td { + border: 1px solid #e9e9e9; + padding: 8px 16px; + text-align: left; +} + +.markdown>table th { + background: #F7F7F7; +} + +.markdown blockquote { + font-size: 90%; + color: #999; + border-left: 4px solid #e9e9e9; + padding-left: 0.8em; + margin: 1em 0; +} + +.markdown blockquote p { + margin: 0; +} + +.markdown .anchor { + opacity: 0; + transition: opacity 0.3s ease; + margin-left: 8px; +} + +.markdown .waiting { + color: #ccc; +} + +.markdown h1:hover .anchor, +.markdown h2:hover .anchor, +.markdown h3:hover .anchor, +.markdown h4:hover .anchor, +.markdown h5:hover .anchor, +.markdown h6:hover .anchor { + opacity: 1; + display: inline-block; +} + +.markdown>br, +.markdown>p>br { + clear: both; +} + + +.hljs { + display: block; + background: white; + padding: 0.5em; + color: #333333; + overflow-x: auto; +} + +.hljs-comment, +.hljs-meta { + color: #969896; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-strong, +.hljs-emphasis, +.hljs-quote { + color: #df5000; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #a71d5d; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute { + color: #0086b3; +} + +.hljs-section, +.hljs-name { + color: #63a35c; +} + +.hljs-tag { + color: #333333; +} + +.hljs-title, +.hljs-attr, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #795da3; +} + +.hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.hljs-link { + text-decoration: underline; +} + +/* 代码高亮 */ +/* PrismJS 1.15.0 +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, +pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, +code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, +pre[class*="language-"] ::selection, +code[class*="language-"]::selection, +code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre)>code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre)>code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #9a6e3a; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} diff --git a/app/3rd_party/markdown_editor_ot/icons/demo_index.html b/app/3rd_party/markdown_editor_ot/icons/demo_index.html new file mode 100644 index 0000000..efc05fc --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/icons/demo_index.html @@ -0,0 +1,720 @@ + + + + + IconFont Demo + + + + + + + + + + + +
+

+ +
+
+
    + +
  • + +
    format-bold
    +
    &#xe757;
    +
  • + +
  • + +
    format-color-text
    +
    &#xe758;
    +
  • + +
  • + +
    format-header-1
    +
    &#xe75b;
    +
  • + +
  • + +
    format-header-2
    +
    &#xe75c;
    +
  • + +
  • + +
    format-header-3
    +
    &#xe75d;
    +
  • + +
  • + +
    format-header-4
    +
    &#xe75e;
    +
  • + +
  • + +
    format-header-5
    +
    &#xe75f;
    +
  • + +
  • + +
    format-header-6
    +
    &#xe760;
    +
  • + +
  • + +
    format-italic
    +
    &#xe762;
    +
  • + +
  • + +
    format-list-bulleted
    +
    &#xe764;
    +
  • + +
  • + +
    format-list-numbers
    +
    &#xe765;
    +
  • + +
  • + +
    format-quote
    +
    &#xe768;
    +
  • + +
  • + +
    format-size
    +
    &#xe769;
    +
  • + +
  • + +
    format-strikethrough
    +
    &#xe76a;
    +
  • + +
  • + +
    format-title
    +
    &#xe76f;
    +
  • + +
  • + +
    format-underline
    +
    &#xe770;
    +
  • + +
  • + +
    image
    +
    &#xe7ac;
    +
  • + +
  • + +
    link-variant
    +
    &#xe7d8;
    +
  • + +
  • + +
    redo
    +
    &#xe873;
    +
  • + +
  • + +
    redo-variant
    +
    &#xe874;
    +
  • + +
  • + +
    timer
    +
    &#xe8eb;
    +
  • + +
  • + +
    undo-variant
    +
    &#xe907;
    +
  • + +
  • + +
    undo
    +
    &#xe908;
    +
  • + +
  • + +
    weather-cloudy
    +
    &#xe92d;
    +
  • + +
+
+

Unicode 引用

+
+ +

Unicode 是字体在网页端最原始的应用方式,特点是:

+
    +
  • 兼容性最好,支持 IE6+,及所有现代浏览器。
  • +
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • +
  • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。
  • +
+
+

注意:新版 iconfont 支持多色图标,这些多色图标在 Unicode 模式下将不能使用,如果有需求建议使用symbol 的引用方式

+
+

Unicode 使用步骤如下:

+

第一步:拷贝项目下面生成的 @font-face

+
@font-face {
+  font-family: 'iconfont';
+  src: url('iconfont.eot');
+  src: url('iconfont.eot?#iefix') format('embedded-opentype'),
+      url('iconfont.woff2') format('woff2'),
+      url('iconfont.woff') format('woff'),
+      url('iconfont.ttf') format('truetype'),
+      url('iconfont.svg#iconfont') format('svg');
+}
+
+

第二步:定义使用 iconfont 的样式

+
.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+

第三步:挑选相应图标并获取字体编码,应用于页面

+
+<span class="iconfont">&#x33;</span>
+
+
+

"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

+
+
+
+
+
    + +
  • + +
    + format-bold +
    +
    .icon-format-bold +
    +
  • + +
  • + +
    + format-color-text +
    +
    .icon-format-color-text +
    +
  • + +
  • + +
    + format-header-1 +
    +
    .icon-format-header- +
    +
  • + +
  • + +
    + format-header-2 +
    +
    .icon-format-header-1 +
    +
  • + +
  • + +
    + format-header-3 +
    +
    .icon-format-header-2 +
    +
  • + +
  • + +
    + format-header-4 +
    +
    .icon-format-header-3 +
    +
  • + +
  • + +
    + format-header-5 +
    +
    .icon-format-header-4 +
    +
  • + +
  • + +
    + format-header-6 +
    +
    .icon-format-header-5 +
    +
  • + +
  • + +
    + format-italic +
    +
    .icon-format-italic +
    +
  • + +
  • + +
    + format-list-bulleted +
    +
    .icon-format-list-bulleted +
    +
  • + +
  • + +
    + format-list-numbers +
    +
    .icon-format-list-numbers +
    +
  • + +
  • + +
    + format-quote +
    +
    .icon-format-quote +
    +
  • + +
  • + +
    + format-size +
    +
    .icon-format-size +
    +
  • + +
  • + +
    + format-strikethrough +
    +
    .icon-format-strikethrough +
    +
  • + +
  • + +
    + format-title +
    +
    .icon-format-title +
    +
  • + +
  • + +
    + format-underline +
    +
    .icon-format-underline +
    +
  • + +
  • + +
    + image +
    +
    .icon-image +
    +
  • + +
  • + +
    + link-variant +
    +
    .icon-link-variant +
    +
  • + +
  • + +
    + redo +
    +
    .icon-redo +
    +
  • + +
  • + +
    + redo-variant +
    +
    .icon-redo-variant +
    +
  • + +
  • + +
    + timer +
    +
    .icon-timer +
    +
  • + +
  • + +
    + undo-variant +
    +
    .icon-undo-variant +
    +
  • + +
  • + +
    + undo +
    +
    .icon-undo +
    +
  • + +
  • + +
    + weather-cloudy +
    +
    .icon-weather-cloudy +
    +
  • + +
+
+

font-class 引用

+
+ +

font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

+

与 Unicode 使用方式相比,具有如下特点:

+
    +
  • 兼容性良好,支持 IE8+,及所有现代浏览器。
  • +
  • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • +
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
  • +
  • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。
  • +
+

使用步骤如下:

+

第一步:引入项目下面生成的 fontclass 代码:

+
<link rel="stylesheet" href="./iconfont.css">
+
+

第二步:挑选相应图标并获取类名,应用于页面:

+
<span class="iconfont icon-xxx"></span>
+
+
+

" + iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

+
+
+
+
+
    + +
  • + +
    format-bold
    +
    #icon-format-bold
    +
  • + +
  • + +
    format-color-text
    +
    #icon-format-color-text
    +
  • + +
  • + +
    format-header-1
    +
    #icon-format-header-
    +
  • + +
  • + +
    format-header-2
    +
    #icon-format-header-1
    +
  • + +
  • + +
    format-header-3
    +
    #icon-format-header-2
    +
  • + +
  • + +
    format-header-4
    +
    #icon-format-header-3
    +
  • + +
  • + +
    format-header-5
    +
    #icon-format-header-4
    +
  • + +
  • + +
    format-header-6
    +
    #icon-format-header-5
    +
  • + +
  • + +
    format-italic
    +
    #icon-format-italic
    +
  • + +
  • + +
    format-list-bulleted
    +
    #icon-format-list-bulleted
    +
  • + +
  • + +
    format-list-numbers
    +
    #icon-format-list-numbers
    +
  • + +
  • + +
    format-quote
    +
    #icon-format-quote
    +
  • + +
  • + +
    format-size
    +
    #icon-format-size
    +
  • + +
  • + +
    format-strikethrough
    +
    #icon-format-strikethrough
    +
  • + +
  • + +
    format-title
    +
    #icon-format-title
    +
  • + +
  • + +
    format-underline
    +
    #icon-format-underline
    +
  • + +
  • + +
    image
    +
    #icon-image
    +
  • + +
  • + +
    link-variant
    +
    #icon-link-variant
    +
  • + +
  • + +
    redo
    +
    #icon-redo
    +
  • + +
  • + +
    redo-variant
    +
    #icon-redo-variant
    +
  • + +
  • + +
    timer
    +
    #icon-timer
    +
  • + +
  • + +
    undo-variant
    +
    #icon-undo-variant
    +
  • + +
  • + +
    undo
    +
    #icon-undo
    +
  • + +
  • + +
    weather-cloudy
    +
    #icon-weather-cloudy
    +
  • + +
+
+

Symbol 引用

+
+ +

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 + 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

+
    +
  • 支持多色图标了,不再受单色限制。
  • +
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • +
  • 兼容性较差,支持 IE9+,及现代浏览器。
  • +
  • 浏览器渲染 SVG 的性能一般,还不如 png。
  • +
+

使用步骤如下:

+

第一步:引入项目下面生成的 symbol 代码:

+
<script src="./iconfont.js"></script>
+
+

第二步:加入通用 CSS 代码(引入一次就行):

+
<style>
+.icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+</style>
+
+

第三步:挑选相应图标并获取类名,应用于页面:

+
<svg class="icon" aria-hidden="true">
+  <use xlink:href="#icon-xxx"></use>
+</svg>
+
+
+
+ +
+
+ + + diff --git a/app/3rd_party/markdown_editor_ot/icons/iconfont.css b/app/3rd_party/markdown_editor_ot/icons/iconfont.css new file mode 100644 index 0000000..a87d3b4 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/icons/iconfont.css @@ -0,0 +1,113 @@ +@font-face {font-family: "iconfont"; + src: url('iconfont.eot?t=1556453824121'); /* IE9 */ + src: url('iconfont.eot?t=1556453824121#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), + url('iconfont.woff?t=1556453824121') format('woff'), + url('iconfont.ttf?t=1556453824121') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ + url('iconfont.svg?t=1556453824121#iconfont') format('svg'); /* iOS 4.1- */ +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-format-bold:before { + content: "\e757"; +} + +.icon-format-color-text:before { + content: "\e758"; +} + +.icon-format-header-:before { + content: "\e75b"; +} + +.icon-format-header-1:before { + content: "\e75c"; +} + +.icon-format-header-2:before { + content: "\e75d"; +} + +.icon-format-header-3:before { + content: "\e75e"; +} + +.icon-format-header-4:before { + content: "\e75f"; +} + +.icon-format-header-5:before { + content: "\e760"; +} + +.icon-format-italic:before { + content: "\e762"; +} + +.icon-format-list-bulleted:before { + content: "\e764"; +} + +.icon-format-list-numbers:before { + content: "\e765"; +} + +.icon-format-quote:before { + content: "\e768"; +} + +.icon-format-size:before { + content: "\e769"; +} + +.icon-format-strikethrough:before { + content: "\e76a"; +} + +.icon-format-title:before { + content: "\e76f"; +} + +.icon-format-underline:before { + content: "\e770"; +} + +.icon-image:before { + content: "\e7ac"; +} + +.icon-link-variant:before { + content: "\e7d8"; +} + +.icon-redo:before { + content: "\e873"; +} + +.icon-redo-variant:before { + content: "\e874"; +} + +.icon-timer:before { + content: "\e8eb"; +} + +.icon-undo-variant:before { + content: "\e907"; +} + +.icon-undo:before { + content: "\e908"; +} + +.icon-weather-cloudy:before { + content: "\e92d"; +} + diff --git a/app/3rd_party/markdown_editor_ot/icons/iconfont.eot b/app/3rd_party/markdown_editor_ot/icons/iconfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..954e8600eb413da8c6a9f7c48ccabfcb16b30385 GIT binary patch literal 4920 zcmd^DZERcB8Ghe$ow($)j=!3g20L~Vh#M!hW4nSRtxEdE+Kv^j=~|$5lAFYh9S6sO z7WQ%3n8r4V1*8^MrfE{qL}L(0`?U#){@Bm4X%dObq#`8PZrThXF~PJSarT^hulo_Y z{g||0yVviz?|IKT@ArAn_1)nnnr|Uu5+pA{HHwLVGS3n__@LFl(VAp*uRL?=vm4YZ zwvn1%{^hUGO<-K9o5-CR|NB4x@d;7Q zX`(wElUioX^W|?|$J~zq$w?5LA1dd8zXJ$Q=1Zr?fPmloXvT7d(adJgoiAYhF#7%Z z%;_1vhrWUFP3Q-vGkI<3;mWsRC*+I#OkuVZIOh2iZ224JT#&je`kGFUIxw9GZPkTl zA5E)C0rj&}&s;gtJn#k8TXQe3{PvQZWIfjw9i*hu-C&2y;?zoes8-H0sdZEbY|ZRj z<8{iwm zM$XtWuRk6f-!n$Lj7P5aSoSMA=@_)Fm!kgH01-6+nbX@War|dn{`;UxO}60)kAdzW znG>6lf}j{jjg!WRp&6%)8RHG(@^Wc;a`P#WUIU*y;rlzFPdznIBQ{Pmv~f`jxyeIbY#Toz=h%w& z?d1Ib&>EzvEK7#G;1%N$3uIJ{qZY^=VH~#rJ|^Q)3uNLMk69oy#CY5Snf1mA3lIx3 zPFf)Mh%sUT;zP!m1u}CC%>u-Xj7bX+JuIfy`ZS1L4 z%r?UQAx=fPE9eRW>F*S~&Ys;84!^CV*`j}TS0{@T4^+P7ZF`P#Y(?jAD$Hh^^6d&f zaDrFM;`pBJhh&_vO??n2%-y(!>p7kZ`r`_pAJ^uuo$TfMzLVFmwyE+t>#Jp57phdv zT$hG^_0o2C+$Hajyn~6j%b7@_@72|^iXShgr>0a@EUqC(R?Cbksq!y|Mh7c&gO_gd z3g61>9&YdK-^t1T&h}bw2$r_7-&5PnL7TGzR-u^xkFBx2F_km>51bTR{C~j4zDZPU zum7V>`(m-s{tRPEcyQB|s{WSDu3KetJm&MdJhc(X9^|+;o`CBUA#W(rpR~maDu=)j zjatT~#%`7;)L5vZ%JE2~td=8@5Q>c%PsNhw!@0B0W1)ofpf}`I63(g&eQ{BZMAZDv zycg5lU0F&ma^#g)>=;-)Be!yd+95A8#oxrvkZr~g_jg)0-DC%3z-Qv*0Vu4RO3UVE zkpdC4JSz=iINCH^rj3~c)wtcgUW1r#D~I4WxN|EKSBg%!`jc)rv}&SsBRpvP5`1qJ zYnI*C?Il zDGn#Y_^sK`%DHs9N|Uat>ak?%QIGnug^+t3OkfvVpv0Xr-%|L=l#4579AzvMq3eoB zWy(v34(SIE>XAs=`blHcm4!4fK$&Bm-N!o93*8m+b^E#(Alurn5s`)#sEZx(lq=|p z*H{ms>R6;#R9(fRydXE|g4x>Z%61*Hvo;az@R}Pwq2sFPOpE#w>lLJ>4##7WF73UM z9}21js;(PWBK$B0v4TaqLaU@MEi6@Zh>NHQ7p_$utF0JEOu2+Lvb-b~SM(JbSzCZ@ z6T~R8&c?yL10@1A$kQa+qap#UjTc3F70mPV%9JmX97sldQ_8%W7BA*mt(4nR-F|;J z$_nuOi>9B}))Xj)lM1z-{V8{_#?{{$3Zejkn?D#!^3MI{d$2#76VyMt_9>>Nz@%vc0_*-eGY!i=sQ#+2xFK2->3$nf;wLIKJE{34*@+y2J7M zcg^2z71V-W>&E-K1J%oP5C;FiIAwmB}pX3ZRa zdYOs4)vwqZYZFM$2q)qY3WY*`XIIMal>1!l`Y&y@_R70Epp49-TQ~@3W5CbYSpM8`ylvUI{Ga5tae<1y)y5Uj z!#3^!j{jbg3Eng7so%zHLEmZPbrhj}HeQdnydvrdJMflC-Sh(+XXN=?HZJfM`L2yC zpucD14&Ya8yasPWpW1jWZDhs9>nP1`8?UEM&eq*u)G{S)EHH8=kR2^dj~Av(byehu zHZhmW6sz6pGp!Y8vxVuv_Gn*~v`?GXid8DD+FeWg;UyS zDLPpy&D`7DD+NhTI^_j)X^9261m2i(kzj0_?pggd>_SX&#-#Bu>12kifodTax%#PBexoU>) zrC!|CuOrp@QS+-wUgn8;p5Tf97Pq}xS+K$muHjmC@&>+x>+rVRz>TQf&AgFac(-@6 zhrR4$KX2kz-c0r5gKQ5I##}Z&TF4cOJtggQsmZ3uPa|5fr}@_I_I3T(x_*3J zzkgjn(P;C_mNL2QsK@T*va`@TTaN~tMj d^IEY1i>{L#veWbww#XzLGny;Rjh#VI{sk#dxs3n- literal 0 HcmV?d00001 diff --git a/app/3rd_party/markdown_editor_ot/icons/iconfont.js b/app/3rd_party/markdown_editor_ot/icons/iconfont.js new file mode 100644 index 0000000..d7b3f07 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/icons/iconfont.js @@ -0,0 +1 @@ +!function(v){var h,t='',o=(h=document.getElementsByTagName("script"))[h.length-1].getAttribute("data-injectcss");if(o&&!v.__iconfont__svg__cssinject__){v.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(h){console&&console.log(h)}}!function(h){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(h,0);else{var o=function(){document.removeEventListener("DOMContentLoaded",o,!1),h()};document.addEventListener("DOMContentLoaded",o,!1)}else document.attachEvent&&(t=h,l=v.document,i=!1,a=function(){i||(i=!0,t())},(e=function(){try{l.documentElement.doScroll("left")}catch(h){return void setTimeout(e,50)}a()})(),l.onreadystatechange=function(){"complete"==l.readyState&&(l.onreadystatechange=null,a())});var t,l,i,a,e}(function(){var h,o;(h=document.createElement("div")).innerHTML=t,t=null,(o=h.getElementsByTagName("svg")[0])&&(o.setAttribute("aria-hidden","true"),o.style.position="absolute",o.style.width=0,o.style.height=0,o.style.overflow="hidden",function(h,o){o.firstChild?function(h,o){o.parentNode.insertBefore(h,o)}(h,o.firstChild):o.appendChild(h)}(o,document.body))})}(window); \ No newline at end of file diff --git a/app/3rd_party/markdown_editor_ot/icons/iconfont.svg b/app/3rd_party/markdown_editor_ot/icons/iconfont.svg new file mode 100644 index 0000000..d299081 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/icons/iconfont.svg @@ -0,0 +1,98 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/3rd_party/markdown_editor_ot/icons/iconfont.ttf b/app/3rd_party/markdown_editor_ot/icons/iconfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fb6f07442d2aaf77f14a677bd07849bfa7599c7f GIT binary patch literal 4752 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 S15(KsX%xD literal 0 HcmV?d00001 diff --git a/app/3rd_party/markdown_editor_ot/lib/customize_physics.dart b/app/3rd_party/markdown_editor_ot/lib/customize_physics.dart new file mode 100644 index 0000000..48f9398 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/lib/customize_physics.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class CustomizePhysics extends ScrollPhysics { + const CustomizePhysics({ + ScrollPhysics? parent, + }) : super(parent: parent); + + @override + CustomizePhysics applyTo(ScrollPhysics? ancestor) { + return CustomizePhysics(parent: buildParent(ancestor)); + } + + @override + bool shouldAcceptUserOffset(ScrollMetrics position) => false; + + @override + bool get allowImplicitScrolling => false; + + @override + double applyBoundaryConditions(ScrollMetrics position, double value) { + return 0; + } + + @override + Simulation? createBallisticSimulation( + ScrollMetrics position, double velocity) { + return null; + } +} diff --git a/app/3rd_party/markdown_editor_ot/lib/fonts/iconfont.ttf b/app/3rd_party/markdown_editor_ot/lib/fonts/iconfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fb6f07442d2aaf77f14a677bd07849bfa7599c7f GIT binary patch literal 4752 zcmd^DYiu0V6+Y+A+8g}LdS7uu%&vF625gq%<%~ngGe#9@`u5dd+$b z32MtgsY+Xk2BZWUs;W{@MZ6%8`l}TZ`O%-HRV8eNN+2X?$BB!OsG#Z(ulwDZS#S*P zk4pX38J|1no_p_k-*fMpn1~ulCxr$M+^6n+qx$QQiFg;Nd%t;Ke{AUOZ`_9dchJv_ z6|%G6{>xuKArcr@8Yc7SCjS1G_k8}FS1|VjKxzsE=ljYf;BNxL zQ-$)`aUkIL4w~_NaV)#lbL+EMKZ1UukUcxgchI*mz6JfjOtzp6-(USMQ48dY!fbJ_ z95~_m6Vc|sVa{cO?ux#q(}NC7CqgT&S^u<~6i`1obN9Yet%ILYqc!*1%5SgAiPm>* z(LqWE-AyDU6_4c9PW!0d>`7_^UZOR#Z;dxl2kjsp*?;(cth5+gBb?7oWuTM#(!#kd4*z(wSwvtTiS-ty%YP!X^-*1^p=maSLST7@7r$85vU+AbMov zEI=H|IAZ}KN=DHF#FmU%3lL>8UbO)6CgYj~h&(ONS%4g%<+26H23lUR0C_>zp{oRB z2wjJ+5|Ar&z1ISnHRh9`d+AYngO<6CkMXnoE50Fa7b9X+=;C?truaxXtjs9CR6cU} z9J?G(NuObde=e5bjaJkV>Ix;}9qg%B%r?sYVNUmPchD6Cl8A^s7cOiIhu_fAY|}rv zEyCi|J=M>7$G+nnThTe34zt;&eS3oUoZ{7rIJs}<5g8|JQy;_$b2o0|MvkY0{qN!&`#;!nAQlT9%rd5ghqk;|)8CTWeX~rC$9!Iw zr#=eVLmcXSRqLZ#f$_WP^s?MmNVAl`hPXlPs&zOj zRO}XYHHxj34u@~GK0p7GA}X0R%4B$&!>KU-*6eTfVkT3gNmteMSTgl!ME%%C$UP1w zv5PHG;!c@wY5d8Qiz{XvWh|4SFBFl=RF;k$(GMTiqtT4@CymWi7c#s6WlltTPDC;b zJyr7c_<9x~+uE;Dk%1Pdi(T=wE9i>XSs$V5Sfp1~UB#!eAUEiO+1l$Wb{(>_HW}>l znj1c;c}Xs==qoa^wgB5Eh*4ynje~m^N(5?wL{V#Dk1w6F==Q97zZ29B5lii6xqq>gf7iS<)I2==!rq0w z!^3;H&Es~vyZRD+?u%_6&ySn`{a9Nwk2Htco3QYO;bFA9lQEAQE4#qqVq1T_|8moR z7;I|})#6e?9^vF<96%^A@+gY={LY|DaSdKjU;MCdyzBKxhKs$IaBRF8zl)VC&vZ_I zPei+3VuaStsf;4);C687Y!NW=E>4EP54_?|f4bZ0cO&ch>MreQPtQ?p*M$qm@nrrOhoE%e7R8)1U>PJ!|}=!X8YRE_P}85@sZ2#Ivnp_e)*bXNA<}}rXtU= zsDe3H6y*wL3jbhm+t3|59M@j9W{y0*%)~wFS8R=S2qb5elW_=zLLtAiJMDMMeJ=L= zm$q7aoGbXRdq^ zlO<6>Rl?n+OKu-%h(F22^qJFz6R=N6pG9QYLBwWAMBs`eLjiI2WblW_XD>DStYbD4 z7&;O?HrL|ak~+kEC?#+V#?;{8(ddbD;_yR4IbW2=4h|ozmkfCPh?B*^RQjMiV`K~o zx3KrEN&p&BxB`0E#vQ=%+e^lzM%?mB zs3YvaEt7ia2R6>g^Ve-$;1>CojVqwPZQ~B$@7Z`AZbKj2cs*@q#l{;b!)_aIqzLC4 z?k;KBvNj$VJr~H06=x=jGv$UFa!i|?&u2@uZta`VN^`m5Okii(Z1z(lE721GE!A#0I{y>HucVm@_x( zGl$FbB!0A&@lIg<1h^Hc0Bpalp$1LzzymaEj-3IYG0g6xDRb2<-AVm;YZle!_nDR! zWS*Gk3BLGk@wHDo3s%^{bzIL*-o&?X18&Pr+>FZI%A47RyS}4POc?-AmR%)Cm zmI~Q&?`SbU?y~W*V!l}FEo*1XEjC5|YeXycw%**`xuGB1(2sBECpPqx%{ISWIh)Uo zdF)<3HwS&@^LY*F=Uz{onJ ActionImageState(); +} + +class ActionImageState extends State { + IconData? _getImageIconCode() { + return _defaultImageAttributes + .firstWhere((img) => img.type == widget.type) + .iconData; + } + + void _disposeAction() { + var firstWhere = + _defaultImageAttributes.firstWhere((img) => img.type == widget.type); + if (firstWhere.type == ActionType.image && widget.getCursorPosition != null) { + var cursorPosition = widget.getCursorPosition!(); + if (widget.imageSelect != null) { + widget.imageSelect!().then( + (str) { + debugPrint('Image select $str'); + if (str.isNotEmpty) { + // 延迟执行它,等待TextFiled获取焦点 + // 否则将无法成功插入文本 + Timer(const Duration(milliseconds: 200), () { + widget.tap(widget.type, '![]($str)', 0, cursorPosition); + }); + } + }, + onError: print, + ); + return; + } + } + widget.tap(widget.type, firstWhere.text ?? '', firstWhere.positionReverse ?? 0); + } + + @override + Widget build(BuildContext context) { + return Tooltip( + preferBelow: false, + message: _defaultImageAttributes + .firstWhere((img) => img.type == widget.type) + .tip, + child: IconButton( + icon: Icon( + _getImageIconCode(), + color: widget.color, + ), + onPressed: _disposeAction, + ), + ); + } +} + +const _fontPackage = 'markdown_editor_ot'; + +const _defaultImageAttributes = [ + ImageAttributes( + type: ActionType.done, + tip: '完成', + iconData: Icons.done, + ), + ImageAttributes( + type: ActionType.undo, + tip: '撤销', + iconData: const IconData( + 0xe907, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.redo, + tip: '恢复', + iconData: const IconData( + 0xe874, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.image, + text: '![]()', + tip: '图片', + positionReverse: 3, + iconData: const IconData( + 0xe7ac, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.link, + text: '[]()', + tip: '链接', + positionReverse: 3, + iconData: const IconData( + 0xe7d8, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.fontBold, + text: '****', + tip: '加粗', + positionReverse: 2, + iconData: const IconData( + 0xe757, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.fontItalic, + text: '**', + tip: '斜体', + positionReverse: 1, + iconData: const IconData( + 0xe762, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.fontStrikethrough, + text: '~~~~', + tip: '删除线', + positionReverse: 2, + iconData: const IconData( + 0xe76a, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.textQuote, + text: '\n>', + tip: '文字引用', + positionReverse: 0, + iconData: const IconData( + 0xe768, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.list, + text: '\n- ', + tip: '无序列表', + positionReverse: 0, + iconData: const IconData( + 0xe764, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.h4, + text: '\n#### ', + tip: '四级标题', + positionReverse: 0, + iconData: const IconData( + 0xe75e, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.h5, + text: '\n##### ', + tip: '五级标题', + positionReverse: 0, + iconData: const IconData( + 0xe75f, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.h1, + text: '\n# ', + tip: '一级标题', + positionReverse: 0, + iconData: const IconData( + 0xe75b, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.h2, + text: '\n## ', + tip: '二级标题', + positionReverse: 0, + iconData: const IconData( + 0xe75c, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), + ImageAttributes( + type: ActionType.h3, + text: '\n### ', + tip: '三级标题', + positionReverse: 0, + iconData: const IconData( + 0xe75d, + fontFamily: 'MyIconFont', + fontPackage: _fontPackage, + ), + ), +]; + +enum ActionType { + done, + undo, + redo, + image, + link, + fontBold, + fontItalic, + fontStrikethrough, + fontDeleteLine, + textQuote, + list, + h1, + h2, + h3, + h4, + h5, +} + +class ImageAttributes { + const ImageAttributes({ + this.tip = '', + this.text, + this.positionReverse, + required this.type, + required this.iconData, + }); + + final ActionType type; + final IconData iconData; + final String tip; + final String? text; + final int? positionReverse; +} + +/// Call this method after clicking the [ActionImage] and completing a series of actions. +/// [text] Adding text. +/// [position] Cursor position that reverse order. +/// [cursorPosition] Will start insert text at this position. +typedef void TapFinishCallback( + ActionType type, + String text, + int positionReverse, [ + int? cursorPosition, +]); + +/// Call this method after clicking the ImageAction. +/// return your select image path. +typedef Future ImageSelectCallback(); + +/// Get the current cursor position. +typedef int GetCursorPosition(); diff --git a/app/3rd_party/markdown_editor_ot/lib/src/edit_perform.dart b/app/3rd_party/markdown_editor_ot/lib/src/edit_perform.dart new file mode 100644 index 0000000..2ab3320 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/lib/src/edit_perform.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +/// 撤销与前进 +class EditPerform { + EditPerform( + this._textEditingController, { + this.initText = '', + }); + + /// 最大的存储长度 + final _maxLength = 50; + + /// 初始文本 + final String initText; + + var _undoList = <_EditData>[]; + var _redoList = <_EditData>[]; + + final TextEditingController _textEditingController; + + void change(text) { + if (_textEditingController.text != '') { + if (_undoList.isNotEmpty) { + if (_textEditingController.text == _undoList.last.text) return; + } + if (_undoList.length >= _maxLength) _undoList.removeAt(0); + _undoList.add(_EditData(_textEditingController.text, + _textEditingController.selection.baseOffset)); + _redoList.clear(); + } + } + + /// 撤销 + void undo() { +// print(_undoList); + if (_undoList.isNotEmpty) { + _redoList.add(_undoList.last); + _undoList.removeLast(); + if (_undoList.isNotEmpty) { + _textEditingController.value = TextEditingValue( + text: _undoList.last.text, + selection: TextSelection( + extentOffset: _undoList.last.position, + baseOffset: _undoList.last.position), + ); + } else { + _textEditingController.value = TextEditingValue( + text: initText, + selection: TextSelection( + extentOffset: initText.length, baseOffset: initText.length), + ); + } + } + } + + /// 恢复 + void redo() { +// print(_redoList); + if (_redoList.isNotEmpty) { + _textEditingController.value = TextEditingValue( + text: _redoList.last.text, + selection: TextSelection( + extentOffset: _redoList.last.position, + baseOffset: _redoList.last.position), + ); + _undoList.add(_redoList.last); + _redoList.removeLast(); + } + } +} + +class _EditData { + final String text; + final int position; + + _EditData(this.text, this.position); + + @override + String toString() { + return 'text:$text position:$position'; + } +} diff --git a/app/3rd_party/markdown_editor_ot/lib/src/editor.dart b/app/3rd_party/markdown_editor_ot/lib/src/editor.dart new file mode 100644 index 0000000..e2a5df5 --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/lib/src/editor.dart @@ -0,0 +1,352 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:markdown_editor_ot/src/action.dart'; +import 'package:markdown_editor_ot/customize_physics.dart'; +import 'package:markdown_editor_ot/src/edit_perform.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +typedef void OnComplete(String content); + +class MdEditor extends StatefulWidget { + MdEditor({ + Key? key, + this.textStyle, + this.padding = const EdgeInsets.all(0.0), + this.initText, + this.hintText, + this.hintTextStyle, + this.imageSelect, + this.textChange, + this.actionIconColor, + this.cursorColor, + this.appendBottomWidget, + this.splitWidget, + this.textFocusNode, + required this.onComplete, + }) : super(key: key); + + final TextStyle? textStyle; + final TextStyle? hintTextStyle; + final EdgeInsetsGeometry padding; + final String? initText; + final String? hintText; + + /// see [ImageSelectCallback] + final ImageSelectCallback? imageSelect; + + final VoidCallback? textChange; + + /// Change icon color, eg: color of font_bold icon. + final Color? actionIconColor; + + final Color? cursorColor; + + final Widget? appendBottomWidget; + + final Widget? splitWidget; + + final FocusNode? textFocusNode; + + final OnComplete onComplete; + + @override + State createState() => MdEditorState(); +} + +class MdEditorState extends State with AutomaticKeepAliveClientMixin { + final _textEditingController = TextEditingController(text: ''); + var _editPerform; + SharedPreferences? _pres; + + String getText() { + return _textEditingController.value.text; + } + + // 将文本框光标移动至末尾 + void moveTextCursorToEnd() { + final str = _textEditingController.text; + _textEditingController.value = TextEditingValue(text: str, selection: TextSelection.collapsed(offset: str.length)); + } + + @override + void initState() { + super.initState(); + _textEditingController.text = widget.initText ?? ''; + + _editPerform = EditPerform( + _textEditingController, + initText: _textEditingController.text, + ); + } + + void _disposeText( + ActionType type, + String text, + int index, [ + int? cursorPosition, + ]) { + final _tempKey = 'markdown_editor_${type.toString()}'; + _pres?.setInt(_tempKey, (_pres?.getInt(_tempKey) ?? 0) + 1); + debugPrint('$_tempKey ${_pres?.getInt(_tempKey)}'); + + var position = cursorPosition ?? _textEditingController.selection.base.offset; + + if (position < 0) { + print('WARN: The insert position value is $position'); + return; + } + + var startText = _textEditingController.text.substring(0, position); + var endText = _textEditingController.text.substring(position); + + var str = startText + text + endText; + _textEditingController.value = + TextEditingValue(text: str, selection: TextSelection.collapsed(offset: startText.length + text.length - index)); + + if (widget.textChange != null) widget.textChange!(); + + _editPerform.change(_textEditingController.text); + } + + /// 获取光标位置 + int _getCursorPosition() { + if (_textEditingController.text.isEmpty) return 0; + if (_textEditingController.selection.base.offset < 0) return _textEditingController.text.length; + return _textEditingController.selection.base.offset; + } + + Future _initSharedPreferences() async { + _pres = await SharedPreferences.getInstance(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Stack( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 40.0), + child: SingleChildScrollView( + child: Padding( + padding: widget.padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + maxLines: null, + minLines: 7, + textAlignVertical: TextAlignVertical.top, + cursorColor: widget.cursorColor, + cursorWidth: 1.5, + controller: _textEditingController, + focusNode: widget.textFocusNode, + autofocus: false, + scrollPhysics: const CustomizePhysics(), + style: widget.textStyle ?? + TextStyle( + fontSize: 17, + height: kIsWeb ? null : 1.3, + ), + onChanged: (text) { + _editPerform.change(text); + if (widget.textChange != null) widget.textChange!(); + }, + decoration: InputDecoration( + hintText: widget.hintText ?? '请输入内容', + border: InputBorder.none, + hintStyle: widget.hintTextStyle, + ), + ), + widget.appendBottomWidget ?? const SizedBox(), + ], + ), + ), + ), + ), + Align( + alignment: Alignment.bottomLeft, + child: Container( + height: 40.0, + width: MediaQuery.of(context).size.width, + child: Ink( + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark ? Colors.black87 : const Color(0xFFF0F0F0), + boxShadow: [ + BoxShadow( + color: + Theme.of(context).brightness == Brightness.dark ? Colors.black87 : const Color(0xAAF0F0F0)), + ], + ), + child: FutureBuilder( + future: _pres == null ? _initSharedPreferences() : null, + builder: (con, snap) { + final widgets = []; + + widgets.add(ActionImage( + type: ActionType.done, + color: widget.actionIconColor, + tap: (t, s, i, [p]) { + widget.onComplete(getText()); + }, + )); + + widgets.add(ActionImage( + type: ActionType.undo, + color: widget.actionIconColor, + tap: (t, s, i, [p]) { + _editPerform.undo(); + }, + )); + widgets.add(ActionImage( + type: ActionType.redo, + color: widget.actionIconColor, + tap: (t, s, i, [p]) { + _editPerform.redo(); + }, + )); + + // sort + if (snap.connectionState == ConnectionState.done || snap.connectionState == ConnectionState.none) + widgets.addAll(_getSortActionWidgets().map((sort) => sort.widget)); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: widgets, + ), + ); + }, + ), + ), + ), + ), + ], + ); + } + + /// Sort action buttons by used count. + List<_SortActionWidget> _getSortActionWidgets() { + final sortWidget = <_SortActionWidget>[]; + final key = 'markdown_editor'; + final getSortValue = (ActionType type) { + return int.parse((_pres?.get('${key}_${type.toString()}') ?? '0').toString()); + }; + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.image), + widget: ActionImage( + type: ActionType.image, + color: widget.actionIconColor, + tap: _disposeText, + imageSelect: widget.imageSelect, + getCursorPosition: _getCursorPosition, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.link), + widget: ActionImage( + type: ActionType.link, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.fontBold), + widget: ActionImage( + type: ActionType.fontBold, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.fontItalic), + widget: ActionImage( + type: ActionType.fontItalic, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.fontStrikethrough), + widget: ActionImage( + type: ActionType.fontStrikethrough, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.textQuote), + widget: ActionImage( + type: ActionType.textQuote, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.list), + widget: ActionImage( + type: ActionType.list, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.h4), + widget: ActionImage( + type: ActionType.h4, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.h5), + widget: ActionImage( + type: ActionType.h5, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.h1), + widget: ActionImage( + type: ActionType.h1, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.h2), + widget: ActionImage( + type: ActionType.h2, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + sortWidget.add(_SortActionWidget( + sortValue: getSortValue(ActionType.h3), + widget: ActionImage( + type: ActionType.h3, + color: widget.actionIconColor, + tap: _disposeText, + ), + )); + + sortWidget.sort((a, b) => (b.sortValue).compareTo(a.sortValue)); + + return sortWidget; + } + + @override + bool get wantKeepAlive => true; +} + +class _SortActionWidget { + final ActionImage widget; + final int sortValue; + + _SortActionWidget({ + required this.widget, + required this.sortValue, + }); +} diff --git a/app/3rd_party/markdown_editor_ot/lib/src/preview.dart b/app/3rd_party/markdown_editor_ot/lib/src/preview.dart new file mode 100644 index 0000000..b14babf --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/lib/src/preview.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:markdown_core/builder.dart'; +import 'package:markdown_core/markdown.dart'; + +class MdPreview extends StatefulWidget { + MdPreview({ + Key? key, + required this.text, + this.padding = const EdgeInsets.all(0.0), + this.onTapLink, + required this.widgetImage, + this.textStyle, + }) : super(key: key); + + final String text; + final EdgeInsetsGeometry padding; + final WidgetImage widgetImage; + final TextStyle? textStyle; + + /// Call this method when it tap link of markdown. + /// If [onTapLink] is null,it will open the link with your default browser. + final TapLinkCallback? onTapLink; + + @override + State createState() => MdPreviewState(); +} + +class MdPreviewState extends State + with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return SingleChildScrollView( + child: Padding( + padding: widget.padding, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Markdown( + data: widget.text, + maxWidth: constraints.maxWidth, + linkTap: (link) { + debugPrint(link); + if (widget.onTapLink != null) { + widget.onTapLink!(link); + } + }, + image: widget.widgetImage, + textStyle: widget.textStyle, + ); + }, + ), + ), + ); + } + + @override + bool get wantKeepAlive => true; +} + +typedef void TapLinkCallback(String link); diff --git a/app/3rd_party/markdown_editor_ot/pubspec.yaml b/app/3rd_party/markdown_editor_ot/pubspec.yaml new file mode 100644 index 0000000..00f847b --- /dev/null +++ b/app/3rd_party/markdown_editor_ot/pubspec.yaml @@ -0,0 +1,61 @@ +name: markdown_editor_ot +description: Simple and easy to implement your markdown editor, it uses its own parser. +version: 1.0.2 +homepage: https://github.com/xia-weiyang/markdown_editor_ot + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + flutter: + sdk: flutter + + shared_preferences: ^2.0.4 + markdown_core: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.io/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.io/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + + fonts: + - family: MyIconFont + fonts: + - asset: packages/markdown_editor_ot/fonts/iconfont.ttf + + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.io/custom-fonts/#from-packages \ No newline at end of file diff --git a/app/lib/main.dart b/app/lib/main.dart index eeca30d..74cf9c3 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -39,6 +39,7 @@ class MyApp extends StatelessWidget { builder: (context, child) { var isDarkMode = context.watch().isDarkMode; var brightnessMode = context.watch().brightnessMode; + var activeColor = context.watch().currentActiveColor; H().sp.updateInt(SPKeys.brightnessMode, brightnessMode?.index ?? 0); late bool showDarkMode; if (brightnessMode == BrightnessMode.system) { @@ -48,7 +49,9 @@ class MyApp extends StatelessWidget { } return MaterialApp( title: CodegenLoader.mapLocales[context.locale.toString()]?[LocaleKeys.app_name], - theme: showDarkMode ? darkTheme : lightTheme, + theme: (showDarkMode ? darkTheme : lightTheme).copyWith( + highlightColor: activeColor, + ), localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index deff43b..b29d6e7 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -12,6 +12,7 @@ import 'package:dde_gesture_manager/utils/keyboard_mapper.dart'; import 'package:dde_gesture_manager/utils/notificator.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:dde_gesture_manager/widgets/dde_data_table.dart'; +import 'package:dde_gesture_manager/widgets/dde_markdown_field.dart'; import 'package:dde_gesture_manager/widgets/dde_text_field.dart'; import 'package:dde_gesture_manager/widgets/table_cell_shortcut_listener.dart'; import 'package:dde_gesture_manager/widgets/table_cell_text_field.dart'; @@ -263,7 +264,7 @@ class GestureEditor extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.all(8.0), - child: Text('name:'), + child: Text(LocaleKeys.gesture_editor_info_name).tr(), ), Expanded( child: DTextField( @@ -273,24 +274,57 @@ class GestureEditor extends StatelessWidget { val = val.trim(); schemeProvider.setProps(name: val); var localSchemesProvider = context.read(); - if (!localSchemesProvider.schemes!.every((element) => element.scheme.name != val)) { + if (!localSchemesProvider.schemes! + .where((element) => element.scheme.id != schemeProvider.id) + .every((element) => element.scheme.name != val)) { Notificator.error( context, title: LocaleKeys.info_scheme_name_conflict_title.tr(), description: LocaleKeys.info_scheme_name_conflict_description.tr(), ); - return; + return false; } - ; var localSchemeEntry = localSchemesProvider.schemes! .firstWhere((ele) => ele.scheme.id == schemeProvider.id); localSchemeEntry.scheme.name = val; localSchemeEntry.save(localSchemesProvider); + return true; }, ), ), ], ), + Divider(), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Padding( + padding: const EdgeInsets.only(top: 5), + child: Text(LocaleKeys.gesture_editor_info_description).tr(), + ), + ), + Expanded( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: kMinInteractiveDimension, maxHeight: 300), + child: DMarkdownField( + initText: schemeProvider.description, + readOnly: schemeProvider.readOnly, + onComplete: (content) { + content = content.trim(); + schemeProvider.setProps(description: content); + var localSchemesProvider = context.read(); + var localSchemeEntry = localSchemesProvider.schemes! + .firstWhere((ele) => ele.scheme.id == schemeProvider.id); + localSchemeEntry.scheme.description = content; + localSchemeEntry.save(localSchemesProvider); + }, + ), + ), + ), + ], + ), ], ), ), diff --git a/app/lib/widgets/dde_markdown_field.dart b/app/lib/widgets/dde_markdown_field.dart new file mode 100644 index 0000000..9e07d53 --- /dev/null +++ b/app/lib/widgets/dde_markdown_field.dart @@ -0,0 +1,112 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/models/settings.provider.dart'; +import 'package:flutter/material.dart'; +import 'package:markdown_editor_ot/markdown_editor.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class DMarkdownField extends StatefulWidget { + const DMarkdownField({ + Key? key, + required this.initText, + required this.onComplete, + required this.readOnly, + }) : super(key: key); + + final bool readOnly; + final String? initText; + final OnComplete onComplete; + + @override + _DMarkdownFieldState createState() => _DMarkdownFieldState(); +} + +class _DMarkdownFieldState extends State { + String? _previewText; + + bool get isPreview => _previewText != null || widget.readOnly; + + final FocusNode _focusNode = FocusNode(); + + @override + void initState() { + _previewText = widget.initText; + super.initState(); + } + + _launchURL(String url) async { + if (await canLaunch(url)) { + await launch(url); + } else { + throw 'Could not launch $url'; + } + } + + @override + void didUpdateWidget(covariant DMarkdownField oldWidget) { + if (oldWidget.initText != widget.initText) { + setState(() { + _previewText = widget.initText; + }); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return Focus( + child: Builder(builder: (context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(defaultBorderRadius), + color: Colors.grey.withOpacity(.3), + border: Border.all( + width: 2, + color: Focus.of(context).hasFocus && !widget.readOnly + ? context.watch().activeColor ?? Color(0xff565656) + : Color(0xff565656)), + ), + child: isPreview + ? GestureDetector( + onTap: widget.readOnly + ? null + : () { + setState(() { + _previewText = null; + }); + }, + child: MouseRegion( + cursor: widget.readOnly ? SystemMouseCursors.basic : SystemMouseCursors.click, + child: MdPreview( + text: _previewText ?? '', + padding: EdgeInsets.only(left: 15), + onTapLink: _launchURL, + widgetImage: (imageUrl) => CachedNetworkImage( + imageUrl: imageUrl, + placeholder: (context, url) => const SizedBox( + width: double.infinity, + height: 300, + child: Center(child: CircularProgressIndicator()), + ), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + ), + ), + ) + : MdEditor( + initText: widget.initText, + textFocusNode: _focusNode, + padding: EdgeInsets.symmetric(horizontal: 5), + onComplete: (content) { + setState(() { + _previewText = content; + }); + widget.onComplete(content); + }, + ), + ); + }), + ); + } +} diff --git a/app/lib/widgets/dde_text_field.dart b/app/lib/widgets/dde_text_field.dart index 17fd26d..aec1aa3 100644 --- a/app/lib/widgets/dde_text_field.dart +++ b/app/lib/widgets/dde_text_field.dart @@ -8,7 +8,7 @@ class DTextField extends StatefulWidget { final String? initText; final String? hint; final bool readOnly; - final Function(String value) onComplete; + final bool Function(String value) onComplete; const DTextField({ Key? key, @@ -40,8 +40,11 @@ class _DTextFieldState extends State { } _handleFocusChange() { - if (!_focusNode.hasFocus) { - widget.onComplete(_controller.text); + if (!_focusNode.hasFocus && !widget.readOnly) { + var ok = widget.onComplete(_controller.text); + if (!ok) { + _focusNode.requestFocus(); + } } } @@ -74,6 +77,7 @@ class _DTextFieldState extends State { padding: const EdgeInsets.only(left: 15), child: TextField( readOnly: widget.readOnly, + style: widget.readOnly ? TextStyle(color: Colors.grey) : null, focusNode: _focusNode, cursorColor: context.watch().activeColor, decoration: InputDecoration.collapsed(hintText: widget.hint), diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc index da1524f..0d852ba 100644 --- a/app/linux/flutter/generated_plugin_registrant.cc +++ b/app/linux/flutter/generated_plugin_registrant.cc @@ -7,12 +7,16 @@ #include "generated_plugin_registrant.h" #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_platform_alert_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin"); flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/app/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake index 5a9d113..9945fd3 100644 --- a/app/linux/flutter/generated_plugins.cmake +++ b/app/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_platform_alert + url_launcher_linux window_manager ) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 42a6021..979df7b 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -38,10 +38,14 @@ dependencies: uuid: ^3.0.5 adaptive_scrollbar: ^2.1.0 flutter_platform_alert: ^0.2.1 + markdown_editor_ot: + path: 3rd_party/markdown_editor_ot cherry_toast: path: 3rd_party/cherry_toast xdg_directories_web: path: 3rd_party/xdg_directories_web + cached_network_image: ^3.2.0 + url_launcher: ^6.0.17 dev_dependencies: flutter_test: diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index f668426..276b910 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -55,6 +55,10 @@ "built_in": "built-in", "commandline": "commandline", "shortcut": "shortcut" + }, + "info": { + "name": "Scheme Name", + "description": "Description" } }, "operation": { diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index c623dcf..53cdbdb 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -55,6 +55,10 @@ "built_in": "内置操作", "commandline": "命令行", "shortcut": "快捷键" + }, + "info": { + "name": "方案名称", + "description": "方案描述" } }, "operation": { -- 2.11.0 From 09a92757056e715c8485be67d84593d8baa9b29a Mon Sep 17 00:00:00 2001 From: debuggerx Date: Thu, 16 Dec 2021 16:06:00 +0800 Subject: [PATCH 10/14] wip: add md editor for scheme description. --- app/lib/widgets/dde_markdown_field.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/lib/widgets/dde_markdown_field.dart b/app/lib/widgets/dde_markdown_field.dart index 9e07d53..46f6c7a 100644 --- a/app/lib/widgets/dde_markdown_field.dart +++ b/app/lib/widgets/dde_markdown_field.dart @@ -99,10 +99,12 @@ class _DMarkdownFieldState extends State { textFocusNode: _focusNode, padding: EdgeInsets.symmetric(horizontal: 5), onComplete: (content) { - setState(() { - _previewText = content; - }); - widget.onComplete(content); + if (content == widget.initText) + setState(() { + _previewText = content; + }); + else + widget.onComplete(content); }, ), ); -- 2.11.0 From 8face6f72446734f703e0e3818e34a56d1555a5c Mon Sep 17 00:00:00 2001 From: debuggerx Date: Thu, 16 Dec 2021 18:55:27 +0800 Subject: [PATCH 11/14] feat: editor almost done. --- app/lib/models/local_schemes_linux.dart | 11 +++++----- app/lib/models/scheme.dart | 2 +- app/lib/pages/gesture_editor.dart | 4 ++-- app/lib/pages/local_manager.dart | 38 +++++++++++++++++++++++++++------ app/lib/widgets/dde_text_field.dart | 5 ++++- app/pubspec.yaml | 4 ++-- app/resources/langs/en.json | 7 +++++- app/resources/langs/zh-CN.json | 7 +++++- 8 files changed, 59 insertions(+), 19 deletions(-) diff --git a/app/lib/models/local_schemes_linux.dart b/app/lib/models/local_schemes_linux.dart index aefe39b..9c36fb3 100644 --- a/app/lib/models/local_schemes_linux.dart +++ b/app/lib/models/local_schemes_linux.dart @@ -49,15 +49,16 @@ class LocalSchemes implements LocalSchemesInterface { Future create() async { var _supportDirectory = await getApplicationSupportDirectory(); return LocalSchemeEntryLinux( - path: join(_supportDirectory.path, 'schemes', '${Uuid().v1()}.json'), - scheme: Scheme.create(), - lastModifyTime: DateTime.now(), - ); + path: join(_supportDirectory.path, 'schemes', '${Uuid().v1()}.json'), + scheme: Scheme.create(), + lastModifyTime: DateTime.now(), + ); } @override void remove(String path) { - File(path).delete(); + var file = File(path); + if (file.existsSync()) file.delete(); } } diff --git a/app/lib/models/scheme.dart b/app/lib/models/scheme.dart index 292b1bb..5ffea37 100644 --- a/app/lib/models/scheme.dart +++ b/app/lib/models/scheme.dart @@ -175,7 +175,7 @@ class Scheme { 'uploaded': uploaded, 'name': name, 'desc': description, - 'gestures': gestures, + 'gestures': gestures?.map((e) => e.toJson()).toList(), }; } diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index b29d6e7..aeb17c9 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -508,7 +508,7 @@ List _buildRowCellsEditing(BuildContext context) { _buildCommandCellsEditing(context), TableCellTextField( initText: gesture.remark, - hint: 'pls input cmd', + hint: LocaleKeys.gesture_editor_hints_remark.tr(), onComplete: (value) => context.read().setProps( remark: value, editMode: true, @@ -523,7 +523,7 @@ Widget _buildCommandCellsEditing(BuildContext context) { case GestureType.commandline: return TableCellTextField( initText: gesture.command, - hint: 'pls input cmd', + hint: LocaleKeys.gesture_editor_hints_command.tr(), onComplete: (value) => context.read().setProps( command: value, editMode: true, diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index cba1f3c..72d2252 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -26,7 +26,7 @@ class LocalManager extends StatefulWidget { class _LocalManagerState extends State { late ScrollController _scrollController; - String? _hoveringItem; + String? _hoveringItemPath; late String _selectedItemPath; @override @@ -51,7 +51,7 @@ class _LocalManagerState extends State { Color _getItemBackgroundColor(int index, String itemPath) { Color _color = index % 2 == 0 ? context.t.scaffoldBackgroundColor : context.t.backgroundColor; - if (itemPath == _hoveringItem) _color = context.t.dialogBackgroundColor; + if (itemPath == _hoveringItemPath) _color = context.t.dialogBackgroundColor; if (itemPath == _selectedItemPath) _color = context.read().currentActiveColor; return _color; } @@ -144,7 +144,7 @@ class _LocalManagerState extends State { cursor: SystemMouseCursors.click, onEnter: (_) { setState(() { - _hoveringItem = localSchemes[index].path; + _hoveringItemPath = localSchemes[index].path; }); }, child: Container( @@ -196,8 +196,13 @@ class _LocalManagerState extends State { onTap: () async { var localSchemesProvider = context.read(); var newSchemes = [...?localSchemesProvider.schemes]; - newSchemes.add(await localSchemesProvider.create()); + var newEntry = await localSchemesProvider.create(); + newSchemes.add(newEntry); localSchemesProvider.setProps(schemes: newSchemes..sort()); + setState(() { + _selectedItemPath = newEntry.path; + }); + _handleItemClick(context, newEntry); }, ), DButton.delete( @@ -209,12 +214,33 @@ class _LocalManagerState extends State { newSchemes.removeAt(index); localSchemesProvider.setProps(schemes: newSchemes); localSchemesProvider.remove(_selectedItemPath); + var newSelectedItem = newSchemes[(index - 1).clamp(1, newSchemes.length)]; + setState(() { + _selectedItemPath = newSelectedItem.path; + }); + _handleItemClick(context, newSelectedItem); + }, + ), + DButton.duplicate( + enabled: _selectedItemPath.notNull, + onTap: () async { + var localSchemesProvider = context.read(); + var newSchemes = [...?localSchemesProvider.schemes]; + var index = newSchemes.indexWhere((element) => element.path == _selectedItemPath); + var newEntry = await localSchemesProvider.create(); + newEntry.scheme = Scheme.parse(newSchemes[index].scheme.toJson()); + newEntry.scheme.id = Uuid().v1(); + newEntry.scheme.name = '${newEntry.scheme.name} (${LocaleKeys.str_copy.tr()})'; + newEntry.scheme.fromMarket = false; + newEntry.scheme.uploaded = false; + newSchemes.add(newEntry); + localSchemesProvider.setProps(schemes: newSchemes..sort()); setState(() { - _selectedItemPath = newSchemes[(index - 1).clamp(1, newSchemes.length)].path; + _selectedItemPath = newEntry.path; }); + _handleItemClick(context, newEntry); }, ), - DButton.duplicate(enabled: _selectedItemPath.notNull), DButton.apply( enabled: true, onTap: () { diff --git a/app/lib/widgets/dde_text_field.dart b/app/lib/widgets/dde_text_field.dart index aec1aa3..e775713 100644 --- a/app/lib/widgets/dde_text_field.dart +++ b/app/lib/widgets/dde_text_field.dart @@ -2,7 +2,6 @@ import 'package:dde_gesture_manager/constants/constants.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class DTextField extends StatefulWidget { final String? initText; @@ -52,6 +51,10 @@ class _DTextFieldState extends State { void didUpdateWidget(covariant DTextField oldWidget) { if (oldWidget.initText != widget.initText) { _controller.text = widget.initText ?? ''; + if (widget.initText == LocaleKeys.str_new_scheme.tr() || + (widget.initText ?? '').contains('(${LocaleKeys.str_copy.tr()})')) { + _focusNode.requestFocus(); + } } super.didUpdateWidget(oldWidget); } diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 979df7b..e64e791 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -38,14 +38,14 @@ dependencies: uuid: ^3.0.5 adaptive_scrollbar: ^2.1.0 flutter_platform_alert: ^0.2.1 + cached_network_image: ^3.2.0 + url_launcher: ^6.0.17 markdown_editor_ot: path: 3rd_party/markdown_editor_ot cherry_toast: path: 3rd_party/cherry_toast xdg_directories_web: path: 3rd_party/xdg_directories_web - cached_network_image: ^3.2.0 - url_launcher: ^6.0.17 dev_dependencies: flutter_test: diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index 276b910..105a844 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -59,6 +59,10 @@ "info": { "name": "Scheme Name", "description": "Description" + }, + "hints": { + "command": "Please enter the command you want to execute", + "remark": "Please enter a remark information" } }, "operation": { @@ -70,7 +74,8 @@ }, "str": { "null": "Null", - "new_scheme": "New gesture scheme" + "new_scheme": "New gesture scheme", + "copy": "copy" }, "built_in_commands": { "ShowWorkspace": "ShowWorkspace", diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index 53cdbdb..f4be855 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -59,6 +59,10 @@ "info": { "name": "方案名称", "description": "方案描述" + }, + "hints": { + "command": "请输入要执行的命令", + "remark": "请输入备注信息" } }, "operation": { @@ -70,7 +74,8 @@ }, "str": { "null": "无", - "new_scheme": "新建手势方案" + "new_scheme": "新建手势方案", + "copy": "副本" }, "built_in_commands": { "ShowWorkspace": "显示工作区", -- 2.11.0 From ed863ecb4150ac1ab164089a4d1c306c9c934b18 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Fri, 17 Dec 2021 18:24:32 +0800 Subject: [PATCH 12/14] feat: change api framework to angel3. --- api/.dockerignore | 10 + api/.gitignore | 99 +++++- api/AUTHORS.md | 12 + api/CHANGELOG.md | 9 + api/CONTRIBUTING.md | 24 ++ api/Dockerfile | 14 + api/LICENSE | 29 ++ api/README.md | 71 ++-- api/analysis_options.yaml | 98 +----- api/bin/dev.dart | 28 ++ api/bin/main.dart | 13 - api/bin/migrate.dart | 28 ++ api/bin/prod.dart | 27 ++ api/config.src.yaml | 6 - api/config/default.yaml | 12 + api/lib/channel.dart | 77 ----- api/lib/dde_gesture_manager.dart | 11 - api/lib/dde_gesture_manager_api.dart | 17 + api/lib/model/model.dart | 18 - api/lib/models.dart | 1 + api/lib/src/config/config.dart | 30 ++ api/lib/src/config/plugins/orm.dart | 35 ++ api/lib/src/config/plugins/plugins.dart | 8 + api/lib/src/models/base_model.dart | 8 + api/lib/src/models/user.dart | 19 ++ api/lib/src/models/user.g.dart | 369 +++++++++++++++++++++ .../routes/controllers/controller_extensions.dart | 21 ++ .../src/routes/controllers/user_controllers.dart | 16 + api/lib/src/routes/routes.dart | 39 +++ api/pubspec.yaml | 41 ++- api/test/all_test.dart | 43 +++ api/test/harness/app.dart | 38 --- api/test/simple_controller_test.dart | 32 -- api/views/error.html | 5 + api/views/layout.html | 10 + api/web/css/site.css | 27 ++ api/web/images/favicon.png | Bin 0 -> 10624 bytes api/web/login.html | 29 -- api/web/robots.txt | 2 + 39 files changed, 1008 insertions(+), 368 deletions(-) create mode 100644 api/.dockerignore create mode 100644 api/AUTHORS.md create mode 100644 api/CHANGELOG.md create mode 100644 api/CONTRIBUTING.md create mode 100644 api/Dockerfile create mode 100644 api/LICENSE create mode 100644 api/bin/dev.dart delete mode 100644 api/bin/main.dart create mode 100644 api/bin/migrate.dart create mode 100644 api/bin/prod.dart delete mode 100644 api/config.src.yaml create mode 100644 api/config/default.yaml delete mode 100644 api/lib/channel.dart delete mode 100644 api/lib/dde_gesture_manager.dart create mode 100644 api/lib/dde_gesture_manager_api.dart delete mode 100644 api/lib/model/model.dart create mode 100644 api/lib/models.dart create mode 100644 api/lib/src/config/config.dart create mode 100644 api/lib/src/config/plugins/orm.dart create mode 100644 api/lib/src/config/plugins/plugins.dart create mode 100644 api/lib/src/models/base_model.dart create mode 100644 api/lib/src/models/user.dart create mode 100644 api/lib/src/models/user.g.dart create mode 100644 api/lib/src/routes/controllers/controller_extensions.dart create mode 100644 api/lib/src/routes/controllers/user_controllers.dart create mode 100644 api/lib/src/routes/routes.dart create mode 100644 api/test/all_test.dart delete mode 100644 api/test/harness/app.dart delete mode 100644 api/test/simple_controller_test.dart create mode 100644 api/views/error.html create mode 100644 api/views/layout.html create mode 100644 api/web/css/site.css create mode 100644 api/web/images/favicon.png delete mode 100644 api/web/login.html create mode 100644 api/web/robots.txt diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..faf23bd --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,10 @@ +.dart_tool +.idea +.pub +.vscode +logs/ +test/ +build/ +.analysis-options +.packages +*.g.dart \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index 57ad605..a0541d9 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1,28 +1,95 @@ # Created by .ignore support plugin (hsz.mobi) ### Dart template -# See https://www.dartlang.org/guides/libraries/private-files +# See https://www.dartlang.org/tools/private-files.html + +# source_gen +.dart_tool # Files and directories created by pub -.dart_tool/ +.buildlog .packages +.project +.pub/ +.scripts-bin/ build/ -# If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock - -# Directory created by dartdoc -# If you don't generate documentation locally you can remove this line. -doc/api/ +**/packages/ -# Avoid committing generated Javascript files: +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) *.dart.js -*.info.json # Produced by the --dump-info flag. -*.js # When generated by dart2js. Don't specify *.js if your - # project includes source files written in JavaScript. -*.js_ +*.part.js *.js.deps *.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### VSCode template +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +logs/ +*.pem +.DS_Store +server_log.txt -.flutter-plugins -.flutter-plugins-dependencies +.metals/ -config.yaml \ No newline at end of file +/config/production.yaml +/config/development.yaml diff --git a/api/AUTHORS.md b/api/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/api/AUTHORS.md @@ -0,0 +1,12 @@ +Primary Authors +=============== + +* __[Thomas Hii](dukefirehawk.apps@gmail.com)__ + + Thomas is the current maintainer of the code base. He has refactored and migrated the + code base to support NNBD. + +* __[Tobe O](thosakwe@gmail.com)__ + + Tobe has written much of the original code prior to NNBD migration. He has moved on and + is no longer involved with the project. diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md new file mode 100644 index 0000000..6c9985b --- /dev/null +++ b/api/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +## 1.0.0 + +* Changed to use `angel3` packages +* Updated to support NNBD +* Updated README +* Updated default `postgresql` setup +* Updated linter to `package:lints` diff --git a/api/CONTRIBUTING.md b/api/CONTRIBUTING.md new file mode 100644 index 0000000..b34fbca --- /dev/null +++ b/api/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contribution + +Any help from the open-source community is always welcome and needed: + +1. Found an issue? + - Please [fill a bug report][tracker] with error message and steps to reproduce it. +2. Wish a feature? + - Open a feature request with use cases. +3. Are you using and liking the project? + - Create an article about your use case + - Do a post on your likes and dislikes + - Make a donation. +4. Are you a developer? + - Fix a bug and send a [pull request][pull_request] + - Implement a new feature + - Improve the Unit Tests + - Improve the [User Guide][doc] and send a [document pull request][doc_repo] +5. Have you already helped in any way? + - **Many thanks to the contributors and everybody that uses this project!** + +[tracker]: https://github.com/dukefirehawk/angel/issues +[pull_request]: https://github.com/dukefirehawk/angel/pulls +[doc]: https://angel3-docs.dukefirehawk.com +[doc_repo]: https://github.com/dukefirehawk/angel3-guide/pulls \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..c84bc87 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,14 @@ +FROM google/dart:latest + +COPY ./ ./ + +# Install dependencies, pre-build +RUN pub get + +# Optionally build generaed sources. +# RUN pub run build_runner build + +# Set environment, start server +ENV ANGEL_ENV=production +EXPOSE 3000 +CMD dart bin/prod.dart diff --git a/api/LICENSE b/api/LICENSE new file mode 100644 index 0000000..df5e063 --- /dev/null +++ b/api/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, dukefirehawk.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/api/README.md b/api/README.md index 7849780..2088a61 100644 --- a/api/README.md +++ b/api/README.md @@ -1,37 +1,62 @@ -# dde_gesture_manager +# ORM Starter Application for Angel3 framework -## Running the Application Locally +This is an ORM starter application for [Angel3 framework](https://angel3-framework.web.app) which is a full-stack Web framework in Dart. The default database is `postgresql`. `mysql` support is still in active development. -Run `conduit serve` from this directory to run the application. For running within an IDE, run `bin/main.dart`. By default, a configuration file named `config.yaml` will be used. +## Installation & Setup -You must have a `config.yaml` file that has correct database connection info, which should point to a local database. To configure a database to match your application's schema, run the following commands: +1. Download and install [Dart](https://dart.dev/get-dart). +2. Install `postgresql` version 9, 10, 11 or 12. **postgresql 13 is not working as the driver do not support SCRAM** +3. Create a new user and database in postgres using `psql` cli. For example: -``` -# if this is a project, run db generate first -conduit db generate -conduit db upgrade --connect postgres://user:password@localhost:5432/app_name -``` + ```sql + postgres=# create database appdb; + postgres=# create user appuser with encrypted password 'App1970#'; + postgres=# grant all privileges on database appdb to appuser; + ``` -To generate a SwaggerUI client, run `conduit document client`. +4. Update the `postgres` section in the `config/default.yaml` file with the newly created user and database name. -## Running Application Tests + ```yaml + postgres: + host: localhost + port: 5432 + database_name: appdb + username: appuser + password: App1970# + useSSL: false + time_zone: UTC + ``` -Tests are run with a local PostgreSQL database named `conduit_test_db`. If this database does not exist, create it from your SQL prompt: +5. Run the migration to generate `migrations` and `greetings` tables in the database. -CREATE DATABASE conduit_test_db; -CREATE USER conduit_test_user WITH createdb; -ALTER USER conduit_test_user WITH password 'conduit!'; -GRANT all ON DATABASE conduit_test_db TO conduit_test_user; + ```bash + dart bin/migration.dart + ``` +### Development -To run all tests for this application, run the following in this directory: +1. Run the following command to start Angel3 server in dev mode to *hot-reloaded* on file changes: -``` -pub run test -``` + ```bash + dart --observe bin/dev.dart + ``` -The default configuration file used when testing is `config.src.yaml`. This file should be checked into version control. It also the template for configuration files used in deployment. +2. Modify the code and watch the changes applied to the application -## Deploying an Application +### Production -See the documentation for [Deployment](https://conduit.io/docs/deploy/). \ No newline at end of file +1. Run the following command: + + ```bash + dart bin/prod.dart + ``` + +2. Run as docker. Edit and run the provided `Dockerfile` to build the image. + +## Resources + +Visit the [Developer Guide](https://angel3-docs.dukefirehawk.com/guides) for dozens of guides and resources, including video tutorials, to get up and running as quickly as possible with Angel3. + +Examples and complete projects can be found [here](https://angel3-framework.web.app/#/examples). + +You can also view the [API Documentation](https://pub.dev/documentation/angel3_framework/latest/). diff --git a/api/analysis_options.yaml b/api/analysis_options.yaml index d474280..d66fc45 100644 --- a/api/analysis_options.yaml +++ b/api/analysis_options.yaml @@ -1,98 +1,6 @@ -analyzer: - strong-mode: - implicit-casts: false +include: package:lints/recommended.yaml linter: rules: - - always_declare_return_types - - always_put_control_body_on_new_line - - always_put_required_named_parameters_first - - always_require_non_null_named_parameters - - annotate_overrides - - avoid_bool_literals_in_conditional_expressions - - avoid_double_and_int_checks - - avoid_empty_else - - avoid_field_initializers_in_const_classes - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_positional_boolean_parameters - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_single_cascade_in_expression_statements - - avoid_slow_async_io - - avoid_types_as_parameter_names - - avoid_unused_constructor_parameters - - await_only_futures - - camel_case_types - - cancel_subscriptions - - close_sinks - - comment_references - - constant_identifier_names - - control_flow_in_finally - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - hash_and_equals - - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type - - join_return_with_assignment - - library_names - - library_prefixes - - list_remove_unrelated_type - - literal_only_boolean_expressions - - no_duplicate_case_values - - non_constant_identifier_names - - null_closures - - package_api_docs - - package_names - - package_prefixed_library_names - - parameter_assignments - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - - prefer_constructors_over_static_methods - - prefer_contains - - prefer_equal_for_default_values - - prefer_final_fields - - prefer_final_locals - - prefer_foreach - - prefer_generic_function_type_aliases - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_iterable_whereType - - prefer_typing_uninitialized_variables - - recursive_getters - - slash_for_doc_comments - - sort_constructors_first - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - type_annotate_public_apis - - type_init_formals - - unawaited_futures - - unnecessary_const - - unnecessary_getters_setters - - unnecessary_lambdas - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_in_if_null_operators - - unnecessary_overrides - - unnecessary_parenthesis - - unnecessary_statements - - unnecessary_this - - unrelated_type_equality_checks - - use_rethrow_when_possible - - use_string_buffers - - use_to_and_as_if_applicable - - valid_regexps - - void_checks \ No newline at end of file + avoid_renaming_method_parameters: false + overridden_fields: false diff --git a/api/bin/dev.dart b/api/bin/dev.dart new file mode 100644 index 0000000..05028ae --- /dev/null +++ b/api/bin/dev.dart @@ -0,0 +1,28 @@ +import 'dart:io'; +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:belatuk_pretty_logging/belatuk_pretty_logging.dart'; +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_hot/angel3_hot.dart'; +import 'package:logging/logging.dart'; + +void main() async { + // Watch the config/ and web/ directories for changes, and hot-reload the server. + hierarchicalLoggingEnabled = true; + + var hot = HotReloader(() async { + var logger = Logger.detached('dde_gesture_manager_api') + ..level = Level.ALL + ..onRecord.listen(prettyLog); + var app = Angel(logger: logger, reflector: MirrorsReflector()); + await app.configure(configureServer); + return app; + }, [ + Directory('config'), + Directory('lib'), + ]); + + var server = await hot.startServer('127.0.0.1', 3000); + print( + 'dde_gesture_manager_api server listening at http://${server.address.address}:${server.port}'); +} diff --git a/api/bin/main.dart b/api/bin/main.dart deleted file mode 100644 index 37d4c6c..0000000 --- a/api/bin/main.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -Future main() async { - final app = Application() - ..options.configurationFilePath = "config.yaml" - ..options.port = 8888; - - await app.startOnCurrentIsolate(); - - print("Application started on port: ${app.options.port}."); - print("Click to open in browser: http://localhost:${app.options.port}"); - print("Use Ctrl-C (SIGINT) to stop running the application."); -} diff --git a/api/bin/migrate.dart b/api/bin/migrate.dart new file mode 100644 index 0000000..5c5e413 --- /dev/null +++ b/api/bin/migrate.dart @@ -0,0 +1,28 @@ +import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'package:angel3_configuration/angel3_configuration.dart'; +import 'package:angel3_migration_runner/angel3_migration_runner.dart'; +import 'package:angel3_migration_runner/postgres.dart'; +import 'package:file/local.dart'; +import 'package:logging/logging.dart'; + +void main(List args) async { + // Enable the logging + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((rec) { + print('${rec.time}: ${rec.level.name}: ${rec.loggerName}: ${rec.message}'); + + if (rec.error != null) { + print(rec.error); + print(rec.stackTrace); + } + }); + + var fs = LocalFileSystem(); + var configuration = await loadStandaloneConfiguration(fs); + var connection = await connectToPostgres(configuration); + var migrationRunner = PostgresMigrationRunner(connection, migrations: [ + UserMigration(), + ]); + await runMigrations(migrationRunner, args); +} diff --git a/api/bin/prod.dart b/api/bin/prod.dart new file mode 100644 index 0000000..3a4437b --- /dev/null +++ b/api/bin/prod.dart @@ -0,0 +1,27 @@ +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:angel3_production/angel3_production.dart'; + +// NOTE: By default, the Runner class does not use the `MirrorsReflector`, or any +// reflector, by default. +// +// If your application is using any sort of functionality reliant on annotations or reflection, +// either include the MirrorsReflector, or use a static reflector variant. +// +// The following use cases require reflection: +// * Use of Controllers, via @Expose() or @ExposeWS() +// * Use of dependency injection into constructors, whether in controllers or plain `container.make` calls +// * Use of the `ioc` function in any route +// +// The `MirrorsReflector` from `package:angel_container/mirrors.dart` is by far the most convenient pattern, +// so use it if possible. +// +// However, the following alternatives exist: +// * Generation via `package:angel_container_generator` +// * Creating an instance of `StaticReflector` +// * Manually implementing the `Reflector` interface (cumbersome; not recommended) +// +// As of January 4th, 2018, the documentation has not yet been updated to state this, +// so in the meantime, visit the Angel chat for further questions: +// +// https://gitter.im/angel_dart/discussion +void main(List args) => Runner('dde_gesture_manager_api', configureServer).run(args); diff --git a/api/config.src.yaml b/api/config.src.yaml deleted file mode 100644 index d3edf9a..0000000 --- a/api/config.src.yaml +++ /dev/null @@ -1,6 +0,0 @@ -database: - username: conduit_test_user - password: conduit! - host: localhost - port: 15432 # change this value - databaseName: conduit_test_db diff --git a/api/config/default.yaml b/api/config/default.yaml new file mode 100644 index 0000000..16dd769 --- /dev/null +++ b/api/config/default.yaml @@ -0,0 +1,12 @@ +# Default server configuration. +host: 127.0.0.1 +port: 3000 +postgres: + host: localhost + port: 5432 + database_name: appdb + username: appuser + password: App1970# + useSSL: false + time_zone: UTC +jwt_secret: "OvA9SBLnncot8gFHvt8Gh1qkQ1ptGIQW" \ No newline at end of file diff --git a/api/lib/channel.dart b/api/lib/channel.dart deleted file mode 100644 index 2eeca45..0000000 --- a/api/lib/channel.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; -import 'package:dde_gesture_manager/model/model.dart'; - -/// This type initializes an application. -/// -/// Override methods in this class to set up routes and initialize services like -/// database connections. See http://conduit.io/docs/http/channel/. -class DdeGestureManagerChannel extends ApplicationChannel { - late ManagedContext context; - - /// Initialize services in this method. - /// - /// Implement this method to initialize services, read values from [options] - /// and any other initialization required before constructing [entryPoint]. - /// - /// This method is invoked prior to [entryPoint] being accessed. - @override - Future prepare() async { - logger.onRecord.listen( - (rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}")); - - final config = - DdeGestureManagerConfiguration(options!.configurationFilePath!); - context = contextWithConnectionInfo(config.database!); - } - - /// Construct the request channel. - /// - /// Return an instance of some [Controller] that will be the initial receiver - /// of all [Request]s. - /// - /// This method is invoked after [prepare]. - @override - Controller get entryPoint { - final router = Router(); - - router - .route("/model/[:id]") - .link(() => ManagedObjectController(context)); - - router - .route("/") - .linkFunction((Request request) async => Response.ok('ok')); - - return router; - } - - /* - * Helper methods - */ - - ManagedContext contextWithConnectionInfo( - DatabaseConfiguration connectionInfo) { - final dataModel = ManagedDataModel.fromCurrentMirrorSystem(); - final psc = PostgreSQLPersistentStore( - connectionInfo.username, - connectionInfo.password, - connectionInfo.host, - connectionInfo.port, - connectionInfo.databaseName); - - return ManagedContext(dataModel, psc); - } -} - -/// An instance of this class reads values from a configuration -/// file specific to this application. -/// -/// Configuration files must have key-value for the properties in this class. -/// For more documentation on configuration files, see https://conduit.io/docs/configure/ and -/// https://pub.dartlang.org/packages/safe_config. -class DdeGestureManagerConfiguration extends Configuration { - DdeGestureManagerConfiguration(String fileName) - : super.fromFile(File(fileName)); - - DatabaseConfiguration? database; -} diff --git a/api/lib/dde_gesture_manager.dart b/api/lib/dde_gesture_manager.dart deleted file mode 100644 index 713096c..0000000 --- a/api/lib/dde_gesture_manager.dart +++ /dev/null @@ -1,11 +0,0 @@ -/// dde_gesture_manager -/// -/// A conduit web server. -library dde_gesture_manager; - -export 'dart:async'; -export 'dart:io'; - -export 'package:conduit/conduit.dart'; - -export 'channel.dart'; diff --git a/api/lib/dde_gesture_manager_api.dart b/api/lib/dde_gesture_manager_api.dart new file mode 100644 index 0000000..aa2f208 --- /dev/null +++ b/api/lib/dde_gesture_manager_api.dart @@ -0,0 +1,17 @@ +/// Your very own web application! +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:file/local.dart'; +import 'src/config/config.dart' as configuration; +import 'src/routes/routes.dart' as routes; + +/// Configures the server instance. +Future configureServer(Angel app) async { + // Grab a handle to the file system, so that we can do things like + // serve static files. + var fs = const LocalFileSystem(); + + // Set up our application, using the plug-ins defined with this project. + await app.configure(configuration.configureServer(fs)); + await app.configure(routes.configureServer(fs)); +} diff --git a/api/lib/model/model.dart b/api/lib/model/model.dart deleted file mode 100644 index 6eb8a1b..0000000 --- a/api/lib/model/model.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -class Model extends ManagedObject<_Model> implements _Model { - @override - void willInsert() { - createdAt = DateTime.now().toUtc(); - } -} - -class _Model { - @primaryKey - int? id; - - @Column(indexed: true) - String? name; - - DateTime? createdAt; -} diff --git a/api/lib/models.dart b/api/lib/models.dart new file mode 100644 index 0000000..b72d675 --- /dev/null +++ b/api/lib/models.dart @@ -0,0 +1 @@ +export 'src/models/user.dart'; diff --git a/api/lib/src/config/config.dart b/api/lib/src/config/config.dart new file mode 100644 index 0000000..358f42e --- /dev/null +++ b/api/lib/src/config/config.dart @@ -0,0 +1,30 @@ +import 'package:angel3_configuration/angel3_configuration.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_jinja/angel3_jinja.dart'; +import 'package:file/file.dart'; +import 'plugins/plugins.dart' as plugins; + +/// This is a perfect place to include configuration and load plug-ins. +AngelConfigurer configureServer(FileSystem fileSystem) { + return (Angel app) async { + // Load configuration from the `config/` directory. + // + // See: https://github.com/angel-dart/configuration + await app.configure(configuration(fileSystem)); + + // Configure our application to render jinja templates from the `views/` directory. + // + // See: https://github.com/angel-dart/jinja + await app.configure(jinja(path: fileSystem.directory('views').path)); + + // Apply another plug-ins, i.e. ones that *you* have written. + // + // Typically, the plugins in `lib/src/config/plugins/plugins.dart` are plug-ins + // that add functionality specific to your application. + // + // If you write a plug-in that you plan to use again, or are + // using one created by the community, include it in + // `lib/src/config/config.dart`. + await plugins.configureServer(app); + }; +} diff --git a/api/lib/src/config/plugins/orm.dart b/api/lib/src/config/plugins/orm.dart new file mode 100644 index 0000000..be286f8 --- /dev/null +++ b/api/lib/src/config/plugins/orm.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_orm_postgres/angel3_orm_postgres.dart'; +import 'package:postgres/postgres.dart'; + +Future configureServer(Angel app) async { + var connection = await connectToPostgres(app.configuration); + await connection.open(); + + var logger = app.environment.isProduction ? null : app.logger; + var executor = PostgreSqlExecutor(connection, logger: logger); + + app + ..container!.registerSingleton(executor) + ..shutdownHooks.add((_) => connection.close()); +} + +Future connectToPostgres(Map configuration) async { + var postgresConfig = configuration['postgres'] as Map? ?? {}; + var connection = PostgreSQLConnection( + postgresConfig['host'] as String? ?? 'localhost', + postgresConfig['port'] as int? ?? 5432, + postgresConfig['database_name'] as String? ?? + Platform.environment['USER'] ?? + Platform.environment['USERNAME'] ?? + '', + username: postgresConfig['username'] as String?, + password: postgresConfig['password'] as String?, + timeZone: postgresConfig['time_zone'] as String? ?? 'UTC', + timeoutInSeconds: postgresConfig['timeout_in_seconds'] as int? ?? 30, + useSSL: postgresConfig['use_ssl'] as bool? ?? false); + return connection; +} diff --git a/api/lib/src/config/plugins/plugins.dart b/api/lib/src/config/plugins/plugins.dart new file mode 100644 index 0000000..acda711 --- /dev/null +++ b/api/lib/src/config/plugins/plugins.dart @@ -0,0 +1,8 @@ +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'orm.dart' as orm; + +Future configureServer(Angel app) async { + // Include any plugins you have made here. + await app.configure(orm.configureServer); +} diff --git a/api/lib/src/models/base_model.dart b/api/lib/src/models/base_model.dart new file mode 100644 index 0000000..4704517 --- /dev/null +++ b/api/lib/src/models/base_model.dart @@ -0,0 +1,8 @@ +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; + +abstract class BaseModel extends Model { + @SerializableField(isNullable: true) + @Column(type: ColumnType.json) + Map? get metadata; +} diff --git a/api/lib/src/models/user.dart b/api/lib/src/models/user.dart new file mode 100644 index 0000000..1288a20 --- /dev/null +++ b/api/lib/src/models/user.dart @@ -0,0 +1,19 @@ +import 'package:angel3_migration/angel3_migration.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:dde_gesture_manager_api/src/models/base_model.dart'; +import 'package:optional/optional.dart'; +part 'user.g.dart'; + +@serializable +@orm +abstract class _User extends BaseModel { + @SerializableField(isNullable: false) + String? get email; + + @SerializableField(isNullable: false) + String? get password; + + @SerializableField(isNullable: false) + String? get token; +} \ No newline at end of file diff --git a/api/lib/src/models/user.g.dart b/api/lib/src/models/user.g.dart new file mode 100644 index 0000000..669e5f8 --- /dev/null +++ b/api/lib/src/models/user.g.dart @@ -0,0 +1,369 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// MigrationGenerator +// ************************************************************************** + +class UserMigration extends Migration { + @override + void up(Schema schema) { + schema.create('users', (table) { + table.serial('id').primaryKey(); + table.timeStamp('created_at'); + table.timeStamp('updated_at'); + table.declareColumn( + 'metadata', Column(type: ColumnType('jsonb'), length: 256)); + table.varChar('email', length: 256); + table.varChar('password', length: 256); + table.varChar('token', length: 256); + }); + } + + @override + void down(Schema schema) { + schema.drop('users'); + } +} + +// ************************************************************************** +// OrmGenerator +// ************************************************************************** + +class UserQuery extends Query { + UserQuery({Query? parent, Set? trampoline}) : super(parent: parent) { + trampoline ??= {}; + trampoline.add(tableName); + _where = UserQueryWhere(this); + } + + @override + final UserQueryValues values = UserQueryValues(); + + UserQueryWhere? _where; + + @override + Map get casts { + return {}; + } + + @override + String get tableName { + return 'users'; + } + + @override + List get fields { + return const [ + 'id', + 'created_at', + 'updated_at', + 'metadata', + 'email', + 'password', + 'token' + ]; + } + + @override + UserQueryWhere? get where { + return _where; + } + + @override + UserQueryWhere newWhereClause() { + return UserQueryWhere(this); + } + + static User? parseRow(List row) { + if (row.every((x) => x == null)) { + return null; + } + var model = User( + id: row[0].toString(), + createdAt: (row[1] as DateTime?), + updatedAt: (row[2] as DateTime?), + metadata: (row[3] as Map?), + email: (row[4] as String?), + password: (row[5] as String?), + token: (row[6] as String?)); + return model; + } + + @override + Optional deserialize(List row) { + return Optional.ofNullable(parseRow(row)); + } +} + +class UserQueryWhere extends QueryWhere { + UserQueryWhere(UserQuery query) + : id = NumericSqlExpressionBuilder(query, 'id'), + createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'), + updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at'), + metadata = MapSqlExpressionBuilder(query, 'metadata'), + email = StringSqlExpressionBuilder(query, 'email'), + password = StringSqlExpressionBuilder(query, 'password'), + token = StringSqlExpressionBuilder(query, 'token'); + + final NumericSqlExpressionBuilder id; + + final DateTimeSqlExpressionBuilder createdAt; + + final DateTimeSqlExpressionBuilder updatedAt; + + final MapSqlExpressionBuilder metadata; + + final StringSqlExpressionBuilder email; + + final StringSqlExpressionBuilder password; + + final StringSqlExpressionBuilder token; + + @override + List get expressionBuilders { + return [id, createdAt, updatedAt, metadata, email, password, token]; + } +} + +class UserQueryValues extends MapQueryValues { + @override + Map get casts { + return {}; + } + + String? get id { + return (values['id'] as String?); + } + + set id(String? value) => values['id'] = value; + DateTime? get createdAt { + return (values['created_at'] as DateTime?); + } + + set createdAt(DateTime? value) => values['created_at'] = value; + DateTime? get updatedAt { + return (values['updated_at'] as DateTime?); + } + + set updatedAt(DateTime? value) => values['updated_at'] = value; + Map? get metadata { + return (values['metadata'] as Map?); + } + + set metadata(Map? value) => values['metadata'] = value; + String? get email { + return (values['email'] as String?); + } + + set email(String? value) => values['email'] = value; + String? get password { + return (values['password'] as String?); + } + + set password(String? value) => values['password'] = value; + String? get token { + return (values['token'] as String?); + } + + set token(String? value) => values['token'] = value; + void copyFrom(User model) { + createdAt = model.createdAt; + updatedAt = model.updatedAt; + metadata = model.metadata; + email = model.email; + password = model.password; + token = model.token; + } +} + +// ************************************************************************** +// JsonModelGenerator +// ************************************************************************** + +@generatedSerializable +class User extends _User { + User( + {this.id, + this.createdAt, + this.updatedAt, + Map? metadata, + required this.email, + required this.password, + required this.token}) + : metadata = Map.unmodifiable(metadata ?? {}); + + /// A unique identifier corresponding to this item. + @override + String? id; + + /// The time at which this item was created. + @override + DateTime? createdAt; + + /// The last time at which this item was updated. + @override + DateTime? updatedAt; + + @override + Map? metadata; + + @override + String? email; + + @override + String? password; + + @override + String? token; + + User copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Map? metadata, + String? email, + String? password, + String? token}) { + return User( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + metadata: metadata ?? this.metadata, + email: email ?? this.email, + password: password ?? this.password, + token: token ?? this.token); + } + + @override + bool operator ==(other) { + return other is _User && + other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + MapEquality( + keys: DefaultEquality(), values: DefaultEquality()) + .equals(other.metadata, metadata) && + other.email == email && + other.password == password && + other.token == token; + } + + @override + int get hashCode { + return hashObjects( + [id, createdAt, updatedAt, metadata, email, password, token]); + } + + @override + String toString() { + return 'User(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, metadata=$metadata, email=$email, password=$password, token=$token)'; + } + + Map toJson() { + return UserSerializer.toMap(this); + } +} + +// ************************************************************************** +// SerializerGenerator +// ************************************************************************** + +const UserSerializer userSerializer = UserSerializer(); + +class UserEncoder extends Converter { + const UserEncoder(); + + @override + Map convert(User model) => UserSerializer.toMap(model); +} + +class UserDecoder extends Converter { + const UserDecoder(); + + @override + User convert(Map map) => UserSerializer.fromMap(map); +} + +class UserSerializer extends Codec { + const UserSerializer(); + + @override + UserEncoder get encoder => const UserEncoder(); + @override + UserDecoder get decoder => const UserDecoder(); + static User fromMap(Map map) { + if (map['email'] == null) { + throw FormatException("Missing required field 'email' on User."); + } + + if (map['password'] == null) { + throw FormatException("Missing required field 'password' on User."); + } + + if (map['token'] == null) { + throw FormatException("Missing required field 'token' on User."); + } + + return User( + id: map['id'] as String?, + createdAt: map['created_at'] != null + ? (map['created_at'] is DateTime + ? (map['created_at'] as DateTime) + : DateTime.parse(map['created_at'].toString())) + : null, + updatedAt: map['updated_at'] != null + ? (map['updated_at'] is DateTime + ? (map['updated_at'] as DateTime) + : DateTime.parse(map['updated_at'].toString())) + : null, + metadata: map['metadata'] is Map + ? (map['metadata'] as Map).cast() + : {}, + email: map['email'] as String?, + password: map['password'] as String?, + token: map['token'] as String?); + } + + static Map toMap(_User? model) { + if (model == null) { + return {}; + } + return { + 'id': model.id, + 'created_at': model.createdAt?.toIso8601String(), + 'updated_at': model.updatedAt?.toIso8601String(), + 'metadata': model.metadata, + 'email': model.email, + 'password': model.password, + 'token': model.token + }; + } +} + +abstract class UserFields { + static const List allFields = [ + id, + createdAt, + updatedAt, + metadata, + email, + password, + token + ]; + + static const String id = 'id'; + + static const String createdAt = 'created_at'; + + static const String updatedAt = 'updated_at'; + + static const String metadata = 'metadata'; + + static const String email = 'email'; + + static const String password = 'password'; + + static const String token = 'token'; +} diff --git a/api/lib/src/routes/controllers/controller_extensions.dart b/api/lib/src/routes/controllers/controller_extensions.dart new file mode 100644 index 0000000..f0f6007 --- /dev/null +++ b/api/lib/src/routes/controllers/controller_extensions.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_orm/angel3_orm.dart' as orm; + +extension ResponseNoContent on ResponseContext { + noContent() { + statusCode = HttpStatus.noContent; + return close(); + } +} + +extension QueryWhereId on orm.Query { + set whereId(int id) { + (where as dynamic).id.equals(id); + } +} + +extension QueryExecutor on RequestContext { + orm.QueryExecutor get queryExecutor => container!.make(); +} diff --git a/api/lib/src/routes/controllers/user_controllers.dart b/api/lib/src/routes/controllers/user_controllers.dart new file mode 100644 index 0000000..d98b1e3 --- /dev/null +++ b/api/lib/src/routes/controllers/user_controllers.dart @@ -0,0 +1,16 @@ +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'controller_extensions.dart'; + +Future configureServer(Angel app) async { + app.get( + '/user/int:id', + (req, res) async { + var user = await (UserQuery()..where?.metadata.contains({"uid": req.params[UserFields.id]})) + .getOne(req.queryExecutor); + + return res.json(user.value); + }, + ); +} diff --git a/api/lib/src/routes/routes.dart b/api/lib/src/routes/routes.dart new file mode 100644 index 0000000..23a8dec --- /dev/null +++ b/api/lib/src/routes/routes.dart @@ -0,0 +1,39 @@ +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:file/file.dart'; +import 'controllers/user_controllers.dart' as UserControllers; + +/// Put your app routes here! +/// +/// See the wiki for information about routing, requests, and responses: +/// * https://angel3-docs.dukefirehawk.com/guides/basic-routing +/// * https://angel3-docs.dukefirehawk.com/guides/requests-and-responses + +AngelConfigurer configureServer(FileSystem fileSystem) { + return (Angel app) async { + // Typically, you want to mount controllers first, after any global middleware. + await app.configure(UserControllers.configureServer); + + // Throw a 404 if no route matched the request. + app.fallback((req, res) => throw AngelHttpException.notFound()); + + // Set our application up to handle different errors. + // + // Read the following for documentation: + // * https://angel3-docs.dukefirehawk.com/guides/error-handling + + var oldErrorHandler = app.errorHandler; + app.errorHandler = (e, req, res) async { + if (req.accepts('text/html', strict: true)) { + if (e.statusCode == 404 && req.accepts('text/html', strict: true)) { + await res.render('error.html', {'message': 'No router exists for ${req.uri}'}); + } else { + return await res.render('error.html', { + 'message': [e.message, '', e.stackTrace.toString().replaceAll('\n', '
')].join('
') + }); + } + } else { + return await oldErrorHandler(e, req, res); + } + }; + }; +} diff --git a/api/pubspec.yaml b/api/pubspec.yaml index c56bbff..877dd7b 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -1,13 +1,34 @@ -name: dde_gesture_manager -description: An conduit application with a database connection and data model. -version: 0.0.1 - +name: dde_gesture_manager_api +version: 1.0.0 +description: An ORM starter application for Angel3 framework +publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - + sdk: '>=2.12.0 <3.0.0' dependencies: - conduit: ^3.0.0 - + angel3_auth: ^4.0.0 + angel3_configuration: ^4.1.0 + angel3_framework: ^4.2.0 + angel3_migration: ^4.0.0 + angel3_orm: ^4.0.0 + angel3_orm_postgres: ^3.0.0 + angel3_serialize: ^4.1.0 + angel3_production: ^3.1.0 + angel3_static: ^4.1.0 + angel3_validate: ^4.0.0 + belatuk_pretty_logging: ^4.0.0 + optional: ^6.0.0 + logging: ^1.0.0 dev_dependencies: - test: ^1.16.5 - conduit_test: ^3.0.0 \ No newline at end of file + angel3_hot: ^4.2.0 + angel3_jinja: ^2.0.1 + angel3_migration_runner: ^4.0.0 + angel3_orm_generator: ^4.1.0 + angel3_serialize_generator: ^4.2.0 + angel3_test: ^4.0.0 + build_runner: ^2.0.3 + io: ^1.0.0 + test: ^1.17.5 + lints: ^1.0.0 + + + diff --git a/api/test/all_test.dart b/api/test/all_test.dart new file mode 100644 index 0000000..cfb69fc --- /dev/null +++ b/api/test/all_test.dart @@ -0,0 +1,43 @@ +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_test/angel3_test.dart'; +import 'package:test/test.dart'; + +// Angel also includes facilities to make testing easier. +// +// `package:angel_test` ships a client that can test +// both plain HTTP and WebSockets. +// +// Tests do not require your server to actually be mounted on a port, +// so they will run faster than they would in other frameworks, where you +// would have to first bind a socket, and then account for network latency. +// +// See the documentation here: +// https://github.com/angel-dart/test +// +// If you are unfamiliar with Dart's advanced testing library, you can read up +// here: +// https://github.com/dart-lang/test + +void main() async { + late TestClient client; + + setUp(() async { + var app = Angel(); + await app.configure(configureServer); + + client = await connectTo(app); + }); + + tearDown(() async { + await client.close(); + }); + + test('index returns 200', () async { + // Request a resource at the given path. + var response = await client.get(Uri.parse('/')); + + // Expect a 200 response. + expect(response, hasStatus(200)); + }); +} diff --git a/api/test/harness/app.dart b/api/test/harness/app.dart deleted file mode 100644 index a78b76f..0000000 --- a/api/test/harness/app.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:conduit_test/conduit_test.dart'; -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -export 'package:conduit/conduit.dart'; -export 'package:conduit_test/conduit_test.dart'; -export 'package:dde_gesture_manager/dde_gesture_manager.dart'; -export 'package:test/test.dart'; - -/// A testing harness for dde_gesture_manager. -/// -/// A harness for testing an conduit application. Example test file: -/// -/// void main() { -/// Harness harness = Harness()..install(); -/// -/// test("GET /path returns 200", () async { -/// final response = await harness.agent.get("/path"); -/// expectResponse(response, 200); -/// }); -/// } -/// -class Harness extends TestHarness with TestHarnessORMMixin { - @override - ManagedContext? get context => channel?.context; - - @override - Future onSetUp() async { - await resetData(); - } - - @override - Future onTearDown() async {} - - @override - Future seed() async { - // restore any static data. called by resetData. - } -} diff --git a/api/test/simple_controller_test.dart b/api/test/simple_controller_test.dart deleted file mode 100644 index c2c0c9c..0000000 --- a/api/test/simple_controller_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'harness/app.dart'; - -Future main() async { - final harness = Harness()..install(); - - tearDown(() async { - await harness.resetData(); - }); - - test("POST /model", () async { - final response = await harness.agent!.post("/model", body: {"name": "Bob"}); - expect( - response, - hasResponse(200, - body: {"id": isNotNull, "name": "Bob", "createdAt": isTimestamp})); - }); - - test("GET /model/:id returns previously created object", () async { - var response = await harness.agent!.post("/model", body: {"name": "Bob"}); - - final createdObject = response?.body.as(); - response = - await harness.agent!.request("/model/${createdObject["id"]}").get(); - expect( - response, - hasResponse(200, body: { - "id": createdObject["id"], - "name": createdObject["name"], - "createdAt": createdObject["createdAt"] - })); - }); -} diff --git a/api/views/error.html b/api/views/error.html new file mode 100644 index 0000000..5b94687 --- /dev/null +++ b/api/views/error.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} +{% block title %}Error{% endblock %} +{% block body %} +

Error: {{ message }}

+{% endblock %} \ No newline at end of file diff --git a/api/views/layout.html b/api/views/layout.html new file mode 100644 index 0000000..2217499 --- /dev/null +++ b/api/views/layout.html @@ -0,0 +1,10 @@ + + + + + {% block title %}{% endblock %} + + + {% block body %}{% endblock %} + + \ No newline at end of file diff --git a/api/web/css/site.css b/api/web/css/site.css new file mode 100644 index 0000000..9e40b8d --- /dev/null +++ b/api/web/css/site.css @@ -0,0 +1,27 @@ +html, body { + height: 100%; +} + +body { + margin: 0; + padding: 0; + width: 100%; + display: table; + font-weight: 100; + font-family: 'Lato', sans-serif; +} + +.container { + text-align: center; + display: table-cell; + vertical-align: middle; +} + +.content { + text-align: center; + display: inline-block; +} + +.title { + font-size: 96px; +} \ No newline at end of file diff --git a/api/web/images/favicon.png b/api/web/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..b8f5e2cfb5f71b9edd3358c54fc55ae965bd6366 GIT binary patch literal 10624 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 am;M`@6j>Vt*#+MK0000 - - - - - Login - - - -
-

Login

-
- - - -
- - -
-
- - -
- -
-
- - - \ No newline at end of file diff --git a/api/web/robots.txt b/api/web/robots.txt new file mode 100644 index 0000000..f328961 --- /dev/null +++ b/api/web/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /admin \ No newline at end of file -- 2.11.0 From 1a0e8f8de72ab86ff07d0c9cbf33c192ede8bac0 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Fri, 17 Dec 2021 19:12:50 +0800 Subject: [PATCH 13/14] feat: change api framework to angel3. --- api/.gitignore | 2 + api/lib/apis.dart | 8 + api/lib/models.dart | 1 + api/lib/src/models/app_version.dart | 11 + api/lib/src/models/user.g.dart | 369 --------------------- .../src/routes/controllers/system_controllers.dart | 26 ++ api/lib/src/routes/routes.dart | 6 +- api/pubspec.yaml | 1 + app/lib/widgets/version_checker.dart | 14 +- app/pubspec.yaml | 2 + 10 files changed, 68 insertions(+), 372 deletions(-) create mode 100644 api/lib/apis.dart create mode 100644 api/lib/src/models/app_version.dart delete mode 100644 api/lib/src/models/user.g.dart create mode 100644 api/lib/src/routes/controllers/system_controllers.dart diff --git a/api/.gitignore b/api/.gitignore index a0541d9..342a80c 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -93,3 +93,5 @@ server_log.txt /config/production.yaml /config/development.yaml + +*.g.dart diff --git a/api/lib/apis.dart b/api/lib/apis.dart new file mode 100644 index 0000000..e309e7b --- /dev/null +++ b/api/lib/apis.dart @@ -0,0 +1,8 @@ +class Apis { + static final system = SystemApis(); +} + +class SystemApis { + static final String _path = '/system'; + String get appVersion => _path + '/app-version'; +} \ No newline at end of file diff --git a/api/lib/models.dart b/api/lib/models.dart index b72d675..e604c02 100644 --- a/api/lib/models.dart +++ b/api/lib/models.dart @@ -1 +1,2 @@ export 'src/models/user.dart'; +export 'src/models/app_version.dart'; \ No newline at end of file diff --git a/api/lib/src/models/app_version.dart b/api/lib/src/models/app_version.dart new file mode 100644 index 0000000..e52b2b3 --- /dev/null +++ b/api/lib/src/models/app_version.dart @@ -0,0 +1,11 @@ +import 'package:angel3_serialize/angel3_serialize.dart'; +part 'app_version.g.dart'; + +@serializable +abstract class _AppVersion { + @SerializableField(isNullable: false) + String? get versionName; + + @SerializableField(isNullable: false) + int? get versionCode; +} \ No newline at end of file diff --git a/api/lib/src/models/user.g.dart b/api/lib/src/models/user.g.dart deleted file mode 100644 index 669e5f8..0000000 --- a/api/lib/src/models/user.g.dart +++ /dev/null @@ -1,369 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user.dart'; - -// ************************************************************************** -// MigrationGenerator -// ************************************************************************** - -class UserMigration extends Migration { - @override - void up(Schema schema) { - schema.create('users', (table) { - table.serial('id').primaryKey(); - table.timeStamp('created_at'); - table.timeStamp('updated_at'); - table.declareColumn( - 'metadata', Column(type: ColumnType('jsonb'), length: 256)); - table.varChar('email', length: 256); - table.varChar('password', length: 256); - table.varChar('token', length: 256); - }); - } - - @override - void down(Schema schema) { - schema.drop('users'); - } -} - -// ************************************************************************** -// OrmGenerator -// ************************************************************************** - -class UserQuery extends Query { - UserQuery({Query? parent, Set? trampoline}) : super(parent: parent) { - trampoline ??= {}; - trampoline.add(tableName); - _where = UserQueryWhere(this); - } - - @override - final UserQueryValues values = UserQueryValues(); - - UserQueryWhere? _where; - - @override - Map get casts { - return {}; - } - - @override - String get tableName { - return 'users'; - } - - @override - List get fields { - return const [ - 'id', - 'created_at', - 'updated_at', - 'metadata', - 'email', - 'password', - 'token' - ]; - } - - @override - UserQueryWhere? get where { - return _where; - } - - @override - UserQueryWhere newWhereClause() { - return UserQueryWhere(this); - } - - static User? parseRow(List row) { - if (row.every((x) => x == null)) { - return null; - } - var model = User( - id: row[0].toString(), - createdAt: (row[1] as DateTime?), - updatedAt: (row[2] as DateTime?), - metadata: (row[3] as Map?), - email: (row[4] as String?), - password: (row[5] as String?), - token: (row[6] as String?)); - return model; - } - - @override - Optional deserialize(List row) { - return Optional.ofNullable(parseRow(row)); - } -} - -class UserQueryWhere extends QueryWhere { - UserQueryWhere(UserQuery query) - : id = NumericSqlExpressionBuilder(query, 'id'), - createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'), - updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at'), - metadata = MapSqlExpressionBuilder(query, 'metadata'), - email = StringSqlExpressionBuilder(query, 'email'), - password = StringSqlExpressionBuilder(query, 'password'), - token = StringSqlExpressionBuilder(query, 'token'); - - final NumericSqlExpressionBuilder id; - - final DateTimeSqlExpressionBuilder createdAt; - - final DateTimeSqlExpressionBuilder updatedAt; - - final MapSqlExpressionBuilder metadata; - - final StringSqlExpressionBuilder email; - - final StringSqlExpressionBuilder password; - - final StringSqlExpressionBuilder token; - - @override - List get expressionBuilders { - return [id, createdAt, updatedAt, metadata, email, password, token]; - } -} - -class UserQueryValues extends MapQueryValues { - @override - Map get casts { - return {}; - } - - String? get id { - return (values['id'] as String?); - } - - set id(String? value) => values['id'] = value; - DateTime? get createdAt { - return (values['created_at'] as DateTime?); - } - - set createdAt(DateTime? value) => values['created_at'] = value; - DateTime? get updatedAt { - return (values['updated_at'] as DateTime?); - } - - set updatedAt(DateTime? value) => values['updated_at'] = value; - Map? get metadata { - return (values['metadata'] as Map?); - } - - set metadata(Map? value) => values['metadata'] = value; - String? get email { - return (values['email'] as String?); - } - - set email(String? value) => values['email'] = value; - String? get password { - return (values['password'] as String?); - } - - set password(String? value) => values['password'] = value; - String? get token { - return (values['token'] as String?); - } - - set token(String? value) => values['token'] = value; - void copyFrom(User model) { - createdAt = model.createdAt; - updatedAt = model.updatedAt; - metadata = model.metadata; - email = model.email; - password = model.password; - token = model.token; - } -} - -// ************************************************************************** -// JsonModelGenerator -// ************************************************************************** - -@generatedSerializable -class User extends _User { - User( - {this.id, - this.createdAt, - this.updatedAt, - Map? metadata, - required this.email, - required this.password, - required this.token}) - : metadata = Map.unmodifiable(metadata ?? {}); - - /// A unique identifier corresponding to this item. - @override - String? id; - - /// The time at which this item was created. - @override - DateTime? createdAt; - - /// The last time at which this item was updated. - @override - DateTime? updatedAt; - - @override - Map? metadata; - - @override - String? email; - - @override - String? password; - - @override - String? token; - - User copyWith( - {String? id, - DateTime? createdAt, - DateTime? updatedAt, - Map? metadata, - String? email, - String? password, - String? token}) { - return User( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - metadata: metadata ?? this.metadata, - email: email ?? this.email, - password: password ?? this.password, - token: token ?? this.token); - } - - @override - bool operator ==(other) { - return other is _User && - other.id == id && - other.createdAt == createdAt && - other.updatedAt == updatedAt && - MapEquality( - keys: DefaultEquality(), values: DefaultEquality()) - .equals(other.metadata, metadata) && - other.email == email && - other.password == password && - other.token == token; - } - - @override - int get hashCode { - return hashObjects( - [id, createdAt, updatedAt, metadata, email, password, token]); - } - - @override - String toString() { - return 'User(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, metadata=$metadata, email=$email, password=$password, token=$token)'; - } - - Map toJson() { - return UserSerializer.toMap(this); - } -} - -// ************************************************************************** -// SerializerGenerator -// ************************************************************************** - -const UserSerializer userSerializer = UserSerializer(); - -class UserEncoder extends Converter { - const UserEncoder(); - - @override - Map convert(User model) => UserSerializer.toMap(model); -} - -class UserDecoder extends Converter { - const UserDecoder(); - - @override - User convert(Map map) => UserSerializer.fromMap(map); -} - -class UserSerializer extends Codec { - const UserSerializer(); - - @override - UserEncoder get encoder => const UserEncoder(); - @override - UserDecoder get decoder => const UserDecoder(); - static User fromMap(Map map) { - if (map['email'] == null) { - throw FormatException("Missing required field 'email' on User."); - } - - if (map['password'] == null) { - throw FormatException("Missing required field 'password' on User."); - } - - if (map['token'] == null) { - throw FormatException("Missing required field 'token' on User."); - } - - return User( - id: map['id'] as String?, - createdAt: map['created_at'] != null - ? (map['created_at'] is DateTime - ? (map['created_at'] as DateTime) - : DateTime.parse(map['created_at'].toString())) - : null, - updatedAt: map['updated_at'] != null - ? (map['updated_at'] is DateTime - ? (map['updated_at'] as DateTime) - : DateTime.parse(map['updated_at'].toString())) - : null, - metadata: map['metadata'] is Map - ? (map['metadata'] as Map).cast() - : {}, - email: map['email'] as String?, - password: map['password'] as String?, - token: map['token'] as String?); - } - - static Map toMap(_User? model) { - if (model == null) { - return {}; - } - return { - 'id': model.id, - 'created_at': model.createdAt?.toIso8601String(), - 'updated_at': model.updatedAt?.toIso8601String(), - 'metadata': model.metadata, - 'email': model.email, - 'password': model.password, - 'token': model.token - }; - } -} - -abstract class UserFields { - static const List allFields = [ - id, - createdAt, - updatedAt, - metadata, - email, - password, - token - ]; - - static const String id = 'id'; - - static const String createdAt = 'created_at'; - - static const String updatedAt = 'updated_at'; - - static const String metadata = 'metadata'; - - static const String email = 'email'; - - static const String password = 'password'; - - static const String token = 'token'; -} diff --git a/api/lib/src/routes/controllers/system_controllers.dart b/api/lib/src/routes/controllers/system_controllers.dart new file mode 100644 index 0000000..9ff1c39 --- /dev/null +++ b/api/lib/src/routes/controllers/system_controllers.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:dde_gesture_manager_api/apis.dart'; +import 'package:dde_gesture_manager_api/src/models/app_version.dart'; +import 'package:file/file.dart'; +import 'package:yaml/yaml.dart'; + +late FileSystem fs; + +Future configureServer(Angel app) async { + app.get( + Apis.system.appVersion, + (req, res) async { + var pubspec = fs.currentDirectory.parent.childDirectory('app').childFile('pubspec.yaml').readAsStringSync(); + var version = loadYaml(pubspec)['version'] as String; + var versions = version.split('+'); + return res.json(AppVersion(versionName: versions.first, versionCode: int.parse(versions.last))); + }, + ); +} + +configureServerWithFileSystem(FileSystem fileSystem) { + fs = fileSystem; + return configureServer; +} diff --git a/api/lib/src/routes/routes.dart b/api/lib/src/routes/routes.dart index 23a8dec..f1eec6c 100644 --- a/api/lib/src/routes/routes.dart +++ b/api/lib/src/routes/routes.dart @@ -1,6 +1,7 @@ import 'package:angel3_framework/angel3_framework.dart'; import 'package:file/file.dart'; -import 'controllers/user_controllers.dart' as UserControllers; +import 'controllers/user_controllers.dart' as user_controllers; +import 'controllers/system_controllers.dart' as system_controllers; /// Put your app routes here! /// @@ -11,7 +12,8 @@ import 'controllers/user_controllers.dart' as UserControllers; AngelConfigurer configureServer(FileSystem fileSystem) { return (Angel app) async { // Typically, you want to mount controllers first, after any global middleware. - await app.configure(UserControllers.configureServer); + await app.configure(system_controllers.configureServerWithFileSystem(fileSystem)); + await app.configure(user_controllers.configureServer); // Throw a 404 if no route matched the request. app.fallback((req, res) => throw AngelHttpException.notFound()); diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 877dd7b..d4f6806 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: belatuk_pretty_logging: ^4.0.0 optional: ^6.0.0 logging: ^1.0.0 + yaml: ^3.1.0 dev_dependencies: angel3_hot: ^4.2.0 angel3_jinja: ^2.0.1 diff --git a/app/lib/widgets/version_checker.dart b/app/lib/widgets/version_checker.dart index 7aeb7d3..7e39321 100644 --- a/app/lib/widgets/version_checker.dart +++ b/app/lib/widgets/version_checker.dart @@ -1,6 +1,11 @@ +import 'dart:convert'; + import 'package:dde_gesture_manager/extensions.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:http/http.dart' as http; +import 'package:dde_gesture_manager_api/apis.dart'; +import 'package:dde_gesture_manager_api/models.dart'; class VersionChecker extends StatelessWidget { const VersionChecker({Key? key}) : super(key: key); @@ -21,7 +26,14 @@ class VersionChecker extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 3), child: TextButton( child: Text(LocaleKeys.version_check_update).tr(), - onPressed: () {}, + onPressed: () { + http.get(Uri.parse('http://127.0.0.1:3000' + Apis.system.appVersion)).then((value) { + var appVersion = AppVersionSerializer.fromMap(json.decode(value.body)); + appVersion.versionName.sout(); + appVersion.versionCode.sout(); + appVersion.sout(); + }); + }, ), ), ], diff --git a/app/pubspec.yaml b/app/pubspec.yaml index e64e791..11a3699 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -46,6 +46,8 @@ dependencies: path: 3rd_party/cherry_toast xdg_directories_web: path: 3rd_party/xdg_directories_web + dde_gesture_manager_api: + path: ../api dev_dependencies: flutter_test: -- 2.11.0 From 853132f1a8f1392abab591184d5614a38383a865 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Thu, 30 Dec 2021 20:04:00 +0800 Subject: [PATCH 14/14] feat: implement some api; add md editor to app; login and signup logic. --- api/3rd_party/neat_cache/CHANGELOG.md | 15 + api/3rd_party/neat_cache/LICENSE | 202 ++++++++ api/3rd_party/neat_cache/README.md | 39 ++ api/3rd_party/neat_cache/analysis_options.yaml | 1 + api/3rd_party/neat_cache/lib/cache_provider.dart | 77 +++ api/3rd_party/neat_cache/lib/neat_cache.dart | 228 +++++++++ .../neat_cache/lib/src/providers/inmemory.dart | 110 +++++ .../neat_cache/lib/src/providers/redis.dart | 210 ++++++++ .../neat_cache/lib/src/providers/resp.dart | 526 +++++++++++++++++++++ api/3rd_party/neat_cache/pubspec.yaml | 18 + api/analysis_options.yaml | 1 + api/bin/migrate.dart | 35 +- api/config/default.yaml | 3 +- api/lib/apis.dart | 94 +++- api/lib/models.dart | 3 +- api/lib/src/config/plugins/jwt.dart | 16 + api/lib/src/config/plugins/plugins.dart | 4 + api/lib/src/config/plugins/redis_cache.dart | 36 ++ api/lib/src/models/login_success.dart | 9 + api/lib/src/models/user.dart | 13 +- .../src/routes/controllers/auth_controllers.dart | 78 +++ .../routes/controllers/controller_extensions.dart | 16 + api/lib/src/routes/controllers/middlewares.dart | 35 ++ .../src/routes/controllers/user_controllers.dart | 16 - api/lib/src/routes/routes.dart | 12 +- api/pubspec.yaml | 6 +- api/source_gen.sh | 4 + api/views/confirm_sign_up.html | 8 + api/views/sign_up_result.html | 16 + app/3rd_party/markdown_core/CHANGELOG.md | 3 + app/3rd_party/markdown_core/LICENSE | 201 ++++++++ app/3rd_party/markdown_core/README.md | 17 + app/3rd_party/markdown_core/lib/builder.dart | 340 +++++++++++++ app/3rd_party/markdown_core/lib/markdown.dart | 67 +++ app/3rd_party/markdown_core/lib/text_style.dart | 107 +++++ app/3rd_party/markdown_core/pubspec.yaml | 56 +++ .../markdown_editor_ot/lib/src/preview.dart | 4 + app/3rd_party/markdown_editor_ot/pubspec.yaml | 4 +- app/lib/constants/constants.dart | 5 +- app/lib/constants/sp_keys.dart | 3 + app/lib/extensions/string_extension.dart | 2 + app/lib/http/api.dart | 135 ++++++ app/lib/models/configs.dart | 41 +- app/lib/models/content_layout.dart | 7 +- app/lib/pages/content.dart | 8 +- app/lib/pages/gesture_editor.dart | 6 +- app/lib/pages/local_manager.dart | 3 - app/lib/pages/market.dart | 65 --- app/lib/pages/market_or_me.dart | 117 +++++ app/lib/utils/helper.dart | 30 +- app/lib/utils/init_linux.dart | 35 ++ app/lib/utils/init_web.dart | 1 + app/lib/utils/notificator.dart | 5 +- app/lib/widgets/dde_button.dart | 17 + app/lib/widgets/dde_markdown_field.dart | 10 +- app/lib/widgets/login.dart | 102 ++++ app/lib/widgets/version_checker.dart | 48 +- app/pubspec.yaml | 2 + app/resources/langs/en.json | 38 +- app/resources/langs/zh-CN.json | 38 +- app/source_gen.sh | 4 +- 61 files changed, 3204 insertions(+), 148 deletions(-) create mode 100644 api/3rd_party/neat_cache/CHANGELOG.md create mode 100644 api/3rd_party/neat_cache/LICENSE create mode 100644 api/3rd_party/neat_cache/README.md create mode 100644 api/3rd_party/neat_cache/analysis_options.yaml create mode 100644 api/3rd_party/neat_cache/lib/cache_provider.dart create mode 100644 api/3rd_party/neat_cache/lib/neat_cache.dart create mode 100644 api/3rd_party/neat_cache/lib/src/providers/inmemory.dart create mode 100644 api/3rd_party/neat_cache/lib/src/providers/redis.dart create mode 100644 api/3rd_party/neat_cache/lib/src/providers/resp.dart create mode 100644 api/3rd_party/neat_cache/pubspec.yaml create mode 100644 api/lib/src/config/plugins/jwt.dart create mode 100644 api/lib/src/config/plugins/redis_cache.dart create mode 100644 api/lib/src/models/login_success.dart create mode 100644 api/lib/src/routes/controllers/auth_controllers.dart create mode 100644 api/lib/src/routes/controllers/middlewares.dart delete mode 100644 api/lib/src/routes/controllers/user_controllers.dart create mode 100644 api/source_gen.sh create mode 100644 api/views/confirm_sign_up.html create mode 100644 api/views/sign_up_result.html create mode 100644 app/3rd_party/markdown_core/CHANGELOG.md create mode 100644 app/3rd_party/markdown_core/LICENSE create mode 100644 app/3rd_party/markdown_core/README.md create mode 100644 app/3rd_party/markdown_core/lib/builder.dart create mode 100644 app/3rd_party/markdown_core/lib/markdown.dart create mode 100644 app/3rd_party/markdown_core/lib/text_style.dart create mode 100644 app/3rd_party/markdown_core/pubspec.yaml create mode 100644 app/lib/http/api.dart delete mode 100644 app/lib/pages/market.dart create mode 100644 app/lib/pages/market_or_me.dart create mode 100644 app/lib/widgets/login.dart diff --git a/api/3rd_party/neat_cache/CHANGELOG.md b/api/3rd_party/neat_cache/CHANGELOG.md new file mode 100644 index 0000000..9e14748 --- /dev/null +++ b/api/3rd_party/neat_cache/CHANGELOG.md @@ -0,0 +1,15 @@ +## v2.0.1 + * Fixed issue when adding multiple commands without waiting for them to complete first. + +## v2.0.0 + * Migrated to null-safety, dropping dependency on `package:dartis`. + +## v1.0.2 + * Upgrade `package:dartis`. + +## v1.0.1 + * Avoid unnecessary purging when calling `set` with a `create` function that + returns `null`. + +## v1.0.0 + * Initial release. diff --git a/api/3rd_party/neat_cache/LICENSE b/api/3rd_party/neat_cache/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/api/3rd_party/neat_cache/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/api/3rd_party/neat_cache/README.md b/api/3rd_party/neat_cache/README.md new file mode 100644 index 0000000..7dff0ca --- /dev/null +++ b/api/3rd_party/neat_cache/README.md @@ -0,0 +1,39 @@ +Neat Cache +========== + +Abstractions around in-memory caches stores such as redis, with timeouts and +automatic reconnects. + +**Disclaimer:** This is not an officially supported Google product. + +## Example + +```dart +import 'dart:async' show Future; +import 'dart:convert' show utf8; +import 'package:neat_cache/neat_cache.dart'; + +Future main() async { + final cacheProvider = Cache.redisCacheProvider('redis://localhost:6379'); + final cache = Cache(cacheProvider); + + /// Create a sub-cache using a prefix, and apply a codec to store utf8 + final userCache = cache.withPrefix('users').withCodec(utf8); + + /// Get data form cache + String userinfo = await userCache['peter-pan'].get(); + print(userinfo); + + /// Put data into cache + await userCache['winnie'].set('Like honey'); + + await cacheProvider.close(); +} +``` + + +## Development +To test the redis `CacheProvider` a redis instance must be running on +`localhost:6379`, this can be setup with: + + * `docker run --rm -p 127.0.0.1:6379:6379 redis` diff --git a/api/3rd_party/neat_cache/analysis_options.yaml b/api/3rd_party/neat_cache/analysis_options.yaml new file mode 100644 index 0000000..108d105 --- /dev/null +++ b/api/3rd_party/neat_cache/analysis_options.yaml @@ -0,0 +1 @@ +include: package:pedantic/analysis_options.yaml diff --git a/api/3rd_party/neat_cache/lib/cache_provider.dart b/api/3rd_party/neat_cache/lib/cache_provider.dart new file mode 100644 index 0000000..77abe4e --- /dev/null +++ b/api/3rd_party/neat_cache/lib/cache_provider.dart @@ -0,0 +1,77 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'neat_cache.dart' show Cache; + +/// Low-level interface for a cache. +/// +/// This can be an in-memory cache, something that writes to disk or to an +/// cache service such as memcached or redis. +/// +/// The [Cache] provided by `package:neat_cache`, is intended to wrap +/// a [CacheProvider] and provide a more convinient high-level interface. +/// +/// Implementers of [CacheProvider] can implement something that stores a value +/// of any type `T`, but usually implementers should aim to implement +/// `CacheProvider>` which stores binary data. +/// +/// Implementations of the [CacheProvider] interface using a remote backing +/// store should throw [IntermittentCacheException] when an intermittent network +/// issue occurs. The [CacheProvider] should obviously attempt to reconnect to +/// the remote backing store, but it should not retry operations. +/// +/// Operations will be retried by [Cache], if necessary. Many use-cases of +/// caching are resilient to intermittent failures. +abstract class CacheProvider { + /// Fetch data stored under [key]. + /// + /// If nothing is cached for [key], this **must** return `null`. + Future get(String key); + + /// Set [value] stored at [key] with optional [ttl]. + /// + /// If a value is already stored at [key], that value should be overwritten + /// with the new [value] given here. + /// + /// When given [ttl] is advisory, however, implementers should avoid returning + /// entries that are far past their [ttl]. + Future set(String key, T value, [Duration? ttl]); + + /// Clear value stored at [key]. + /// + /// After this has returned future calls to [get] for the given [key] should + /// not return any value, unless a new value have been set. + Future purge(String key); + + /// Close all connections, causing all future operations to throw. + /// + /// This method frees resources used by this [CacheProvider], if backed by + /// a remote service like redis, this should close the connection. + /// + /// Calling [close] multiple times does not throw. But after this has returned + /// all future operations should throw [StateError]. + Future close(); +} + +/// Exception thrown when there is an intermittent exception. +/// +/// This is typically thrown if there is an intermittent connection error. +class IntermittentCacheException implements Exception { + final String _message; + IntermittentCacheException(this._message); + + @override + String toString() => _message; +} diff --git a/api/3rd_party/neat_cache/lib/neat_cache.dart b/api/3rd_party/neat_cache/lib/neat_cache.dart new file mode 100644 index 0000000..5f029c1 --- /dev/null +++ b/api/3rd_party/neat_cache/lib/neat_cache.dart @@ -0,0 +1,228 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:convert/convert.dart' show IdentityCodec; +import 'package:logging/logging.dart'; +import 'package:retry/retry.dart'; + +import 'cache_provider.dart'; +import 'src/providers/inmemory.dart'; +import 'src/providers/redis.dart'; + +final _logger = Logger('neat_cache'); + +/// Cache for objects of type [T], wrapping a [CacheProvider] to provide a +/// high-level interface. +/// +/// Cache entries are accessed using the indexing operator `[]`, this returns a +/// [Entry] wrapper that can be used to get/set data cached at given key. +/// +/// **Example** +/// ```dart +/// final Cache> cache = Cache.inMemoryCache(4096); +/// +/// // Write data to cache +/// await cache['cached-zeros'].set([0, 0, 0, 0]); +/// +/// // Read data from cache +/// var r = await cache['cached-zeros'].get(); +/// expect(r, equals([0, 0, 0, 0])); +/// ``` +/// +/// A [Cache] can be _fused_ with a [Codec] using [withCodec] to get a cache +/// that stores a different kind of objects. It is also possible to create +/// a chuild cache using [withPrefix], such that all entries in the child +/// cache have a given prefix. +abstract class Cache { + /// Get [Entry] wrapping data cached at [key]. + Entry operator [](String key); + + /// Get a [Cache] wrapping of this cache with given [prefix]. + Cache withPrefix(String prefix); + + /// Get a [Cache] wrapping of this cache by encoding objects of type [S] as + /// [T] using the given [codec]. + Cache withCodec(Codec codec); + + /// Get a [Cache] wrapping of this cache with given [ttl] as default for all + /// entries being set using [Entry.set]. + /// + /// This only specifies a different default [ttl], to be used when [Entry.set] + /// is called without a [ttl] parameter. + Cache withTTL(Duration ttl); + + /// Create a [Cache] wrapping a [CacheProvider]. + factory Cache(CacheProvider provider) { + return _Cache(provider, '', IdentityCodec()); + } + + /// Create an in-memory [CacheProvider] holding a maximum of [maxSize] cache + /// entries. + static CacheProvider> inMemoryCacheProvider(int maxSize) { + return InMemoryCacheProvider(maxSize); + } + + /// Create a redis [CacheProvider] by connecting using a [connectionString] on + /// the form `redis://:`. + static CacheProvider> redisCacheProvider(Uri connectionString, {Duration commandTimeLimit = const Duration(milliseconds: 200)}) { + return RedisCacheProvider(connectionString, commandTimeLimit: commandTimeLimit); + } +} + +/// Pointer to a location in the cache. +/// +/// This simply wraps a cache key, such that you don't need to supply a cache +/// key for [get], [set] and [purge] operations. +abstract class Entry { + /// Get value stored in this cache entry. + /// + /// If used without [create], this function simply gets the value or `null` if + /// no value is stored. + /// + /// If used with [create], this function becomes an upsert, returning the + /// value stored if any, otherwise creating a new value and storing it with + /// optional [ttl]. If multiple callers are using the same cache this is an + /// inherently racy operation, that is multiple instances of the value may + /// be created. + /// + /// The [get] method is a best-effort method. In case of intermittent failures + /// from the underlying [CacheProvider] the [get] method will ignore failures + /// and return `null` (or result from [create] if specified). + Future get([Future Function() create, Duration ttl]); + + /// Set the value stored in this cache entry. + /// + /// If given [ttl] specifies the time-to-live. Notice that this is advisatory, + /// the underlying [CacheProvider] may choose to evit cache entries at any + /// time. However, it can be assumed that entries will not live far past + /// their [ttl]. + /// + /// The [set] method is a best-effort method. In case of intermittent failures + /// from the underlying [CacheProvider] the [set] method will ignore failures. + /// + /// To ensure that cache entries are purged, use the [purge] method with + /// `retries` not set to zero. + Future set(T? value, [Duration ttl]); + + /// Clear the value stored in this cache entry. + /// + /// If [retries] is `0` (default), this is a best-effort method, which will + /// ignore intermittent failures. If [retries] is non-zero the operation will + /// be retried with exponential back-off, and [IntermittentCacheException] + /// will be thrown if all retries fails. + Future purge({int retries = 0}); +} + +class _Cache implements Cache { + final CacheProvider _provider; + final String _prefix; + final Codec _codec; + final Duration? _ttl; + + _Cache(this._provider, this._prefix, this._codec, [this._ttl]); + + @override + Entry operator [](String key) => _Entry(this, _prefix + key); + + @override + Cache withPrefix(String prefix) => + _Cache(_provider, _prefix + prefix, _codec, _ttl); + + @override + Cache withCodec(Codec codec) => + _Cache(_provider, _prefix, codec.fuse(_codec), _ttl); + + @override + Cache withTTL(Duration ttl) => _Cache(_provider, _prefix, _codec, ttl); +} + +class _Entry implements Entry { + final _Cache _owner; + final String _key; + _Entry(this._owner, this._key); + + @override + Future get([Future Function()? create, Duration? ttl]) async { + V? value; + try { + _logger.finest(() => 'reading cache entry for "$_key"'); + value = await _owner._provider.get(_key); + } on IntermittentCacheException { + _logger.fine( + // embedding [intermittent-cache-failure] to allow for easy log metrics + '[intermittent-cache-failure], failed to get cache entry for "$_key"', + ); + value = null; + } + if (value == null) { + if (create == null) { + return null; + } + final created = await create(); + if (created != null) { + // Calling `set(null)` is equivalent to `purge()`, we can skip that here + await set(created, ttl); + } + return created; + } + return _owner._codec.decode(value); + } + + @override + Future set(T? value, [Duration? ttl]) async { + if (value == null) { + await purge(); + return null; + } + ttl ??= _owner._ttl; + final raw = _owner._codec.encode(value); + try { + await _owner._provider.set(_key, raw, ttl); + } on IntermittentCacheException { + _logger.fine( + // embedding [intermittent-cache-failure] to allow for easy log metrics + '[intermittent-cache-failure], failed to set cache entry for "$_key"', + ); + } + return value; + } + + @override + Future purge({int retries = 0}) async { + // Common path is that we have no retries + if (retries == 0) { + try { + await _owner._provider.purge(_key); + } on IntermittentCacheException { + _logger.fine( + // embedding [intermittent-cache-failure] to allow for easy log metrics + '[intermittent-cache-failure], failed to purge cache entry for "$_key"', + ); + } + return; + } + // Test that we have a positive number of retries. + if (retries < 0) { + ArgumentError.value(retries, 'retries', 'retries < 0 is not allowed'); + } + return await retry( + () => _owner._provider.purge(_key), + retryIf: (e) => e is IntermittentCacheException, + maxAttempts: 1 + retries, + ); + } +} diff --git a/api/3rd_party/neat_cache/lib/src/providers/inmemory.dart b/api/3rd_party/neat_cache/lib/src/providers/inmemory.dart new file mode 100644 index 0000000..45e1347 --- /dev/null +++ b/api/3rd_party/neat_cache/lib/src/providers/inmemory.dart @@ -0,0 +1,110 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import '../../cache_provider.dart'; + +class _InMemoryEntry { + final T value; + final DateTime? _expires; + _InMemoryEntry(this.value, [this._expires]); + bool get isExpired => _expires != null && _expires!.isBefore(DateTime.now()); +} + +/// Simple two-generational LRU cache inspired by: +/// https://github.com/sindresorhus/quick-lru +class InMemoryCacheProvider extends CacheProvider { + /// New generation of cache entries. + Map> _new = >{}; + + /// Old generation of cache entries. + Map> _old = >{}; + + /// Maximum size before clearing old generation. + final int _maxSize; + + /// Have this been closed. + bool _isClosed = false; + + InMemoryCacheProvider(this._maxSize); + + /// Clear old generation, if _maxSize have been reached. + void _maintainGenerations() { + if (_new.length >= _maxSize) { + _old = _new; + _new = {}; + } + } + + @override + Future get(String key) async { + if (_isClosed) { + throw StateError('CacheProvider.close() have been called'); + } + // Lookup in the new generation + var entry = _new[key]; + if (entry != null) { + if (!entry.isExpired) { + return entry.value; + } + // Remove, if expired + _new.remove(key); + } + // Lookup in the old generation + entry = _old[key]; + if (entry != null) { + if (!entry.isExpired) { + // If not expired, we insert the entry into the new generation + _new[key] = entry; + _maintainGenerations(); + return entry.value; + } + // Remove, if expired + _old.remove(key); + } + return null; + } + + @override + Future set(String key, T value, [Duration? ttl]) async { + if (_isClosed) { + throw StateError('CacheProvider.close() have been called'); + } + if (ttl == null) { + _new[key] = _InMemoryEntry(value); + } else { + _new[key] = _InMemoryEntry(value, DateTime.now().add(ttl)); + } + // Always remove key from old generation to avoid risks of looking up there + // if it's overwritten by an entry with a shorter ttl + _old.remove(key); + _maintainGenerations(); + } + + @override + Future purge(String key) async { + if (_isClosed) { + throw StateError('CacheProvider.close() have been called'); + } + _new.remove(key); + _old.remove(key); + } + + @override + Future close() async { + _isClosed = true; + _old = {}; + _new = {}; + } +} diff --git a/api/3rd_party/neat_cache/lib/src/providers/redis.dart b/api/3rd_party/neat_cache/lib/src/providers/redis.dart new file mode 100644 index 0000000..0480d88 --- /dev/null +++ b/api/3rd_party/neat_cache/lib/src/providers/redis.dart @@ -0,0 +1,210 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:io' show IOException, Socket, SocketOption; +import 'dart:typed_data'; +import 'resp.dart'; +import 'package:logging/logging.dart'; +import '../../cache_provider.dart'; + +final _log = Logger('neat_cache'); + +class _RedisContext { + final RespClient client; + _RedisContext({ + required this.client, + }); +} + +class RedisCacheProvider extends CacheProvider> { + final Uri _connectionString; + final Duration _connectTimeLimit; + final Duration _commandTimeLimit; + final Duration _reconnectDelay; + + Future<_RedisContext>? _context; + bool _isClosed = false; + + RedisCacheProvider( + Uri connectionString, { + Duration connectTimeLimit = const Duration(seconds: 30), + Duration commandTimeLimit = const Duration(milliseconds: 200), + Duration reconnectDelay = const Duration(seconds: 30), + }) : _connectionString = connectionString, + _connectTimeLimit = connectTimeLimit, + _commandTimeLimit = commandTimeLimit, + _reconnectDelay = reconnectDelay { + if (!connectionString.isScheme('redis')) { + throw ArgumentError.value( + connectionString, 'connectionString', 'must have scheme redis://'); + } + if (!connectionString.hasEmptyPath) { + throw ArgumentError.value( + connectionString, 'connectionString', 'cannot have a path'); + } + if (connectTimeLimit.isNegative) { + throw ArgumentError.value( + connectTimeLimit, 'connectTimeLimit', 'must be positive'); + } + if (commandTimeLimit.isNegative) { + throw ArgumentError.value( + commandTimeLimit, 'commandTimeLimit', 'must be positive'); + } + if (reconnectDelay.isNegative) { + throw ArgumentError.value( + reconnectDelay, 'reconnectDelay', 'must be positive'); + } + } + + Future<_RedisContext> _createContext() async { + try { + _log.info('Connecting to redis'); + final socket = await Socket.connect( + _connectionString.host, + _connectionString.port, + ).timeout(_connectTimeLimit); + socket.setOption(SocketOption.tcpNoDelay, true); + + + var client = RespClient(socket, socket); + if (_connectionString.userInfo.isNotEmpty) { + await client.command(['AUTH', _connectionString.userInfo]); + } + + // Create context + return _RedisContext( + client: client, + ); + } on RedisConnectionException { + throw IntermittentCacheException('connection failed'); + } on TimeoutException { + throw IntermittentCacheException('connect failed with timeout'); + } on IOException catch (e) { + throw IntermittentCacheException('connect failed with IOException: $e'); + } on Exception { + throw IntermittentCacheException('connect failed with exception'); + } + } + + Future<_RedisContext> _getContext() { + if (_context != null) { + return _context!; + } + _context = _createContext(); + scheduleMicrotask(() async { + _RedisContext ctx; + try { + ctx = await _context!; + } on IntermittentCacheException { + // If connecting fails, then we sleep and try again + await Future.delayed(_reconnectDelay); + _context = null; // reset _context, so next operation creates a new + return; + } catch (e) { + _log.shout('unknown error/exception connecting to redis', e); + _context = null; // reset _context, so next operation creates a new + rethrow; // propagate the error to crash to application. + } + // If connecting was successful, then we await the connection being + // closed or error, and we reset _context. + try { + await ctx.client.closed; + } catch (e) { + // ignore error + } + _context = null; + }); + return _context!; + } + + Future _withResp(Future Function(RespClient) fn) async { + if (_isClosed) { + throw StateError('CacheProvider.closed() has been called'); + } + final ctx = await _getContext(); + try { + return await fn(ctx.client).timeout(_commandTimeLimit); + } on RedisCommandException catch (e) { + throw AssertionError('error from redis command: $e'); + } on TimeoutException { + // If we had a timeout, doing the command we forcibly disconnect + // from the server, such that next retry will use a new connection. + await ctx.client.close(force: true); + throw IntermittentCacheException('redis command timeout'); + } on RedisConnectionException catch (e) { + throw IntermittentCacheException('redis error: $e'); + } on IOException catch (e) { + throw IntermittentCacheException('socket broken: $e'); + } + } + + @override + Future close() async { + _isClosed = true; + if (_context != null) { + try { + final ctx = await _context!; + await ctx.client.close(); + } catch (e) { + // ignore + } + } + } + + @override + Future?> get(String key) => _withResp((client) async { + final r = await client.command(['GET', key]).catchError((e) { + throw e; + }); + if (r == null) { + return null; + } + if (r is Uint8List) { + return r; + } + assert(false, 'unexpected response from redis server'); + + // Force close the client + scheduleMicrotask(() => client.close(force: true)); + }); + + @override + Future set(String key, List value, [Duration? ttl]) => + _withResp((client) async { + final r = await client.command([ + 'SET', + key, + value, + if (ttl != null) ...['EX', ttl.inSeconds], + ]); + if (r != 'OK') { + assert(false, 'unexpected response from redis server'); + + // Force close the client + scheduleMicrotask(() => client.close(force: true)); + } + }); + + @override + Future purge(String key) => _withResp((client) async { + final r = await client.command(['DEL', key]); + if (r is! int) { + assert(false, 'unexpected response from redis server'); + + // Force close the client + scheduleMicrotask(() => client.close(force: true)); + } + }); +} diff --git a/api/3rd_party/neat_cache/lib/src/providers/resp.dart b/api/3rd_party/neat_cache/lib/src/providers/resp.dart new file mode 100644 index 0000000..0883d60 --- /dev/null +++ b/api/3rd_party/neat_cache/lib/src/providers/resp.dart @@ -0,0 +1,526 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Minimalistic [RESP[1] protocol implementation in Dart. +/// +/// [1]: https://redis.io/topics/protocol +library resp; + +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:logging/logging.dart'; + +final _log = Logger('neat_cache:redis'); + +/// Thrown when the server returns an error in response to a command. +class RedisCommandException implements Exception { + final String type; + final String message; + + RedisCommandException._(this.type, this.message); + + @override + String toString() => 'RedisCommandException: $type $message'; +} + +/// Thrown if the redis connection is broken. +/// +/// This typically happens if the connection is unexpectedly closed, the +/// [RESP protocol][1] is violated, or there is an internal error. +/// +/// [1]: https://redis.io/topics/protocol +class RedisConnectionException implements Exception { + final String message; + + RedisConnectionException._(this.message); + + @override + String toString() => 'RedisConnectionException: $message'; +} + +/// Client implementing the [RESP protocol][1]. +/// +/// [1]: https://redis.io/topics/protocol +class RespClient { + static final _newLine = ascii.encode('\r\n'); + + /// No value in redis can be more than [512 MB][1]. + /// + /// [1]: https://redis.io/topics/data-types + static const _maxValueSize = 512 * 1024 * 1024; + + final _ByteStreamScanner _input; + final StreamSink> _output; + Future _pendingStream = Future.value(null); + + final _pending = Queue>(); + + bool _closing = false; + final _closed = Completer(); + + /// Creates an instance of [RespClient] given an [input]stream and an [output] + /// sink. + /// + /// If connecting over TCP as usual the `Socket` object from `dart:io` + /// implements both [Stream] and [StreamSink>]. Thus, + /// the following example is a reasonable way to make a client: + /// + /// ```dart + /// // Connect to redis server + /// final socket = await Socket.connect(host, port); + /// socket.setOption(SocketOption.tcpNoDelay, true); + /// + /// // Create client + /// final client = RespClient(socket, socket); + /// ``` + RespClient( + Stream input, + StreamSink> output, + ) : _input = _ByteStreamScanner(input), + _output = output { + scheduleMicrotask(_readInput); + scheduleMicrotask(() async { + try { + await _output.done; + } catch (e, st) { + if (!_closing) { + return await _abort(e, st); + } + } + if (!_closing) { + await _abort( + RedisConnectionException._('outgoing connection closed'), + StackTrace.current, + ); + } + }); + } + + /// Returns a [Future] that is resolved when the connection is closed. + Future get closed => _closed.future; + + /// Send command to redis and return the result. + /// + /// The [args] is a list of: + /// * [String], + /// * [List], and, + /// * [int]. + /// This is always encoded as an _RESP Array_ of _RESP Bulk Strings_. + /// + /// Response will decoded as follows: + /// * RESP Simple String: returns [String], + /// * RESP Error: throws [RedisCommandException], + /// * RESP Integer: returns [int], + /// * RESP Bulk String: returns [Uint8List], + /// * RESP nil Bulk String: returns `null`, + /// * RESP Array: returns [List], and, + /// * RESP nil Arrray: returns `null`. + /// + /// Throws [RedisConnectionException] if underlying connection as been broken + /// or if the [RESP protocol][1] has been violated. After this, the client + /// should not be used further. + /// + /// Forwards any [Exception] thrown by the underlying connection and aborts + /// the [RespClient]. Once aborted [closed] will be resolved, and further + /// attempts to call [command] will throw [RedisConnectionException]. + /// + /// Consumers are encouraged to handle [RedisConnectionException] and + /// reconnect, creating a new [RespClient], when [RedisConnectionException] is + /// encountered. + /// + /// [1]: https://redis.io/topics/protocol + Future command(List args) async { + if (_closing) { + throw RedisConnectionException._('redis connection is closed'); + } + + final out = BytesBuilder(copy: false); + out.addByte('*'.codeUnitAt(0)); + out.add(ascii.encode(args.length.toString())); + out.add(_newLine); + for (final arg in args) { + List bytes; + if (arg is String) { + bytes = utf8.encode(arg); + } else if (arg is List) { + bytes = arg; + } else if (arg is int) { + bytes = ascii.encode(arg.toString()); + } else { + throw ArgumentError.value( + args, + 'args', + 'arguments for redis must be String, List, int', + ); + } + + out.addByte(r'$'.codeUnitAt(0)); + out.add(ascii.encode(bytes.length.toString())); + out.add(_newLine); + out.add(bytes); + out.add(_newLine); + } + + final c = Completer(); + _pending.addLast(c); + try { + _pendingStream = _pendingStream + .then((value) => _output.addStream(Stream.value(out.toBytes()))); + } on Exception catch (e, st) { + await _abort(e, st); + } + + try { + return await c.future; + } on RedisCommandException catch (e) { + // Don't use rethrow because the stack-trace really should start here. + // we always throw RedisCommandException with a StackTrace.empty, because + // it's a thing that happens on the server, and that stack-trace of the + // code that reads the value from the server is uninteresting. + throw e; // ignore: use_rethrow_when_possible + } + } + + /// Send `QUIT` command to redis and close the connection. + /// + /// If [force] is `true`, then the connection will be forcibly closed + /// immediately. Otherwise, connection will reject new commands, but wait for + /// existing commands to complete. + Future close({bool force = false}) async { + _closing = true; + + if (!_closing) { + // Always send QUIT message to be nice + try { + final quit = command(['QUIT']); + scheduleMicrotask(() async { + await quit.catchError((_) {/* ignore */}); + }); + } catch (_) { + // ignore + } + } + + if (!force) { + scheduleMicrotask(() async { + await _output.close().catchError((_) {/* ignore */}); + }); + await _closed.future; + } else { + await _output.close().catchError((_) {/* ignore */}); + + // Resolve all outstanding requests + final pending = _pending.toList(growable: false); + _pending.clear(); + final e = RedisConnectionException._('redis client forcibly closed'); + final st = StackTrace.current; + pending.forEach((c) => c.completeError(e, st)); + } + await _input.cancel(); + + assert(_pending.isEmpty, 'new pending requests added after close()'); + } + + /// Abort due to internal error + Future _abort(Object e, StackTrace st) async { + if (!_closing) { + _log.warning('redis connection broken:', e, st); + } + + _closing = true; + + // Resolve all outstanding requests + final pending = _pending.toList(growable: false); + _pending.clear(); + scheduleMicrotask(() { + pending.forEach((c) => c.completeError(e, st)); + }); + + if (!_closed.isCompleted) { + _closed.complete(); + } + await _input.cancel(); + + assert(_pending.isEmpty, 'new pending requests added after aborting'); + } + + void _readInput() async { + try { + while (true) { + Object? value; + try { + value = await _readValue(); + } on RedisConnectionException catch (e, st) { + return await _abort(e, st); + } + if (_pending.isEmpty) { + return await _abort( + RedisConnectionException._('unexpected value from server'), + StackTrace.current, + ); + } + final c = _pending.removeFirst(); + if (value is RedisCommandException) { + // This is an error code returned by the server, it doesn't have a + // stack-trace! + c.completeError(value, StackTrace.empty); + } else { + c.complete(value); + } + + if (_closing && _pending.isEmpty) { + return _closed.complete(); + } + } + } catch (e, st) { + _log.shout('internal redis client error:', e, st); + await _abort( + RedisConnectionException._('internal redis client error: $e'), + st, + ); + } + } + + static final _whitespacePattern = RegExp(r'\s'); + + Future _readValue() async { + Uint8List line; + try { + line = await _input.readLine(maxSize: _maxValueSize); + } on Exception catch (e, st) { + await _abort(e, st); + throw RedisConnectionException._('exception reading line: $e'); + } + if (line.isEmpty) { + throw RedisConnectionException._('Incoming stream from server closed'); + } + if (!_endsWithNewLine(line)) { + throw RedisConnectionException._( + 'Invalid server message: missing newline', + ); + } + final type = line[0]; + final rest = Uint8List.sublistView(line, 1, line.length - 2); + + // Handle simple strings + if (type == '+'.codeUnitAt(0)) { + try { + return utf8.decode(rest); + } on FormatException catch (e) { + throw RedisConnectionException._( + 'Invalid simple string from server: $e', + ); + } + } + + // Handle errors + if (type == '-'.codeUnitAt(0)) { + final message = utf8.decode( + rest, + allowMalformed: true, + ); + final i = message.indexOf(_whitespacePattern); + if (i != -1 && i + 1 < message.length) { + return RedisCommandException._( + message.substring(0, i), + message.substring(i + 1), + ); + } + return RedisCommandException._('ERR', message); + } + + // Handle integers + if (type == ':'.codeUnitAt(0)) { + int value; + try { + value = int.parse(ascii.decode(rest)); + } on FormatException catch (e) { + throw RedisConnectionException._( + 'Invalid integer from server: $e', + ); + } + if (value < 0) { + throw RedisConnectionException._( + 'Invalid integer from server: value < 0', + ); + } + return value; + } + + // Handle bulk strings (binary blobs) + if (type == r'$'.codeUnitAt(0)) { + int length; + try { + length = int.parse(ascii.decode(rest)); + } on FormatException catch (e) { + throw RedisConnectionException._( + 'Invalid bulk string length from server: $e', + ); + } + if (length == -1) { + return null; // Special case for nil value + } + if (length < 0 || length > _maxValueSize) { + throw RedisConnectionException._( + 'Invalid bulk string length from server: $length', + ); + } + Uint8List bytes; + try { + bytes = await _input.readBytes(length + 2); + } on Exception catch (e, st) { + await _abort(e, st); + throw RedisConnectionException._('exception reading bytes: $e'); + } + if (bytes.length != length + 2) { + throw RedisConnectionException._('Incoming stream from server closed'); + } + if (!_endsWithNewLine(bytes)) { + throw RedisConnectionException._('Invalid bulk string from server'); + } + return Uint8List.sublistView(bytes, 0, length); + } + + // Handle arrays + if (type == '*'.codeUnitAt(0)) { + int length; + try { + length = int.parse(ascii.decode(rest)); + } on FormatException catch (e) { + throw RedisConnectionException._( + 'Invalid array length from server: $e', + ); + } + if (length == -1) { + return null; // Special case for nil value + } + if (length < 0) { + throw RedisConnectionException._( + 'Invalid array length from server: $length', + ); + } + final values = []; + for (var i = 0; i < length; i++) { + values.add(await _readValue()); + } + return values; + } + + throw RedisConnectionException._( + 'Unknown type from server: ${String.fromCharCode(type)}', + ); + } +} + +bool _endsWithNewLine(Uint8List line) { + final N = line.length; + return (N >= 2 && + line[N - 2] == '\r'.codeUnitAt(0) && + line[N - 1] == '\n'.codeUnitAt(0)); +} + +/// An stream wrapper for reading line-by-line or reading N bytes. +class _ByteStreamScanner { + static final _emptyList = Uint8List.fromList([]); + + final StreamIterator _input; + Uint8List _buffer = _emptyList; + + _ByteStreamScanner(Stream stream) + : _input = StreamIterator(stream); + + /// Read a single byte, return zero if stream has ended. + Future readByte() async { + final bytes = await readBytes(1); + if (bytes.isEmpty) { + return null; + } + return bytes[0]; + } + + /// Read up to [size] bytes from stream, returns less than [size] bytes if + /// stream ends before [size] bytes are read. + Future readBytes(int size) async { + RangeError.checkNotNegative(size, 'size'); + + final out = BytesBuilder(copy: false); + while (size > 0) { + if (_buffer.isEmpty) { + if (!(await _input.moveNext())) { + // Don't attempt to read more data, as there is no more data. + break; + } + _buffer = _input.current; + } + + if (_buffer.isNotEmpty) { + if (size < _buffer.length) { + out.add(Uint8List.sublistView(_buffer, 0, size)); + _buffer = Uint8List.sublistView(_buffer, size); + break; + } + + out.add(_buffer); + size -= _buffer.length; + _buffer = _emptyList; + } + } + + return out.toBytes(); + } + + /// Read until the next `\n` inclusive. + /// + /// Throws [RedisConnectionException] if [maxSize] is exceeded. + Future readLine({int? maxSize}) async { + if (maxSize != null) { + RangeError.checkNotNegative(maxSize, 'maxSize'); + } + + final out = BytesBuilder(copy: false); + while (true) { + if (_buffer.isEmpty) { + if (!(await _input.moveNext())) { + // Don't attempt to read more data, as there is no more data. + break; + } + _buffer = _input.current; + } + + if (_buffer.isNotEmpty) { + final i = _buffer.indexOf('\n'.codeUnitAt(0)); + if (i != -1) { + out.add(Uint8List.sublistView(_buffer, 0, i + 1)); + _buffer = Uint8List.sublistView(_buffer, i + 1); + break; + } + + out.add(_buffer); + _buffer = _emptyList; + } + + if (maxSize != null && out.length > maxSize) { + throw RedisConnectionException._('Line exceeds maxSize: $maxSize'); + } + } + + return out.toBytes(); + } + + /// Cancel underlying stream, ending it prematurely. + Future cancel() async => await _input.cancel(); +} diff --git a/api/3rd_party/neat_cache/pubspec.yaml b/api/3rd_party/neat_cache/pubspec.yaml new file mode 100644 index 0000000..e5097a2 --- /dev/null +++ b/api/3rd_party/neat_cache/pubspec.yaml @@ -0,0 +1,18 @@ +name: neat_cache +version: 2.0.1 +description: | + A neat cache abstraction for wrapping in-memory or redis caches. +homepage: https://github.com/google/dart-neats/tree/master/neat_cache +repository: https://github.com/google/dart-neats.git +issue_tracker: https://github.com/google/dart-neats/labels/pkg:neat_cache +dependencies: + convert: ^3.0.0 + logging: ^1.0.1 + retry: ^3.0.0 + async: ^2.7.0 + meta: ^1.4.0 +dev_dependencies: + test: ^1.5.1 + pedantic: ^1.4.0 +environment: + sdk: '>=2.12.0 <3.0.0' diff --git a/api/analysis_options.yaml b/api/analysis_options.yaml index d66fc45..89ea25f 100644 --- a/api/analysis_options.yaml +++ b/api/analysis_options.yaml @@ -4,3 +4,4 @@ linter: rules: avoid_renaming_method_parameters: false overridden_fields: false + unnecessary_statements: true diff --git a/api/bin/migrate.dart b/api/bin/migrate.dart index 5c5e413..8963f56 100644 --- a/api/bin/migrate.dart +++ b/api/bin/migrate.dart @@ -1,3 +1,5 @@ +import 'package:angel3_migration/angel3_migration.dart'; +import 'package:angel3_orm_postgres/angel3_orm_postgres.dart'; import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart'; import 'package:dde_gesture_manager_api/models.dart'; import 'package:angel3_configuration/angel3_configuration.dart'; @@ -6,6 +8,8 @@ import 'package:angel3_migration_runner/postgres.dart'; import 'package:file/local.dart'; import 'package:logging/logging.dart'; +late Map configuration; + void main(List args) async { // Enable the logging Logger.root.level = Level.INFO; @@ -19,10 +23,39 @@ void main(List args) async { }); var fs = LocalFileSystem(); - var configuration = await loadStandaloneConfiguration(fs); + configuration = await loadStandaloneConfiguration(fs); var connection = await connectToPostgres(configuration); var migrationRunner = PostgresMigrationRunner(connection, migrations: [ UserMigration(), + UserSeed(), ]); await runMigrations(migrationRunner, args); } + +class UserSeed extends Migration { + @override + void up(Schema schema) async { + await doUserSeed(); + } + + @override + void down(Schema schema) async {} +} + +Future doUserSeed() async { + var connection = await connectToPostgres(configuration); + await connection.open(); + var executor = PostgreSqlExecutor(connection); + var userQuery = UserQuery(); + userQuery.where?.email.equals('admin@admin.com'); + var one = await userQuery.getOne(executor); + if (one.isEmpty) { + userQuery = UserQuery(); + userQuery.values.copyFrom(User( + email: 'admin@admin.com', + password: '1234567890', + )); + return userQuery.insert(executor).then((value) => connection.close()); + } + return connection.close(); +} diff --git a/api/config/default.yaml b/api/config/default.yaml index 16dd769..8ee46d8 100644 --- a/api/config/default.yaml +++ b/api/config/default.yaml @@ -9,4 +9,5 @@ postgres: password: App1970# useSSL: false time_zone: UTC -jwt_secret: "OvA9SBLnncot8gFHvt8Gh1qkQ1ptGIQW" \ No newline at end of file +jwt_secret: "OvA9SBLnncot8gFHvt8Gh1qkQ1ptGIQW" +password_salt: "test" \ No newline at end of file diff --git a/api/lib/apis.dart b/api/lib/apis.dart index e309e7b..3f1ba2f 100644 --- a/api/lib/apis.dart +++ b/api/lib/apis.dart @@ -1,8 +1,96 @@ class Apis { + static const apiScheme = 'http'; + static const apiHost = '127.0.0.1'; + static const apiPort = 3000; + + static const appNewVersionUrl = 'https://www.debuggerx.com'; + static final system = SystemApis(); + static final auth = AuthApis(); +} + +class AuthApis { + static final String path = '/auth'; + + String get loginOrSignup => [path, 'login_or_signup'].joinPath(); + + String confirmSignup({required StringParam accessKey}) => [path, 'confirm_sign_up', accessKey].joinPath(); } class SystemApis { - static final String _path = '/system'; - String get appVersion => _path + '/app-version'; -} \ No newline at end of file + static final String path = '/system'; + + String get appVersion => [path, 'app-version'].joinPath(); +} + +final _paramsMap = { + 'IntParam': IntParam.nameOnRoute, + 'DoubleParam': DoubleParam.nameOnRoute, + 'StringParam': StringParam.nameOnRoute, +}; + +extension JoinPath on List { + joinPath() => join('/'); +} + +extension RouteUrl on Function { + String get route { + var funStr = toString(); + funStr = funStr.replaceAll(RegExp(r'.+\(\{'), ' ').replaceAll(RegExp(r'\}\).+'), ' ').replaceAll(' required ', ''); + var parts = funStr.split(','); + Map params = {}; + for (var part in parts) { + var p = part.trim().split(' '); + params[Symbol(p.last)] = (_paramsMap[p.first] as Function).call(p.last); + } + return Function.apply(this, [], params); + } +} + +class IntParam { + final int val; + String? name; + + IntParam(this.val); + + IntParam.nameOnRoute(this.name) : val = 0; + + @override + String toString() => name == null ? val.toString() : 'int:$name'; +} + +class DoubleParam { + final double val; + String? name; + + DoubleParam(this.val); + + DoubleParam.nameOnRoute(this.name) : val = 0; + + @override + String toString() => name == null ? val.toString() : 'double:$name'; +} + +class StringParam { + final String val; + String? name; + + StringParam(this.val); + + StringParam.nameOnRoute(this.name) : val = ''; + + @override + String toString() => name == null ? val.toString() : ':$name'; +} + +extension IntParamExt on int { + IntParam get param => IntParam(this); +} + +extension DoubleParamExt on double { + DoubleParam get param => DoubleParam(this); +} + +extension StringParamExt on String { + StringParam get param => StringParam(this); +} diff --git a/api/lib/models.dart b/api/lib/models.dart index e604c02..c608c7a 100644 --- a/api/lib/models.dart +++ b/api/lib/models.dart @@ -1,2 +1,3 @@ export 'src/models/user.dart'; -export 'src/models/app_version.dart'; \ No newline at end of file +export 'src/models/app_version.dart'; +export 'src/models/login_success.dart'; \ No newline at end of file diff --git a/api/lib/src/config/plugins/jwt.dart b/api/lib/src/config/plugins/jwt.dart new file mode 100644 index 0000000..8cf8700 --- /dev/null +++ b/api/lib/src/config/plugins/jwt.dart @@ -0,0 +1,16 @@ +import 'package:angel3_auth/angel3_auth.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_orm/angel3_orm.dart' as orm; +import 'package:dde_gesture_manager_api/models.dart'; + +Future configureServer(Angel app) async { + var auth = AngelAuth( + jwtKey: app.configuration['jwt_secret'], + allowCookie: false, + deserializer: (p) async => (UserQuery()..where!.id.equals(int.parse(p))) + .getOne(app.container!.make()) + .then((value) => value.value), + serializer: (p) => p.id ?? '', + ); + await auth.configureServer(app); +} diff --git a/api/lib/src/config/plugins/plugins.dart b/api/lib/src/config/plugins/plugins.dart index acda711..a8e18dd 100644 --- a/api/lib/src/config/plugins/plugins.dart +++ b/api/lib/src/config/plugins/plugins.dart @@ -1,8 +1,12 @@ import 'dart:async'; import 'package:angel3_framework/angel3_framework.dart'; import 'orm.dart' as orm; +import 'jwt.dart' as jwt; +import 'redis_cache.dart' as redis_cache; Future configureServer(Angel app) async { // Include any plugins you have made here. await app.configure(orm.configureServer); + await app.configure(jwt.configureServer); + await app.configure(redis_cache.configureServer); } diff --git a/api/lib/src/config/plugins/redis_cache.dart b/api/lib/src/config/plugins/redis_cache.dart new file mode 100644 index 0000000..5ce6962 --- /dev/null +++ b/api/lib/src/config/plugins/redis_cache.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:logging/logging.dart'; +import 'package:neat_cache/neat_cache.dart'; + +Future configureServer(Angel app) async { + final _log = Logger('RedisPlugin'); + + if (app.container == null) { + _log.severe('Angel3 container is null'); + throw StateError('Angel.container is null. All authentication will fail.'); + } + var appContainer = app.container!; + final cache = RedisCache(app.configuration); + appContainer.registerSingleton(cache); +} + +class RedisCache { + late Cache cache; + + RedisCache(Map config) { + var redisConfig = config['redis'] as Map? ?? {}; + + final cacheProvider = Cache.redisCacheProvider( + Uri( + scheme: 'redis', + host: redisConfig['host'], + port: redisConfig['port'], + userInfo: redisConfig['password'], + ), + commandTimeLimit: const Duration(seconds: 1), + ); + cache = Cache(cacheProvider).withCodec(utf8); + } +} diff --git a/api/lib/src/models/login_success.dart b/api/lib/src/models/login_success.dart new file mode 100644 index 0000000..a553682 --- /dev/null +++ b/api/lib/src/models/login_success.dart @@ -0,0 +1,9 @@ +import 'package:angel3_serialize/angel3_serialize.dart'; + +part 'login_success.g.dart'; + +@serializable +class _LoginSuccess { + @SerializableField(isNullable: false) + String? token; +} \ No newline at end of file diff --git a/api/lib/src/models/user.dart b/api/lib/src/models/user.dart index 1288a20..c34c7ef 100644 --- a/api/lib/src/models/user.dart +++ b/api/lib/src/models/user.dart @@ -1,19 +1,24 @@ +import 'dart:convert'; + import 'package:angel3_migration/angel3_migration.dart'; import 'package:angel3_serialize/angel3_serialize.dart'; import 'package:angel3_orm/angel3_orm.dart'; import 'package:dde_gesture_manager_api/src/models/base_model.dart'; import 'package:optional/optional.dart'; +import 'package:crypto/crypto.dart'; + part 'user.g.dart'; @serializable @orm abstract class _User extends BaseModel { + @Column(isNullable: false, indexType: IndexType.unique) @SerializableField(isNullable: false) String? get email; - @SerializableField(isNullable: false) + @Column(isNullable: false, length: 32) + @SerializableField(isNullable: true, exclude: true) String? get password; - @SerializableField(isNullable: false) - String? get token; -} \ No newline at end of file + String secret(String salt) => base64.encode(Hmac(sha256, salt.codeUnits).convert((password ?? '').codeUnits).bytes); +} diff --git a/api/lib/src/routes/controllers/auth_controllers.dart b/api/lib/src/routes/controllers/auth_controllers.dart new file mode 100644 index 0000000..aae33e1 --- /dev/null +++ b/api/lib/src/routes/controllers/auth_controllers.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:angel3_auth/angel3_auth.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:dde_gesture_manager_api/apis.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'package:mailer/mailer.dart'; +import 'package:mailer/smtp_server.dart'; +import 'package:uuid/uuid.dart'; + +import 'controller_extensions.dart'; + +Future configureServer(Angel app) async { + app.post(Apis.auth.loginOrSignup, (req, res) async { + var userParams = UserSerializer.fromMap(req.bodyAsMap); + userParams.password = req.bodyAsMap[UserFields.password]; + var userQuery = UserQuery(); + userQuery.where?.email.equals(userParams.email ?? ''); + var user = await userQuery.getOne(req.queryExecutor); + + if (user.isEmpty) { + String accessKey = Uuid().v1(); + + await req.cache + .withPrefix('sign_up:')[accessKey] + .set(json.encode({'email': userParams.email, 'password': userParams.password}), Duration(minutes: 30)); + var smtpConfig = app.configuration['smtp']; + var smtpServer = + SmtpServer(smtpConfig['host'], ssl: true, username: smtpConfig['username'], password: smtpConfig['password']); + var message = Message() + ..from = Address(smtpConfig['username']) + ..recipients.add(userParams.email) + ..subject = '确认注册' + ..html = await app.viewGenerator!( + 'confirm_sign_up.html', + { + "confirm_url": Uri( + scheme: Apis.apiScheme, + host: Apis.apiHost, + port: Apis.apiPort, + path: Apis.auth.confirmSignup(accessKey: accessKey.param), + ), + }, + ); + + send(message, smtpServer); + return res.notFound(); + } else if (user.value.password != userParams.password) { + return res.unauthorized(); + } else { + var angelAuth = req.container!.make(); + await angelAuth.loginById(user.value.id!, req, res); + var authToken = req.container!.make(); + authToken.payload[UserFields.password] = user.value.secret(app.configuration['password_salt']); + var serializedToken = authToken.serialize(angelAuth.hmac); + return res.json(LoginSuccess(token: serializedToken)); + } + }); + + app.get(Apis.auth.confirmSignup.route, (req, res) async { + var accessKey = req.params['accessKey']; + var cache = req.cache.withPrefix('sign_up:'); + var signupInfo = await cache[accessKey].get(); + if (signupInfo != null && signupInfo is String && signupInfo.isNotEmpty) { + var decodedSignupInfo = json.decode(signupInfo); + var userQuery = UserQuery(); + userQuery.values.copyFrom(User( + email: decodedSignupInfo[UserFields.email], + password: decodedSignupInfo[UserFields.password], + )); + await userQuery.insert(req.queryExecutor); + cache[accessKey].purge(); + return res.render('sign_up_result.html', {'success': true}); + } + return res.render('sign_up_result.html', {'success': false}); + }); +} diff --git a/api/lib/src/routes/controllers/controller_extensions.dart b/api/lib/src/routes/controllers/controller_extensions.dart index f0f6007..f13140e 100644 --- a/api/lib/src/routes/controllers/controller_extensions.dart +++ b/api/lib/src/routes/controllers/controller_extensions.dart @@ -2,12 +2,24 @@ import 'dart:io'; import 'package:angel3_framework/angel3_framework.dart'; import 'package:angel3_orm/angel3_orm.dart' as orm; +import 'package:dde_gesture_manager_api/src/config/plugins/redis_cache.dart'; +import 'package:neat_cache/neat_cache.dart'; extension ResponseNoContent on ResponseContext { noContent() { statusCode = HttpStatus.noContent; return close(); } + + notFound() { + statusCode = HttpStatus.notFound; + return close(); + } + + unauthorized() { + statusCode = HttpStatus.unauthorized; + return close(); + } } extension QueryWhereId on orm.Query { @@ -19,3 +31,7 @@ extension QueryWhereId on orm.Query { extension QueryExecutor on RequestContext { orm.QueryExecutor get queryExecutor => container!.make(); } + +extension RedisExecutor on RequestContext { + Cache get cache => container!.make().cache; +} diff --git a/api/lib/src/routes/controllers/middlewares.dart b/api/lib/src/routes/controllers/middlewares.dart new file mode 100644 index 0000000..84c9b63 --- /dev/null +++ b/api/lib/src/routes/controllers/middlewares.dart @@ -0,0 +1,35 @@ +import 'package:angel3_auth/angel3_auth.dart'; +import 'package:angel3_framework/angel3_framework.dart'; + +import 'package:dde_gesture_manager_api/models.dart'; + +RequestHandler jwtMiddleware() { + return (RequestContext req, ResponseContext res, {bool throwError = true}) async { + bool _reject(ResponseContext res) { + if (throwError) { + res.statusCode = 403; + throw AngelHttpException.forbidden(); + } else { + return false; + } + } + + if (req.container != null) { + var reqContainer = req.container!; + if (reqContainer.has() || req.method == 'OPTIONS') { + return true; + } else if (reqContainer.has>()) { + User user = await reqContainer.makeAsync(); + var authToken = req.container!.make(); + if (user.secret(req.app!.configuration['password_salt']) != authToken.payload[UserFields.password]) { + return _reject(res); + } + return true; + } else { + return _reject(res); + } + } else { + return _reject(res); + } + }; +} diff --git a/api/lib/src/routes/controllers/user_controllers.dart b/api/lib/src/routes/controllers/user_controllers.dart deleted file mode 100644 index d98b1e3..0000000 --- a/api/lib/src/routes/controllers/user_controllers.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'dart:async'; -import 'package:angel3_framework/angel3_framework.dart'; -import 'package:dde_gesture_manager_api/models.dart'; -import 'controller_extensions.dart'; - -Future configureServer(Angel app) async { - app.get( - '/user/int:id', - (req, res) async { - var user = await (UserQuery()..where?.metadata.contains({"uid": req.params[UserFields.id]})) - .getOne(req.queryExecutor); - - return res.json(user.value); - }, - ); -} diff --git a/api/lib/src/routes/routes.dart b/api/lib/src/routes/routes.dart index f1eec6c..4b86464 100644 --- a/api/lib/src/routes/routes.dart +++ b/api/lib/src/routes/routes.dart @@ -1,6 +1,6 @@ import 'package:angel3_framework/angel3_framework.dart'; import 'package:file/file.dart'; -import 'controllers/user_controllers.dart' as user_controllers; +import 'controllers/auth_controllers.dart' as auth_controllers; import 'controllers/system_controllers.dart' as system_controllers; /// Put your app routes here! @@ -11,9 +11,17 @@ import 'controllers/system_controllers.dart' as system_controllers; AngelConfigurer configureServer(FileSystem fileSystem) { return (Angel app) async { + // ParseBody middleware + app.fallback((req, res) async { + if (req.method == "POST") { + await req.parseBody(); + } + return true; + }); + // Typically, you want to mount controllers first, after any global middleware. await app.configure(system_controllers.configureServerWithFileSystem(fileSystem)); - await app.configure(user_controllers.configureServer); + await app.configure(auth_controllers.configureServer); // Throw a 404 if no route matched the request. app.fallback((req, res) => throw AngelHttpException.notFound()); diff --git a/api/pubspec.yaml b/api/pubspec.yaml index d4f6806..9831fbc 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -3,7 +3,7 @@ version: 1.0.0 description: An ORM starter application for Angel3 framework publish_to: none environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.15.0 <3.0.0' dependencies: angel3_auth: ^4.0.0 angel3_configuration: ^4.1.0 @@ -19,6 +19,10 @@ dependencies: optional: ^6.0.0 logging: ^1.0.0 yaml: ^3.1.0 + mailer: ^5.0.2 + uuid: ^3.0.5 + neat_cache: + path: 3rd_party/neat_cache dev_dependencies: angel3_hot: ^4.2.0 angel3_jinja: ^2.0.1 diff --git a/api/source_gen.sh b/api/source_gen.sh new file mode 100644 index 0000000..0903c08 --- /dev/null +++ b/api/source_gen.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +dart pub get +dart run build_runner build \ No newline at end of file diff --git a/api/views/confirm_sign_up.html b/api/views/confirm_sign_up.html new file mode 100644 index 0000000..3e1d60a --- /dev/null +++ b/api/views/confirm_sign_up.html @@ -0,0 +1,8 @@ +{% extends "layout.html" %} +{% block title %}确认注册{% endblock %} +{% block body %} +

确认注册

+

如果是您本人点击了《DDE手势管理器》的登录/注册按钮,并收到了本邮件,请点击下面的链接以完成注册,完成后请回到软件中使用之前填入的邮箱和密码登录账户。

+

如果您对上面的操作并不知情,则可能是其他用户错误使用了您的邮箱地址进行了注册,请无视本邮件,不要点击下面的链接,谢谢合作~

+ {{ confirm_url }} +{% endblock %} \ No newline at end of file diff --git a/api/views/sign_up_result.html b/api/views/sign_up_result.html new file mode 100644 index 0000000..e4fe969 --- /dev/null +++ b/api/views/sign_up_result.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} +{% block title %}注册结果{% endblock %} +{% block body %} +

注册结果

+{% if success %} +

注册成功~

+{% else %} +

注册失败..

+

可能是因为:

+
    +
  • 本链接已经超过三十分钟的有效期,请重新在软件中点击注册按钮
  • +
  • 本链接已经被点击并注册成功,请勿重复点击
  • +
  • 其他错误……非常抱歉,如果方便的话请通过邮件联系我~
  • +
+{% endif%} +{% endblock %} \ No newline at end of file diff --git a/app/3rd_party/markdown_core/CHANGELOG.md b/app/3rd_party/markdown_core/CHANGELOG.md new file mode 100644 index 0000000..4aeb4d7 --- /dev/null +++ b/app/3rd_party/markdown_core/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* first version diff --git a/app/3rd_party/markdown_core/LICENSE b/app/3rd_party/markdown_core/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/app/3rd_party/markdown_core/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/app/3rd_party/markdown_core/README.md b/app/3rd_party/markdown_core/README.md new file mode 100644 index 0000000..6c3cbaf --- /dev/null +++ b/app/3rd_party/markdown_core/README.md @@ -0,0 +1,17 @@ +# markdown_core + +Parse markdown and render it into rich text. + +![show](https://xia-weiyang.github.io/gif/markdown_core.gif) + +``` dart +Markdown( + data: markdownDataString, + linkTap: (link) => print('点击了链接 $link'), + textStyle: // your text style , + image: (imageUrl) { + print('imageUrl $imageUrl'); + return // Your image widget ; + }, +) +``` diff --git a/app/3rd_party/markdown_core/lib/builder.dart b/app/3rd_party/markdown_core/lib/builder.dart new file mode 100644 index 0000000..cff6d8e --- /dev/null +++ b/app/3rd_party/markdown_core/lib/builder.dart @@ -0,0 +1,340 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:html_unescape/html_unescape_small.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:markdown_core/text_style.dart'; + +/// 递归解析标签 +/// [_elementList] 每个标签依次放入该集合 +/// 在[visitElementBefore]时添加 +/// 在[visitElementAfter]时将其移除 + +class MarkdownBuilder implements md.NodeVisitor { + MarkdownBuilder( + this.context, + this.linkTap, + this.widgetImage, + this.maxWidth, + this.defaultTextStyle, { + this.tagTextStyle = defaultTagTextStyle, + required this.onCodeCopied, + }); + + final _widgets = []; + // int _level = 0; + List<_Element> _elementList = <_Element>[]; + + final TextStyle defaultTextStyle; + final TagTextStyle tagTextStyle; + + final BuildContext context; + final LinkTap linkTap; + final WidgetImage widgetImage; + final double maxWidth; + final Function onCodeCopied; + + @override + bool visitElementBefore(md.Element element) { + // _level++; + // debugPrint('visitElementBefore $_level ${element.textContent}'); + + String lastTag = ''; + if (_elementList.isNotEmpty) { + lastTag = _elementList.last.tag; + } + + var textStyle = tagTextStyle( + lastTag, + element.tag, + _elementList.isNotEmpty ? _elementList.last.textStyle : defaultTextStyle, + ); + + _elementList.add(_Element( + element.tag, + textStyle, + element.attributes, + )); + + return true; + } + + @override + void visitText(md.Text text) { + // debugPrint('text ${text.text}'); + + if (_elementList.isEmpty) return; + var last = _elementList.last; + last.textSpans ??= []; + + // 替换特定字符串 + var content = text.text.replaceAll('>', '>'); + content = content.replaceAll('<', '<'); + + if (last.tag == 'a') { + last.textSpans?.add(TextSpan( + text: content, + style: last.textStyle, + recognizer: TapGestureRecognizer() + ..onTap = () { + debugPrint(last.attributes.toString()); + linkTap(last.attributes['href'] ?? ''); + }, + )); + return; + } + + last.textSpans?.add(TextSpan( + text: content, + style: last.textStyle, + )); + } + + final padding = const EdgeInsets.fromLTRB(0, 5, 0, 5); + + String getTextFromElement(dynamic element) { + String result = ''; + if (element is List) { + result = element.map(getTextFromElement).join('\n'); + } else if (element is md.Element) { + result = result = element.children?.map(getTextFromElement).join('\n') ?? ''; + } else { + result = element.text; + } + return result; + } + + @override + void visitElementAfter(md.Element element) { + // debugPrint('visitElementAfter $_level ${element.tag}'); + // _level--; + + if (_elementList.isEmpty) return; + var last = _elementList.last; + _elementList.removeLast(); + var tempWidget; + if (kTextTags.indexOf(element.tag) != -1) { + if (_elementList.isNotEmpty && kTextParentTags.indexOf(_elementList.last.tag) != -1) { + // 内联标签处理 + _elementList.last.textSpans ??= []; + _elementList.last.textSpans?.addAll(last.textSpans ?? []); + } else { + if (last.textSpans?.isNotEmpty ?? false) { + tempWidget = SelectableText.rich( + TextSpan( + children: last.textSpans, + style: last.textStyle, + ), + ); + } + } + } else if ('li' == element.tag) { + tempWidget = _resolveToLi(last); + } else if ('pre' == element.tag) { + var preCode = HtmlUnescape().convert(getTextFromElement(element.children)); + tempWidget = _resolveToPre(last, preCode); + } else if ('blockquote' == element.tag) { + tempWidget = _resolveToBlockquote(last); + } else if ('img' == element.tag) { + if (_elementList.isNotEmpty && (_elementList.last.textSpans?.isNotEmpty ?? false)) { + _widgets.add( + Padding( + padding: padding, + child: RichText( + text: TextSpan( + children: _elementList.last.textSpans, + style: _elementList.last.textStyle, + ), + ), + ), + ); + _elementList.last.textSpans = null; + } + // debugPrint(element.attributes.toString()); + //_elementList.clear(); + _widgets.add( + Padding( + padding: padding, + child: widgetImage(element.attributes['src'] ?? ''), + ), + ); + } else if (last.widgets?.isNotEmpty ?? false) { + if (last.widgets?.length == 1) { + tempWidget = last.widgets?[0]; + } else { + tempWidget = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: last.widgets ?? [], + ); + } + } + + if (tempWidget != null) { + if (_elementList.isEmpty) { + _widgets.add( + Padding( + padding: padding, + child: tempWidget, + ), + ); + } else { + _elementList.last.widgets ??= []; + if (tempWidget is List) { + _elementList.last.widgets?.addAll(tempWidget); + } else { + _elementList.last.widgets?.add(tempWidget); + } + } + } + } + + List build(List nodes) { + _widgets.clear(); + + for (md.Node node in nodes) { + // _level = 0; + _elementList.clear(); + + node.accept(this); + } + return _widgets; + } + + dynamic _resolveToLi(_Element last) { + int liNum = 1; + _elementList.forEach((element) { + if (element.tag == 'li') liNum++; + }); + List widgets = last.widgets ?? []; + List spans = []; + spans.addAll(last.textSpans ?? []); + widgets.insert( + 0, + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.fromLTRB( + 8, + ((last.textStyle.fontSize ?? 0) * 2 - 10) / 2.2, + 8, + 0, + ), + child: Icon( + Icons.circle, + size: 10, + color: last.textStyle.color, + ), + ), + Container( + width: maxWidth - (26 * liNum), + child: RichText( + strutStyle: StrutStyle( + height: 1, + fontSize: last.textStyle.fontSize, + forceStrutHeight: true, + leading: 1, + ), + // textAlign: TextAlign.center, + text: TextSpan( + children: spans, + style: last.textStyle, + ), + ), + ) + ], + )); + + /// 如果是顶层,返回column + if (liNum == 1) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: widgets, + ); + } else { + return widgets; + } + } + + Widget _resolveToPre(_Element last, String preCode) { + return Padding( + padding: const EdgeInsets.fromLTRB(0, 5, 15, 5), + child: Stack( + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark ? const Color(0xff111111) : const Color(0xffeeeeee), + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.fromLTRB(8, 14, 8, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: last.widgets ?? [], + ), + ), + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(10), + child: IconButton( + icon: Icon(Icons.copy_outlined), + onPressed: () { + Clipboard.setData(ClipboardData(text: preCode)).then((_) { + onCodeCopied(); + }); + }, + ), + ), + ), + ], + ), + ); + } + + Widget _resolveToBlockquote(_Element last) { + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 7, + height: double.infinity, + color: Colors.grey.shade400, + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: last.widgets ?? [], + ), + ), + ], + ), + ); + } +} + +class _Element { + _Element( + this.tag, + this.textStyle, + this.attributes, + ); + + final String tag; + List? widgets; + List? textSpans; + TextStyle textStyle; + Map attributes; +} + +/// 链接点击 +typedef void LinkTap(String link); + +typedef Widget WidgetImage(String imageUrl); + +typedef TextStyle TagTextStyle(String lastTag, String tag, TextStyle textStyle); diff --git a/app/3rd_party/markdown_core/lib/markdown.dart b/app/3rd_party/markdown_core/lib/markdown.dart new file mode 100644 index 0000000..5da7aad --- /dev/null +++ b/app/3rd_party/markdown_core/lib/markdown.dart @@ -0,0 +1,67 @@ +library markdown_core; + +import 'package:flutter/material.dart'; +import 'package:markdown/markdown.dart'; +import 'package:markdown_core/builder.dart'; +import 'package:markdown_core/text_style.dart'; + +class Markdown extends StatefulWidget { + const Markdown({ + Key? key, + required this.data, + required this.linkTap, + required this.image, + required this.onCodeCopied, + this.maxWidth, + this.textStyle, + }) : super(key: key); + + final String data; + + final LinkTap linkTap; + + final WidgetImage image; + + final double? maxWidth; + + final TextStyle? textStyle; + + final Function onCodeCopied; + + @override + MarkdownState createState() => MarkdownState(); +} + +class MarkdownState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _parseMarkdown(), + ); + } + + List _parseMarkdown() { + // debugPrint(markdownToHtml( + // widget.data, + // extensionSet: ExtensionSet.gi法inaltHubWeb, + // )); + final List lines = widget.data.split(RegExp(r'\r?\n')); + final nodes = Document( + extensionSet: ExtensionSet.gitHubWeb, + ).parseLines(lines); + return MarkdownBuilder( + context, + widget.linkTap, + widget.image, + widget.maxWidth ?? MediaQuery.of(context).size.width, + widget.textStyle ?? defaultTextStyle(context), + onCodeCopied: widget.onCodeCopied, + ).build(nodes); + } +} diff --git a/app/3rd_party/markdown_core/lib/text_style.dart b/app/3rd_party/markdown_core/lib/text_style.dart new file mode 100644 index 0000000..fe090a7 --- /dev/null +++ b/app/3rd_party/markdown_core/lib/text_style.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +const List kTextTags = const [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'p', + 'code', + 'strong', + 'em', + 'del', + 'a', +]; + +const List kTextParentTags = const [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'p', + 'li', +]; + +TextStyle defaultTextStyle(BuildContext context) => TextStyle( + fontSize: 18, + height: 1.8, + color: Theme.of(context).brightness == Brightness.dark + ? const Color(0xffaaaaaa) + : const Color(0xff444444), + ); + +TextStyle defaultTagTextStyle(String lastTag, String tag, TextStyle textStyle) { + switch (tag) { + case 'h1': + textStyle = textStyle.copyWith( + fontSize: (textStyle.fontSize ?? 0) + 9, + ); + break; + case 'h2': + textStyle = textStyle.copyWith( + fontSize: (textStyle.fontSize ?? 0) + 6, + ); + break; + case 'h3': + textStyle = textStyle.copyWith( + fontSize: (textStyle.fontSize ?? 0) + 4, + ); + break; + case 'h4': + textStyle = textStyle.copyWith( + fontSize: (textStyle.fontSize ?? 0) + 3, + ); + break; + case 'h5': + textStyle = textStyle.copyWith( + fontSize: (textStyle.fontSize ?? 0) + 2, + ); + break; + case 'h6': + textStyle = textStyle.copyWith( + fontSize: (textStyle.fontSize ?? 0) + 1, + ); + break; + case 'p': + break; + case 'li': + break; + case 'code': + textStyle = textStyle.copyWith( + fontSize: (textStyle.fontSize ?? 0) - 3, + color: textStyle.color?.withAlpha(200), + ); + if (lastTag == 'p') { + textStyle = textStyle.copyWith( + color: Colors.red.shade800, + ); + } + + break; + case 'strong': + textStyle = textStyle.copyWith( + fontWeight: FontWeight.bold, + ); + break; + case 'em': + textStyle = textStyle.copyWith( + fontStyle: FontStyle.italic, + ); + break; + case 'del': + textStyle = textStyle.copyWith( + decoration: TextDecoration.lineThrough, + ); + break; + case 'a': + textStyle = textStyle.copyWith( + color: Colors.blue, + ); + break; + } + return textStyle; +} diff --git a/app/3rd_party/markdown_core/pubspec.yaml b/app/3rd_party/markdown_core/pubspec.yaml new file mode 100644 index 0000000..76579dc --- /dev/null +++ b/app/3rd_party/markdown_core/pubspec.yaml @@ -0,0 +1,56 @@ +name: markdown_core +description: Parse markdown and render it into rich text. +version: 1.0.0 +homepage: https://github.com/xia-weiyang/markdown_core +repository: https://github.com/xia-weiyang/markdown_core + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + flutter: + sdk: flutter + + markdown: ^4.0.1 + html_unescape: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/app/3rd_party/markdown_editor_ot/lib/src/preview.dart b/app/3rd_party/markdown_editor_ot/lib/src/preview.dart index b14babf..d93ca16 100644 --- a/app/3rd_party/markdown_editor_ot/lib/src/preview.dart +++ b/app/3rd_party/markdown_editor_ot/lib/src/preview.dart @@ -9,6 +9,7 @@ class MdPreview extends StatefulWidget { this.padding = const EdgeInsets.all(0.0), this.onTapLink, required this.widgetImage, + required this.onCodeCopied, this.textStyle, }) : super(key: key); @@ -17,6 +18,8 @@ class MdPreview extends StatefulWidget { final WidgetImage widgetImage; final TextStyle? textStyle; + final Function onCodeCopied; + /// Call this method when it tap link of markdown. /// If [onTapLink] is null,it will open the link with your default browser. final TapLinkCallback? onTapLink; @@ -46,6 +49,7 @@ class MdPreviewState extends State }, image: widget.widgetImage, textStyle: widget.textStyle, + onCodeCopied: widget.onCodeCopied, ); }, ), diff --git a/app/3rd_party/markdown_editor_ot/pubspec.yaml b/app/3rd_party/markdown_editor_ot/pubspec.yaml index 00f847b..253d1bd 100644 --- a/app/3rd_party/markdown_editor_ot/pubspec.yaml +++ b/app/3rd_party/markdown_editor_ot/pubspec.yaml @@ -11,7 +11,9 @@ dependencies: sdk: flutter shared_preferences: ^2.0.4 - markdown_core: ^1.0.0 + markdown_core: + path: + ../markdown_core dev_dependencies: flutter_test: diff --git a/app/lib/constants/constants.dart b/app/lib/constants/constants.dart index b57bd45..0a4ab78 100644 --- a/app/lib/constants/constants.dart +++ b/app/lib/constants/constants.dart @@ -1,10 +1,9 @@ import 'package:flutter/cupertino.dart'; /// [UOS设计指南](https://docs.uniontech.com/zh/content/t_dbG3kBK9iDf9B963ok) - const double localManagerPanelWidth = 260; -const double marketPanelWidth = 300; +const double marketOrMePanelWidth = 300; const shortDuration = const Duration(milliseconds: 100); @@ -39,5 +38,5 @@ const List builtInCommands = [ enum PanelType { local_manager, - market, + market_or_me, } diff --git a/app/lib/constants/sp_keys.dart b/app/lib/constants/sp_keys.dart index e3140a4..aac5a36 100644 --- a/app/lib/constants/sp_keys.dart +++ b/app/lib/constants/sp_keys.dart @@ -2,4 +2,7 @@ class SPKeys { static final String brightnessMode = 'BRIGHTNESS_MODE'; static final String appliedSchemeId = 'APPLIED_SCHEME_ID'; static final String userLanguage = 'USER_LANGUAGE'; + static final String accessToken = 'USER_ACCESS_TOKEN'; + static final String loginEmail = 'USER_LOGIN_EMAIL'; + static final String ignoredUpdateVersion = 'IGNORED_UPDATE_VERSION'; } diff --git a/app/lib/extensions/string_extension.dart b/app/lib/extensions/string_extension.dart index 6432f1b..8a9e5e3 100644 --- a/app/lib/extensions/string_extension.dart +++ b/app/lib/extensions/string_extension.dart @@ -1,3 +1,5 @@ extension StringNotNull on String? { bool get notNull => this != null && this != ''; + + bool get isNull => !notNull; } \ No newline at end of file diff --git a/app/lib/http/api.dart b/app/lib/http/api.dart new file mode 100644 index 0000000..44fcafa --- /dev/null +++ b/app/lib/http/api.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dde_gesture_manager/constants/sp_keys.dart'; +import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/utils/helper.dart'; +import 'package:dde_gesture_manager/utils/notificator.dart'; +import 'package:dde_gesture_manager_api/apis.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'package:http/http.dart' as http; + +typedef T BeanBuilder(Map res); + +typedef T HandleRespBuild(http.Response resp); + +getStatusCodeFunc(Map resp) => resp["statusCode"]; + +class HttpErrorCode extends Error { + int statusCode; + + HttpErrorCode(this.statusCode, {this.message}); + + String? message; + + @override + String toString() => '[$statusCode] $message'; +} + +class Api { + static _handleHttpError(e) { + if (e is SocketException) { + Notificator.error( + H().topContext, + title: LocaleKeys.info_server_error_title.tr(), + description: LocaleKeys.info_server_error_description.tr(), + ); + } else { + throw e; + } + } + + static HandleRespBuild _handleRespBuild(BeanBuilder builder) => (http.Response resp) { + if (builder == getStatusCodeFunc) return builder({"statusCode": resp.statusCode}); + T res; + try { + res = builder(json.decode(resp.body)); + } catch (e) { + throw HttpErrorCode(resp.statusCode, message: resp.body); + } + return res; + }; + + static Future _get( + String path, + BeanBuilder builder, { + Map? queryParams, + bool ignoreToken = false, + bool ignoreErrorHandle = false, + }) => + http + .get( + Uri( + scheme: Apis.apiScheme, + host: Apis.apiHost, + port: Apis.apiPort, + queryParameters: queryParams, + path: path, + ), + headers: { + HttpHeaders.contentTypeHeader: ContentType.json.value, + }..addAll( + ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}), + ) + .then( + _handleRespBuild(builder), + onError: (e) { + if (ignoreErrorHandle) + throw e; + else + return _handleHttpError(e); + }, + ); + + static Future _post( + String path, + BeanBuilder builder, { + Map? body, + bool ignoreToken = false, + bool ignoreErrorHandle = false, + }) => + http + .post( + Uri( + scheme: Apis.apiScheme, + host: Apis.apiHost, + port: Apis.apiPort, + path: path, + ), + body: jsonEncode(body), + headers: { + HttpHeaders.contentTypeHeader: ContentType.json.value, + }..addAll( + ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}), + ) + .then( + _handleRespBuild(builder), + onError: (e) { + if (ignoreErrorHandle) + throw e; + else + return _handleHttpError(e); + }, + ); + + static Future loginOrSignup({ + required String email, + required String password, + }) => + _post( + Apis.auth.loginOrSignup, + LoginSuccessSerializer.fromMap, + body: { + UserFields.email: email, + UserFields.password: password, + }, + ignoreToken: true, + ); + + static Future checkAppVersion({ignoreErrorHandle = false}) => _get( + Apis.system.appVersion, + AppVersionSerializer.fromMap, + ignoreToken: true, + ignoreErrorHandle: ignoreErrorHandle, + ); +} diff --git a/app/lib/models/configs.dart b/app/lib/models/configs.dart index 691756b..4b27e0e 100644 --- a/app/lib/models/configs.dart +++ b/app/lib/models/configs.dart @@ -1,5 +1,6 @@ import 'package:dde_gesture_manager/builder/provider_annotation.dart'; import 'package:dde_gesture_manager/constants/sp_keys.dart'; +import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; enum BrightnessMode { @@ -14,11 +15,49 @@ class Configs { BrightnessMode? brightnessMode; @ProviderModelProp() - String? appliedSchemeId; + String? get appliedSchemeId => _appliedSchemeId; + + set appliedSchemeId(String? schemeId) { + _appliedSchemeId = schemeId; + if (schemeId.notNull) + H().sp.updateString(SPKeys.appliedSchemeId, schemeId!); + else + H().sp.remove(SPKeys.appliedSchemeId); + } + + String? _appliedSchemeId; + + @ProviderModelProp() + String? get accessToken => _accessToken; + + set accessToken(String? token) { + _accessToken = token; + if (token.notNull) + H().sp.updateString(SPKeys.accessToken, token!); + else + H().sp.remove(SPKeys.accessToken); + } + + String? _accessToken; + + @ProviderModelProp() + String? get email => _email; + + set email(String? emailAddress) { + _email = emailAddress; + if (emailAddress.notNull) + H().sp.updateString(SPKeys.loginEmail, emailAddress!); + else + H().sp.remove(SPKeys.loginEmail); + } + + String? _email; Configs() { this.brightnessMode = BrightnessMode.values[H().sp.getInt(SPKeys.brightnessMode)?.clamp(0, BrightnessMode.values.length - 1) ?? 0]; this.appliedSchemeId = H().sp.getString(SPKeys.appliedSchemeId); + this.accessToken = H().sp.getString(SPKeys.accessToken); + this.email = H().sp.getString(SPKeys.loginEmail); } } diff --git a/app/lib/models/content_layout.dart b/app/lib/models/content_layout.dart index 4e11242..fed062f 100644 --- a/app/lib/models/content_layout.dart +++ b/app/lib/models/content_layout.dart @@ -6,5 +6,10 @@ class ContentLayout { bool? localManagerOpened; @ProviderModelProp() - bool? marketOpened; + bool? marketOrMeOpened; + + @ProviderModelProp() + bool? currentIsMarket = true; + + bool get isMarket => currentIsMarket ?? true; } diff --git a/app/lib/pages/content.dart b/app/lib/pages/content.dart index a674c94..5068b75 100644 --- a/app/lib/pages/content.dart +++ b/app/lib/pages/content.dart @@ -3,7 +3,7 @@ import 'package:dde_gesture_manager/models/content_layout.provider.dart'; import 'package:dde_gesture_manager/models/scheme.provider.dart'; import 'package:dde_gesture_manager/pages/gesture_editor.dart'; import 'package:dde_gesture_manager/pages/local_manager.dart'; -import 'package:dde_gesture_manager/pages/market.dart'; +import 'package:dde_gesture_manager/pages/market_or_me.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:flutter/material.dart'; @@ -31,7 +31,7 @@ class _ContentState extends State { ChangeNotifierProvider( create: (context) => ContentLayoutProvider() ..localManagerOpened = preferredPanelsStatus.localManagerPanelOpened - ..marketOpened = preferredPanelsStatus.marketPanelOpened, + ..marketOrMeOpened = preferredPanelsStatus.marketOrMePanelOpened, ), ChangeNotifierProvider( create: (context) => CopiedGesturePropProvider.empty(), @@ -42,7 +42,7 @@ class _ContentState extends State { Future.microtask( () => context.read().setProps( localManagerOpened: preferredPanelsStatus.localManagerPanelOpened, - marketOpened: preferredPanelsStatus.marketPanelOpened, + marketOrMeOpened: preferredPanelsStatus.marketOrMePanelOpened, ), ); } @@ -52,7 +52,7 @@ class _ContentState extends State { children: [ LocalManager(), GestureEditor(), - Market(), + MarketOrMe(), ], ); }, diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index aeb17c9..537b42d 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -68,12 +68,12 @@ class GestureEditor extends StatelessWidget { ), ).tr(), Visibility( - visible: layoutProvider.marketOpened == false, + visible: layoutProvider.marketOrMeOpened == false, child: DButton( width: defaultButtonHeight, - onTap: () => H.openPanel(context, PanelType.market), + onTap: () => H.openPanel(context, PanelType.market_or_me), child: Icon( - CupertinoIcons.cart, + layoutProvider.isMarket ? CupertinoIcons.cart : CupertinoIcons.person, ), ), ), diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index 72d2252..14276bd 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; -import 'package:dde_gesture_manager/constants/sp_keys.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/content_layout.provider.dart'; @@ -9,7 +8,6 @@ import 'package:dde_gesture_manager/models/local_schemes_provider.dart'; import 'package:dde_gesture_manager/models/scheme.dart'; import 'package:dde_gesture_manager/models/scheme.provider.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; -import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -247,7 +245,6 @@ class _LocalManagerState extends State { var appliedId = localSchemes.firstWhere((ele) => ele.path == _selectedItemPath).scheme.id!; appliedId.sout(); - H().sp.updateString(SPKeys.appliedSchemeId, appliedId); context.read().setProps(appliedSchemeId: appliedId); }, ), diff --git a/app/lib/pages/market.dart b/app/lib/pages/market.dart deleted file mode 100644 index ef448c4..0000000 --- a/app/lib/pages/market.dart +++ /dev/null @@ -1,65 +0,0 @@ -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/widgets/dde_button.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class Market extends StatelessWidget { - const Market({ - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - var isOpen = context.watch().marketOpened == true; - return AnimatedContainer( - duration: mediumDuration, - curve: Curves.easeInOut, - width: isOpen ? marketPanelWidth : 0, - child: OverflowBox( - alignment: Alignment.centerLeft, - maxWidth: marketPanelWidth, - minWidth: marketPanelWidth, - child: Material( - color: context.t.backgroundColor, - elevation: isOpen ? 10 : 0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - DButton( - width: defaultButtonHeight - 2, - height: defaultButtonHeight - 2, - onTap: () => context.read().setProps(marketOpened: !isOpen), - child: Icon( - CupertinoIcons.chevron_right_2, - size: 20, - ), - ), - Flexible( - child: Center( - child: Text( - LocaleKeys.market_title, - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - ), - Container(width: defaultButtonHeight), - ], - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/app/lib/pages/market_or_me.dart b/app/lib/pages/market_or_me.dart new file mode 100644 index 0000000..e769260 --- /dev/null +++ b/app/lib/pages/market_or_me.dart @@ -0,0 +1,117 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/models/configs.provider.dart'; +import 'package:dde_gesture_manager/models/content_layout.provider.dart'; +import 'package:dde_gesture_manager/widgets/dde_button.dart'; +import 'package:dde_gesture_manager/widgets/login.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class MarketOrMe extends StatelessWidget { + const MarketOrMe({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var layoutProvider = context.watch(); + bool isOpen = layoutProvider.marketOrMeOpened == true; + bool isMarket = layoutProvider.isMarket; + bool showLogin = context.watch().accessToken.isNull && !isMarket; + return AnimatedContainer( + duration: mediumDuration, + curve: Curves.easeInOut, + width: isOpen ? marketOrMePanelWidth * (showLogin ? 1.5 : 1) : 0, + child: OverflowBox( + alignment: Alignment.centerLeft, + maxWidth: marketOrMePanelWidth * (showLogin ? 1.5 : 1), + minWidth: marketOrMePanelWidth * (showLogin ? 1.5 : 1), + child: Material( + color: context.t.backgroundColor, + elevation: isOpen ? 10 : 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DButton( + width: defaultButtonHeight - 2, + height: defaultButtonHeight - 2, + onTap: () => context.read().setProps(marketOrMeOpened: !isOpen), + child: Icon( + CupertinoIcons.chevron_right_2, + size: 20, + ), + ), + Flexible( + child: Center( + child: Text( + isMarket ? LocaleKeys.market_title : LocaleKeys.me_title, + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + ), + DButton( + width: defaultButtonHeight - 2, + height: defaultButtonHeight - 2, + onTap: () => context.read().setProps(currentIsMarket: !isMarket), + child: Icon( + !isMarket ? CupertinoIcons.cart : CupertinoIcons.person, + size: 20, + ), + ), + ], + ), + if (isMarket) buildMarketContent(context), + if (!isMarket) buildMeContent(context), + ], + ), + ), + ), + ), + ); + } + + Widget buildMeContent(BuildContext context) { + var accessToken = context.watch().accessToken; + if (accessToken.isNull) return LoginWidget(); + + return Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon(Icons.person, size: defaultButtonHeight), + Flexible( + child: AutoSizeText( + context.watch().email ?? '', + style: TextStyle( + fontSize: 18, + ), + maxLines: 1, + ), + ), + DButton.logout( + enabled: true, + onTap: () => context.read().setProps(accessToken: '', email: ''), + ), + ], + ), + ], + ), + ); + } + + Widget buildMarketContent(BuildContext context) { + return Container(); + } +} diff --git a/app/lib/utils/helper.dart b/app/lib/utils/helper.dart index 61350dd..88c9b19 100644 --- a/app/lib/utils/helper.dart +++ b/app/lib/utils/helper.dart @@ -29,34 +29,42 @@ class H { initSharedPreference() async { _sp = await SharedPreferences.getInstance(); } + + late BuildContext _topContext; + + BuildContext get topContext => _topContext; + + initTopContext(BuildContext context) { + _topContext = context; + } static void openPanel(BuildContext context, PanelType panelType) { var windowWidth = MediaQuery.of(context).size.width; - if (windowWidth < minWindowSize.width + localManagerPanelWidth + marketPanelWidth) { + if (windowWidth < minWindowSize.width + localManagerPanelWidth + marketOrMePanelWidth) { context.read().setProps( localManagerOpened: panelType == PanelType.local_manager, - marketOpened: panelType == PanelType.market, + marketOrMeOpened: panelType == PanelType.market_or_me, ); } else { switch (panelType) { case PanelType.local_manager: return context.read().setProps(localManagerOpened: true); - case PanelType.market: - return context.read().setProps(marketOpened: true); + case PanelType.market_or_me: + return context.read().setProps(marketOrMeOpened: true); } } } static PreferredPanelsStatus getPreferredPanelsStatus(double windowWidth) { - var preferredPanelsStatus = PreferredPanelsStatus(localManagerPanelOpened: true, marketPanelOpened: true); - if (windowWidth > minWindowSize.width + localManagerPanelWidth + marketPanelWidth) + var preferredPanelsStatus = PreferredPanelsStatus(localManagerPanelOpened: true, marketOrMePanelOpened: true); + if (windowWidth > minWindowSize.width + localManagerPanelWidth + marketOrMePanelWidth) return preferredPanelsStatus; else if (windowWidth < minWindowSize.width + localManagerPanelWidth) return preferredPanelsStatus - ..marketPanelOpened = false + ..marketOrMePanelOpened = false ..localManagerPanelOpened = false; else - return preferredPanelsStatus..marketPanelOpened = false; + return preferredPanelsStatus..marketOrMePanelOpened = false; } static Gesture getGestureByName(String gestureName) => Gesture.values.findByName(gestureName) ?? Gesture.swipe; @@ -108,15 +116,15 @@ class H { class PreferredPanelsStatus { bool localManagerPanelOpened; - bool marketPanelOpened; + bool marketOrMePanelOpened; PreferredPanelsStatus({ required this.localManagerPanelOpened, - required this.marketPanelOpened, + required this.marketOrMePanelOpened, }); @override String toString() { - return 'PreferredPanelsStatus{localManagerPanelOpened: $localManagerPanelOpened, marketPanelOpened: $marketPanelOpened}'; + return 'PreferredPanelsStatus{localManagerPanelOpened: $localManagerPanelOpened, marketOrMePanelOpened: $marketOrMePanelOpened}'; } } diff --git a/app/lib/utils/init_linux.dart b/app/lib/utils/init_linux.dart index 7a553fe..d650cce 100644 --- a/app/lib/utils/init_linux.dart +++ b/app/lib/utils/init_linux.dart @@ -3,13 +3,22 @@ import 'package:dde_gesture_manager/constants/sp_keys.dart'; import 'package:dde_gesture_manager/constants/supported_locales.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/generated/codegen_loader.g.dart'; +import 'package:dde_gesture_manager/http/api.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; +import 'package:dde_gesture_manager/utils/notificator.dart'; +import 'package:dde_gesture_manager_api/apis.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; import 'package:gsettings/gsettings.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; +bool _updateChecked = false; + Future initEvents(BuildContext context) async { + H().initTopContext(context); var isDark = MediaQuery.of(context).platformBrightness == Brightness.dark; if (isDark) { context.read().setProps(isDarkMode: isDark); @@ -39,6 +48,32 @@ Future initEvents(BuildContext context) async { }); } } + + if (!_updateChecked) + Api.checkAppVersion(ignoreErrorHandle: true).then((value) async { + _updateChecked = true; + var info = await PackageInfo.fromPlatform(); + var _buildNumber = int.parse(info.buildNumber); + var _newVersionCode = value?.versionCode ?? 0; + var _ignoredVersionCode = H().sp.getInt(SPKeys.ignoredUpdateVersion) ?? 0; + if (_buildNumber < _newVersionCode && _ignoredVersionCode < _newVersionCode) { + Notificator.showConfirm( + title: LocaleKeys.info_new_version_title.tr(namedArgs: {'version': '${value?.versionName}'}), + description: LocaleKeys.info_new_version_description_for_startup.tr(namedArgs: { + 'yes': LocaleKeys.str_yes.tr(), + 'no': LocaleKeys.str_no.tr(), + }), + ).then((confirmed) async { + if (confirmed == CustomButton.positiveButton) { + if (await canLaunch(Apis.appNewVersionUrl)) { + await launch(Apis.appNewVersionUrl); + } + } else if (confirmed == CustomButton.negativeButton) { + H().sp.updateInt(SPKeys.ignoredUpdateVersion, value?.versionCode ?? 0); + } + }); + } + }); } Future initConfigs() async { diff --git a/app/lib/utils/init_web.dart b/app/lib/utils/init_web.dart index 8fe0d79..fd3335c 100644 --- a/app/lib/utils/init_web.dart +++ b/app/lib/utils/init_web.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; Future initEvents(BuildContext context) async { + H().initTopContext(context); var isDark = MediaQuery.of(context).platformBrightness == Brightness.dark; context.read().setProps(isDarkMode: isDark); } diff --git a/app/lib/utils/notificator.dart b/app/lib/utils/notificator.dart index 3ef808d..4872c46 100644 --- a/app/lib/utils/notificator.dart +++ b/app/lib/utils/notificator.dart @@ -1,5 +1,6 @@ import 'package:cherry_toast/cherry_toast.dart'; import 'package:cherry_toast/resources/arrays.dart'; +import 'package:dde_gesture_manager/extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_alert/flutter_platform_alert.dart'; @@ -27,8 +28,8 @@ class Notificator { return AlertImpl().showConfirm( windowTitle: title, text: description, - positiveButtonTitle: positiveButtonTitle, - negativeButtonTitle: negativeButtonTitle, + positiveButtonTitle: positiveButtonTitle ?? LocaleKeys.str_yes.tr(), + negativeButtonTitle: negativeButtonTitle ?? LocaleKeys.str_no.tr(), ); } diff --git a/app/lib/widgets/dde_button.dart b/app/lib/widgets/dde_button.dart index 7aad5ad..1b94fb9 100644 --- a/app/lib/widgets/dde_button.dart +++ b/app/lib/widgets/dde_button.dart @@ -107,6 +107,23 @@ class DButton extends StatefulWidget { message: LocaleKeys.operation_paste.tr(), )); + factory DButton.logout({ + Key? key, + required enabled, + GestureTapCallback? onTap, + height = defaultButtonHeight, + width = defaultButtonHeight, + }) => + DButton( + key: key, + width: width, + height: height, + onTap: enabled ? onTap : null, + child: Tooltip( + child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.logout_rounded, size: 20)), + message: LocaleKeys.operation_logout.tr(), + )); + factory DButton.dropdown({ Key? key, width = 60.0, diff --git a/app/lib/widgets/dde_markdown_field.dart b/app/lib/widgets/dde_markdown_field.dart index 46f6c7a..cdf6b8d 100644 --- a/app/lib/widgets/dde_markdown_field.dart +++ b/app/lib/widgets/dde_markdown_field.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; +import 'package:dde_gesture_manager/utils/notificator.dart'; import 'package:flutter/material.dart'; import 'package:markdown_editor_ot/markdown_editor.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -77,11 +78,18 @@ class _DMarkdownFieldState extends State { }); }, child: MouseRegion( - cursor: widget.readOnly ? SystemMouseCursors.basic : SystemMouseCursors.click, + cursor: widget.readOnly ? SystemMouseCursors.basic : SystemMouseCursors.text, child: MdPreview( text: _previewText ?? '', padding: EdgeInsets.only(left: 15), onTapLink: _launchURL, + onCodeCopied: () { + Notificator.success( + context, + title: LocaleKeys.info_code_copied_titte.tr(), + description: LocaleKeys.info_code_copied_description.tr(), + ); + }, widgetImage: (imageUrl) => CachedNetworkImage( imageUrl: imageUrl, placeholder: (context, url) => const SizedBox( diff --git a/app/lib/widgets/login.dart b/app/lib/widgets/login.dart new file mode 100644 index 0000000..20e1386 --- /dev/null +++ b/app/lib/widgets/login.dart @@ -0,0 +1,102 @@ +import 'dart:io'; + +import 'package:dde_gesture_manager/constants/sp_keys.dart'; +import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/http/api.dart'; +import 'package:dde_gesture_manager/models/configs.provider.dart'; +import 'package:dde_gesture_manager/models/settings.provider.dart'; +import 'package:dde_gesture_manager/utils/helper.dart'; +import 'package:dde_gesture_manager/utils/notificator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_login/flutter_login.dart'; + +class LoginWidget extends StatefulWidget { + const LoginWidget({ + Key? key, + }) : super(key: key); + + @override + _LoginWidgetState createState() => _LoginWidgetState(); +} + +class _LoginWidgetState extends State { + ValueKey _key = ValueKey(0); + + @override + Widget build(BuildContext context) { + return Expanded( + child: OverflowBox( + alignment: Alignment.topCenter, + child: Container( + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith( + physics: const NeverScrollableScrollPhysics(), + ), + child: FlutterLogin( + key: _key, + onLogin: (loginData) async { + try { + var res = await Api.loginOrSignup(email: loginData.name, password: loginData.password); + if (res != null && res.token.notNull) + context.read().setProps(accessToken: res.token, email: loginData.name); + } catch (e) { + if (!(e is HttpErrorCode)) return; + var code = e.statusCode; + if (code == HttpStatus.unauthorized) + Notificator.error( + context, + title: LocaleKeys.info_login_failed_title.tr(), + description: LocaleKeys.info_login_failed_description.tr(), + ); + else if (code == HttpStatus.notFound) + Notificator.info( + context, + title: LocaleKeys.info_sign_up_hint_title.tr(), + description: LocaleKeys.info_sign_up_hint_description.tr(), + ); + else + throw e; + } + }, + onSubmitAnimationCompleted: () { + var token = H().sp.getString(SPKeys.accessToken); + if (token.isNull) + setState(() { + _key = ValueKey(_key.value + 1); + }); + else if (context.read().accessToken != token) + context + .read() + .setProps(accessToken: token, email: H().sp.getString(SPKeys.loginEmail)); + }, + onRecoverPassword: (_) {}, + hideForgotPasswordButton: true, + disableCustomPageTransformer: true, + messages: LoginMessages( + userHint: LocaleKeys.me_login_email_hint.tr(), + passwordHint: LocaleKeys.me_login_password_hint.tr(), + loginButton: LocaleKeys.me_login_login_or_signup.tr(), + ), + userValidator: (value) { + if (FlutterLogin.defaultEmailValidator(value) != null) { + return LocaleKeys.me_login_email_error_hint.tr(); + } + }, + passwordValidator: (value) { + if (value!.isEmpty || value.length < 8 || value.length > 16) { + return LocaleKeys.me_login_password_hint.tr(); + } + }, + theme: LoginTheme( + pageColorDark: Colors.transparent, + pageColorLight: Colors.transparent, + primaryColor: context.watch().currentActiveColor, + footerBackgroundColor: Colors.transparent, + ), + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/widgets/version_checker.dart b/app/lib/widgets/version_checker.dart index 7e39321..f9a0a3f 100644 --- a/app/lib/widgets/version_checker.dart +++ b/app/lib/widgets/version_checker.dart @@ -1,11 +1,12 @@ -import 'dart:convert'; - import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/http/api.dart'; +import 'package:dde_gesture_manager/utils/notificator.dart'; +import 'package:dde_gesture_manager_api/apis.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:http/http.dart' as http; -import 'package:dde_gesture_manager_api/apis.dart'; -import 'package:dde_gesture_manager_api/models.dart'; +import 'package:url_launcher/url_launcher.dart'; class VersionChecker extends StatelessWidget { const VersionChecker({Key? key}) : super(key: key); @@ -22,20 +23,31 @@ class VersionChecker extends StatelessWidget { Text( '${LocaleKeys.version_current.tr()} : ${snapshot.data?.version ?? ''}', ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3), - child: TextButton( - child: Text(LocaleKeys.version_check_update).tr(), - onPressed: () { - http.get(Uri.parse('http://127.0.0.1:3000' + Apis.system.appVersion)).then((value) { - var appVersion = AppVersionSerializer.fromMap(json.decode(value.body)); - appVersion.versionName.sout(); - appVersion.versionCode.sout(); - appVersion.sout(); - }); - }, + if (!kIsWeb) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: TextButton( + child: Text(LocaleKeys.version_check_update).tr(), + onPressed: () { + Api.checkAppVersion().then((value) { + if (value != null && (value.versionCode ?? 0) > int.parse(snapshot.data?.buildNumber ?? '0')) { + Notificator.showConfirm( + title: LocaleKeys.info_new_version_title.tr(namedArgs: {'version': '${value.versionName}'}), + description: LocaleKeys.info_new_version_description_for_manual.tr(), + ).then((value) async { + if (value == CustomButton.positiveButton) { + if (await canLaunch(Apis.appNewVersionUrl)) { + await launch(Apis.appNewVersionUrl); + } + } + }); + } else { + Notificator.info(context, title: LocaleKeys.info_new_version_title_already_latest.tr()); + } + }); + }, + ), ), - ), ], ), ); diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 11a3699..b774f56 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -40,6 +40,8 @@ dependencies: flutter_platform_alert: ^0.2.1 cached_network_image: ^3.2.0 url_launcher: ^6.0.17 + flutter_login: ^3.1.0 + auto_size_text: ^3.0.0 markdown_editor_ot: path: 3rd_party/markdown_editor_ot cherry_toast: diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index 105a844..bcd5583 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -70,12 +70,15 @@ "delete": "delete", "duplicate": "duplicate", "apply": "apply", - "paste": "paste" + "paste": "paste", + "logout": "sign out" }, "str": { "null": "Null", "new_scheme": "New gesture scheme", - "copy": "copy" + "copy": "copy", + "yes": "Yes", + "no": "No" }, "built_in_commands": { "ShowWorkspace": "ShowWorkspace", @@ -101,6 +104,37 @@ "scheme_name_conflict": { "title": "Save failed!", "description": "Scheme name conflict, please rename it!" + }, + "code_copied": { + "titte": "Code has been copied!", + "description": "Please paste it into the terminal and run ~" + }, + "server_error": { + "title": "Server connection error", + "description": "Please try again later ~" + }, + "login_failed": { + "title": "Login failed", + "description": "Please check the password ~" + }, + "sign_up_hint": { + "title": "Verification code has been sent", + "description": "Please log in to the mailbox to continue the registration ~" + }, + "new_version": { + "title": "Update found(v{version})", + "description_for_startup": "Click [{YES}] to view, click [{NO}] ignore this update", + "title_already_latest": "Already the latest version ~", + "description_for_manual": "Visit the official website to see more?" + } + }, + "me": { + "title": "Me", + "login": { + "login_or_signup": "Login/Signup", + "email_hint": "Please enter email", + "password_hint": "Please enter 8-16-bit password", + "email_error_hint": "Please enter your vaild email" } } } \ No newline at end of file diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index f4be855..3832629 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -70,12 +70,15 @@ "delete": "删除", "duplicate": "复制", "apply": "应用", - "paste": "粘贴" + "paste": "粘贴", + "logout": "退出登录" }, "str": { "null": "无", "new_scheme": "新建手势方案", - "copy": "副本" + "copy": "副本", + "yes": "是", + "no": "否" }, "built_in_commands": { "ShowWorkspace": "显示工作区", @@ -101,6 +104,37 @@ "scheme_name_conflict": { "title": "保存失败!", "description": "方案名冲突,请重新命名!" + }, + "code_copied": { + "titte": "代码已复制!", + "description": "请粘贴到终端中执行~" + }, + "server_error": { + "title": "连接服务器异常", + "description": "请稍后再试~" + }, + "login_failed": { + "title": "登录失败", + "description": "请检查账号密码是否正确~" + }, + "sign_up_hint": { + "title": "验证码已发送", + "description": "请登录邮箱继续完成注册~" + }, + "new_version": { + "title": "发现新版本(v{version})", + "description_for_startup": "点击[{yes}]查看,点击[{no}]忽略本次更新", + "title_already_latest": "已经是最新版本~", + "description_for_manual": "是否前去官网查看?" + } + }, + "me": { + "title": "我的", + "login": { + "login_or_signup": "登录/注册", + "email_hint": "请输入邮箱", + "password_hint": "请输入8-16位密码", + "email_error_hint": "请输入正确的邮箱" } } } \ No newline at end of file diff --git a/app/source_gen.sh b/app/source_gen.sh index 4ec7e73..9c60bc8 100755 --- a/app/source_gen.sh +++ b/app/source_gen.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash flutter packages pub get -flutter packages pub run build_runner build --delete-conflicting-outputs flutter pub run easy_localization:generate -flutter pub run easy_localization:generate -f keys -o locale_keys.g.dart \ No newline at end of file +flutter pub run easy_localization:generate -f keys -o locale_keys.g.dart +flutter packages pub run build_runner build --delete-conflicting-outputs -- 2.11.0