diff --git a/api/lib/channel.dart b/api/lib/channel.dart index 8a95282..2eeca45 100644 --- a/api/lib/channel.dart +++ b/api/lib/channel.dart @@ -1,5 +1,5 @@ -import 'package:dde_gesture_manager/model/model.dart'; import 'package:dde_gesture_manager/dde_gesture_manager.dart'; +import 'package:dde_gesture_manager/model/model.dart'; /// This type initializes an application. /// diff --git a/api/test/harness/app.dart b/api/test/harness/app.dart index 029207a..a78b76f 100644 --- a/api/test/harness/app.dart +++ b/api/test/harness/app.dart @@ -1,10 +1,10 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; import 'package:conduit_test/conduit_test.dart'; +import 'package:dde_gesture_manager/dde_gesture_manager.dart'; -export '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'; -export 'package:conduit/conduit.dart'; /// A testing harness for dde_gesture_manager. /// diff --git a/app/README.md b/app/README.md index d446fd0..a7411e9 100644 --- a/app/README.md +++ b/app/README.md @@ -1,16 +1,55 @@ -# dde_gesture_manager +# DDE Gesture Manager +专为 DDE 桌面环境打造的触摸板手势管理工具 -A new Flutter project. +## ProviderGenerator +利用 [source_gen](https://pub.dev/packages/source_gen) 和 [build_runner](https://pub.flutter-io.cn/packages/build_runner) 生成 [provider](https://pub.flutter-io.cn/packages/provider) 的模板代码: +1. 在 `lib/models/` 下编写模型类 +```dart +import 'package:dde_gesture_manager/builder/provider_annotation.dart'; -## Getting Started +@ProviderModel() +class Test { + @ProviderModelProp() + bool? tested; -This project is a starting point for a Flutter application. + @ProviderModelProp() + String? name; +} -A few resources to get you started if this is your first Flutter project: +``` -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) +2. `app` 项目目录下执行 `flutter packages pub get && flutter packages pub run build_runner build --delete-conflicting-outputs` -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +3. 将在 `lib/models/test.provider.dart` 生成如下代码: +```dart +import 'package:flutter/foundation.dart'; +import 'package:dde_gesture_manager/extensions/compare_extension.dart'; +import 'test.dart'; + +class TestProvider extends Test with ChangeNotifier { + void setProps({ + bool? tested, + String? name, + }) { + bool changed = false; + if (tested.diff(this.tested)) { + this.tested = tested; + changed = true; + } + if (name.diff(this.name)) { + this.name = name; + changed = true; + } + if (changed) notifyListeners(); + } +} + +``` + +## easy_localization +### 生成资源代码 +`flutter pub run easy_localization:generate && flutter pub run easy_localization:generate -f keys -o locale_keys.g.dart` + +### 已经支持语言 +- 简体中文(zh-CN) +- English(en) diff --git a/app/build.yaml b/app/build.yaml new file mode 100644 index 0000000..710187a --- /dev/null +++ b/app/build.yaml @@ -0,0 +1,11 @@ +builders: + provider_builder: + import: 'lib/builder/provider_builder.dart' + builder_factories: [ 'providerBuilder' ] + build_extensions: { '.dart': [ '.provider.dart' ] } + auto_apply: root_package + build_to: source + defaults: + generate_for: + include: + - lib/models/** diff --git a/app/lib/builder/provider_annotation.dart b/app/lib/builder/provider_annotation.dart new file mode 100644 index 0000000..3f66ed0 --- /dev/null +++ b/app/lib/builder/provider_annotation.dart @@ -0,0 +1,9 @@ +class ProviderModel { + const ProviderModel(); +} + +class ProviderModelProp { + const ProviderModelProp({this.nullable = true}); + + final bool nullable; +} diff --git a/app/lib/builder/provider_builder.dart b/app/lib/builder/provider_builder.dart new file mode 100644 index 0000000..42f92e8 --- /dev/null +++ b/app/lib/builder/provider_builder.dart @@ -0,0 +1,6 @@ +import 'package:dde_gesture_manager/builder/provider_generator.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:build/build.dart'; + +Builder providerBuilder(BuilderOptions options) => + LibraryBuilder(ProviderGenerator(), generatedExtension: '.provider.dart'); diff --git a/app/lib/builder/provider_generator.dart b/app/lib/builder/provider_generator.dart new file mode 100644 index 0000000..14fc6bc --- /dev/null +++ b/app/lib/builder/provider_generator.dart @@ -0,0 +1,48 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/src/builder/build_step.dart'; +import 'package:dde_gesture_manager/builder/provider_annotation.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:collection/collection.dart'; + +class AnnotationField { + String name; + String type; + + AnnotationField(this.name, this.type); +} + +class ProviderGenerator extends GeneratorForAnnotation { + @override + generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { + var className = (element as ClassElement).source.shortName; + List fields = []; + element.fields.forEach((field) { + var annotation = field.metadata.firstWhereOrNull( + (m) => m.computeConstantValue()?.type?.getDisplayString(withNullability: true) == 'ProviderModelProp'); + if (annotation != null) + fields.add( + AnnotationField( + field.displayName, + field.type.getDisplayString( + withNullability: annotation.computeConstantValue()?.getField('nullable')?.toBoolValue() ?? true, + ), + ), + ); + }); + return ''' +import 'package:flutter/foundation.dart'; +import 'package:dde_gesture_manager/extensions/compare_extension.dart'; +import '$className'; + +class ${element.displayName}Provider extends ${element.displayName} with ChangeNotifier { + void setProps({ + ${fields.map((f) => '${f.type.endsWith('?') ? '' : 'required '}${f.type} ${f.name},').join('\n')} + }) { + bool changed = false; + ${fields.map((f) => 'if (${f.name}.diff(this.${f.name})) {this.${f.name} = ${f.name}; changed = true; }').join('\n')} + if (changed) notifyListeners(); + } +} + '''; + } +} diff --git a/app/lib/constants/sp_keys.dart b/app/lib/constants/sp_keys.dart new file mode 100644 index 0000000..5b23928 --- /dev/null +++ b/app/lib/constants/sp_keys.dart @@ -0,0 +1,4 @@ +class SPKeys { + static final String brightnessMode = 'BRIGHTNESS_MODE'; + static final String userLanguage = 'USER_LANGUAGE'; +} \ No newline at end of file diff --git a/app/lib/constants/supported_locales.dart b/app/lib/constants/supported_locales.dart new file mode 100644 index 0000000..0897879 --- /dev/null +++ b/app/lib/constants/supported_locales.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +enum SupportedLocale { + zh_CN, + en, +} + +const zh_CN = Locale('zh', 'CN'); +const en = Locale('en'); + +const supportedLocales = [ + zh_CN, + en, +]; + +const supportedLocaleNames = { + SupportedLocale.zh_CN: '简体中文', + SupportedLocale.en: 'English', +}; + +Locale getSupportedLocale(SupportedLocale supportedLocale) => supportedLocales[supportedLocale.index]; + diff --git a/app/lib/extensions.dart b/app/lib/extensions.dart new file mode 100644 index 0000000..4d66350 --- /dev/null +++ b/app/lib/extensions.dart @@ -0,0 +1,11 @@ +library extensions; + +export 'package:dde_gesture_manager/extensions/sout_extension.dart'; +export 'package:dde_gesture_manager/extensions/string_extension.dart'; +export 'package:dde_gesture_manager/extensions/context_extension.dart'; +export 'package:dde_gesture_manager/extensions/shared_preferences_extension.dart'; + +export 'package:dde_gesture_manager/generated/locale_keys.g.dart'; +export 'package:easy_localization/easy_localization.dart'; + +export 'package:provider/provider.dart'; \ No newline at end of file diff --git a/app/lib/extensions/compare_extension.dart b/app/lib/extensions/compare_extension.dart new file mode 100644 index 0000000..7d54d64 --- /dev/null +++ b/app/lib/extensions/compare_extension.dart @@ -0,0 +1,3 @@ +extension CompareExtension on Object? { + bool diff(other) => this != null && this != other; +} diff --git a/app/lib/extensions/context_extension.dart b/app/lib/extensions/context_extension.dart new file mode 100644 index 0000000..5607fb6 --- /dev/null +++ b/app/lib/extensions/context_extension.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +extension ContextExtension on BuildContext { + ThemeData get t => Theme.of(this); + + NavigatorState get n => Navigator.of(this); +} diff --git a/app/lib/extensions/shared_preferences_extension.dart b/app/lib/extensions/shared_preferences_extension.dart new file mode 100644 index 0000000..b8a0d27 --- /dev/null +++ b/app/lib/extensions/shared_preferences_extension.dart @@ -0,0 +1,13 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +extension SharedPreferencesExtenstion on SharedPreferences { + Future updateInt(String key, int value) { + if (this.getInt(key) == value) return Future.value(false); + return this.setInt(key, value); + } + + Future updateString(String key, String value) { + if (this.getString(key) == value) return Future.value(false); + return this.setString(key, value); + } +} diff --git a/app/lib/extensions/sout_extension.dart b/app/lib/extensions/sout_extension.dart new file mode 100644 index 0000000..c1ad104 --- /dev/null +++ b/app/lib/extensions/sout_extension.dart @@ -0,0 +1,12 @@ +extension SoutExtension on Object? { + void sout() { + switch (this.runtimeType) { + case String: + return print(this); + case Null: + return print(null); + default: + return print(this.toString()); + } + } +} diff --git a/app/lib/extensions/string_extension.dart b/app/lib/extensions/string_extension.dart new file mode 100644 index 0000000..6432f1b --- /dev/null +++ b/app/lib/extensions/string_extension.dart @@ -0,0 +1,3 @@ +extension StringNotNull on String? { + bool get notNull => this != null && this != ''; +} \ No newline at end of file diff --git a/app/lib/main.dart b/app/lib/main.dart index 576db6d..702852e 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,97 +1,79 @@ -import 'package:flutter/foundation.dart'; +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'; +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:gsettings/gsettings.dart'; -import 'package:window_manager/window_manager.dart'; -import 'package:xdg_directories/xdg_directories.dart' as xdgDir; +import 'package:flutter/widgets.dart'; + +import 'pages/home.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - if (!kIsWeb) { - print(await xdgDir.configHome); - print(await xdgDir.cacheHome); - print(await xdgDir.dataHome); - print('------'); - print(await xdgDir.configDirs.join('\n')); - print('------'); - print(await xdgDir.dataDirs.join('\n')); - print('------'); - print(await xdgDir.runtimeDir); - print('------'); - var windowManager = WindowManager.instance; - windowManager.setTitle('Gesture Manager For DDE'); - windowManager.setMinimumSize(const Size(800, 600)); - var xsettings = GSettings('com.deepin.xsettings'); - // xsettings.get('scale-factor').then((value) { - // print(value.toString()); - // }); - xsettings.get('theme-name').then((value) { - print(value.toString()); - }); - xsettings.keysChanged.listen((event) { - xsettings.get('theme-name').then((value) { - print(value.toString()); - }); - }); - } - runApp(MyApp()); + EasyLocalization.logger.enableLevels = []; + await EasyLocalization.ensureInitialized(); + await initConfigs(); + runApp(EasyLocalization( + supportedLocales: supportedLocales, + fallbackLocale: zh_CN, + path: 'resources/langs', + assetLoader: CodegenLoader(), + child: MyApp(), + )); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => SettingsProvider()), + ChangeNotifierProvider(create: (context) => ConfigsProvider()), + ], + builder: (context, child) { + var isDarkMode = context.watch().isDarkMode; + var brightnessMode = context.watch().brightnessMode; + H().sp.updateInt(SPKeys.brightnessMode, brightnessMode?.index ?? 0); + late bool showDarkMode; + if (brightnessMode == BrightnessMode.system) { + showDarkMode = isDarkMode ?? false; + } else { + showDarkMode = brightnessMode == BrightnessMode.dark; + } + return MaterialApp( + title: CodegenLoader.mapLocales[context.locale.toString()]?[LocaleKeys.app_name], + theme: showDarkMode ? darkTheme : lightTheme, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + home: AnimatedCrossFade( + crossFadeState: isDarkMode != null ? CrossFadeState.showSecond : CrossFadeState.showFirst, + alignment: Alignment.center, + layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) => Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Positioned(key: bottomChildKey, child: bottomChild), + Positioned(key: topChildKey, child: topChild), + ], ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: Icon(Icons.add), - ), + firstChild: Builder(builder: (context) { + Future.microtask(() => initEvents(context)); + return Container(); + }), + secondChild: HomePage(), + duration: Duration(milliseconds: 500), + ), + ); + }, ); } } diff --git a/app/lib/models/configs.dart b/app/lib/models/configs.dart new file mode 100644 index 0000000..7df082f --- /dev/null +++ b/app/lib/models/configs.dart @@ -0,0 +1,20 @@ +import 'package:dde_gesture_manager/builder/provider_annotation.dart'; +import 'package:dde_gesture_manager/constants/sp_keys.dart'; +import 'package:dde_gesture_manager/utils/helper.dart'; + +enum BrightnessMode { + system, + light, + dark, +} + +@ProviderModel() +class Configs { + @ProviderModelProp() + BrightnessMode? brightnessMode; + + Configs() { + this.brightnessMode = + BrightnessMode.values[H().sp.getInt(SPKeys.brightnessMode)?.clamp(0, BrightnessMode.values.length - 1) ?? 0]; + } +} diff --git a/app/lib/models/settings.dart b/app/lib/models/settings.dart new file mode 100644 index 0000000..27a264d --- /dev/null +++ b/app/lib/models/settings.dart @@ -0,0 +1,7 @@ +import 'package:dde_gesture_manager/builder/provider_annotation.dart'; + +@ProviderModel() +class Settings { + @ProviderModelProp() + bool? isDarkMode; +} diff --git a/app/lib/pages/home.dart b/app/lib/pages/home.dart new file mode 100644 index 0000000..d24aa85 --- /dev/null +++ b/app/lib/pages/home.dart @@ -0,0 +1,33 @@ +import 'package:dde_gesture_manager/widgets/footer.dart'; +import 'package:flutter/material.dart'; + +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text('WIP'), + ], + ), + SizedBox( + height: 36, + child: Footer(), + ), + ], + ), + ); + } +} diff --git a/app/lib/themes/dark.dart b/app/lib/themes/dark.dart new file mode 100644 index 0000000..c7d3266 --- /dev/null +++ b/app/lib/themes/dark.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +var darkTheme = ThemeData.dark().copyWith( + primaryColor: Colors.grey, +); diff --git a/app/lib/themes/light.dart b/app/lib/themes/light.dart new file mode 100644 index 0000000..4939083 --- /dev/null +++ b/app/lib/themes/light.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +var lightTheme = ThemeData.light().copyWith( + primaryColor: Colors.blue, +); \ No newline at end of file diff --git a/app/lib/utils/helper.dart b/app/lib/utils/helper.dart new file mode 100644 index 0000000..f306e6a --- /dev/null +++ b/app/lib/utils/helper.dart @@ -0,0 +1,17 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class H { + H._(); + + static final _h = H._(); + + factory H() => _h; + + late SharedPreferences _sp; + + SharedPreferences get sp => _sp; + + initSharedPreference() async { + _sp = await SharedPreferences.getInstance(); + } +} diff --git a/app/lib/utils/init.dart b/app/lib/utils/init.dart new file mode 100644 index 0000000..edafa2a --- /dev/null +++ b/app/lib/utils/init.dart @@ -0,0 +1 @@ +export 'init_web.dart' if (dart.library.io) 'init_linux.dart'; diff --git a/app/lib/utils/init_linux.dart b/app/lib/utils/init_linux.dart new file mode 100644 index 0000000..e11a845 --- /dev/null +++ b/app/lib/utils/init_linux.dart @@ -0,0 +1,46 @@ +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 { + var isDark = MediaQuery.of(context).platformBrightness == Brightness.dark; + if (isDark) { + context.read().setProps(isDarkMode: isDark); + } else { + var xsettings = GSettings('com.deepin.xsettings'); + String? themeName; + try { + themeName = (await xsettings.get('theme-name')).toString(); + } catch (e) { + print(e); + context.read().setProps(isDarkMode: false); + } + + if (themeName != null) { + context.read().setProps(isDarkMode: themeName.contains('dark')); + xsettings.keysChanged.listen((event) { + xsettings.get('theme-name').then((value) { + context.read().setProps(isDarkMode: value.toString().contains('dark')); + }); + }); + } + } +} + +Future initConfigs() async { + await H().initSharedPreference(); + var userLanguageIndex = H().sp.getInt(SPKeys.userLanguage) ?? 0; + var locale = supportedLocales[userLanguageIndex]; + windowManager.setTitle(CodegenLoader.mapLocales[locale.toString()]?[LocaleKeys.app_name]); + windowManager.setMinimumSize(const Size(800, 600)); +} + +var windowManager = WindowManager.instance; diff --git a/app/lib/utils/init_web.dart b/app/lib/utils/init_web.dart new file mode 100644 index 0000000..8fe0d79 --- /dev/null +++ b/app/lib/utils/init_web.dart @@ -0,0 +1,13 @@ +import 'package:dde_gesture_manager/models/settings.provider.dart'; +import 'package:dde_gesture_manager/utils/helper.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +Future initEvents(BuildContext context) async { + var isDark = MediaQuery.of(context).platformBrightness == Brightness.dark; + context.read().setProps(isDarkMode: isDark); +} + +Future initConfigs() async { + await H().initSharedPreference(); +} diff --git a/app/lib/widgets/footer.dart b/app/lib/widgets/footer.dart new file mode 100644 index 0000000..38a1f95 --- /dev/null +++ b/app/lib/widgets/footer.dart @@ -0,0 +1,39 @@ +import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/widgets/language_switcher.dart'; +import 'package:dde_gesture_manager/widgets/theme_switcher.dart'; +import 'package:dde_gesture_manager/widgets/version_checker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class Footer extends StatefulWidget { + const Footer({Key? key}) : super(key: key); + + @override + _FooterState createState() => _FooterState(); +} + +class _FooterState extends State