diff --git a/app/lib/main.dart b/app/lib/main.dart index 4dd916c..b853035 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -15,6 +15,7 @@ import 'package:stride/routes/logging_routes.dart'; import 'package:stride/routes/tasks_route.dart'; import 'package:stride/theme.dart'; import 'package:stride/utils/functions.dart'; +import 'package:feedback/feedback.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -46,7 +47,7 @@ Future main() async { ); } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { final Settings settings; final PluginManagerState pluginManagerState; const MyApp({ @@ -55,6 +56,11 @@ class MyApp extends StatelessWidget { required this.pluginManagerState, }); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -63,7 +69,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => LogBloc()), BlocProvider( create: (context) => SettingsBloc( - settings: settings, + settings: widget.settings, logBloc: context.read(), ), ), @@ -77,7 +83,7 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => PluginManagerBloc( logBloc: context.read(), - state: pluginManagerState, + state: widget.pluginManagerState, taskBloc: context.read(), ), lazy: false, @@ -91,59 +97,62 @@ class MyApp extends StatelessWidget { }, child: BlocBuilder( builder: (context, state) { - return MaterialApp( - title: 'Stride', - theme: generateTheme(darkMode: false), - darkTheme: generateTheme(darkMode: true), - themeMode: - state.settings.darkMode ? ThemeMode.dark : ThemeMode.light, - home: BlocListener( - listener: (context, state) { - if (!state.show) { - return; - } + return BetterFeedback( + darkTheme: FeedbackThemeData.dark(), + child: MaterialApp( + title: 'Stride', + theme: generateTheme(darkMode: false), + darkTheme: generateTheme(darkMode: true), + themeMode: + state.settings.darkMode ? ThemeMode.dark : ThemeMode.light, + home: BlocListener( + listener: (context, state) { + if (!state.show) { + return; + } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message.split('\n')[0]), - behavior: SnackBarBehavior.floating, - duration: const Duration(seconds: 10), - backgroundColor: state.isError ? Colors.red[300] : null, - action: SnackBarAction( - label: 'Go to Logs', - onPressed: () async { - // TODO: Maybe don't push if already there on top. - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const LoggingRoute(), - ), - ); - }, + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message.split('\n')[0]), + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 10), + backgroundColor: state.isError ? Colors.red[300] : null, + action: SnackBarAction( + label: 'Go to Logs', + onPressed: () async { + // TODO: Maybe don't push if already there on top. + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const LoggingRoute(), + ), + ); + }, + ), ), - ), - ); - }, - child: BlocListener( - listener: (context, state) async { - final title = await state.title(context); - if (!context.mounted) return; - final description = await state.content?.call(context); - if (!context.mounted) return; - showAlertDialog( - context: context, - content: description == null - ? title - : Column( - mainAxisSize: MainAxisSize.min, - children: [title, description], - ), - onConfirm: state.onConfirm, - onCancel: state.onCancel, ); }, - child: settings.repositories.isEmpty - ? const InitialRoute() - : const TasksRoute(), + child: BlocListener( + listener: (context, state) async { + final title = await state.title(context); + if (!context.mounted) return; + final description = await state.content?.call(context); + if (!context.mounted) return; + showAlertDialog( + context: context, + content: description == null + ? title + : Column( + mainAxisSize: MainAxisSize.min, + children: [title, description], + ), + onConfirm: state.onConfirm, + onCancel: state.onCancel, + ); + }, + child: widget.settings.repositories.isEmpty + ? const InitialRoute() + : const TasksRoute(), + ), ), ), ); diff --git a/app/lib/routes/settings_route.dart b/app/lib/routes/settings_route.dart index 9faabf0..74ec34b 100644 --- a/app/lib/routes/settings_route.dart +++ b/app/lib/routes/settings_route.dart @@ -1,3 +1,4 @@ +import 'package:feedback/feedback.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,6 +12,44 @@ import 'package:stride/routes/ssh_keys_route.dart'; import 'package:stride/widgets/settings_widget.dart'; import 'package:url_launcher/url_launcher_string.dart'; +/// Shows an [AlertDialog] with the given feedback. +/// This is useful for debugging purposes. +void alertFeedbackFunction( + BuildContext outerContext, + UserFeedback feedback, +) { + showDialog( + context: outerContext, + builder: (context) { + return AlertDialog( + title: Text(feedback.text), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (feedback.extra != null) Text(feedback.extra!.toString()), + Image.memory( + feedback.screenshot, + height: 600, + width: 500, + fit: BoxFit.contain, + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ); + }, + ); +} + class SettingsRoute extends StatelessWidget { TextStyle get headingStyle => const TextStyle( fontSize: 16, @@ -21,7 +60,7 @@ class SettingsRoute extends StatelessWidget { const SettingsRoute({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context_) { return Scaffold( appBar: AppBar(title: const Text('Settings')), body: BlocBuilder( @@ -67,6 +106,22 @@ class SettingsRoute extends StatelessWidget { title: const Text('Plugins'), builder: (context) => const PluginListRoute(), ), + SettingsTileSwitch( + title: Text("FeedBack"), + value: false, + onChanged: (value) { + BetterFeedback.of(context_).show( + (feedback) async { + // upload to server, share whatever + // for example purposes just show it to the user + alertFeedbackFunction( + context, + feedback, + ); + }, + ); + }, + ) ], ), // SettingsSection( diff --git a/app/pubspec.lock b/app/pubspec.lock index a94d82e..6dfc055 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -241,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + feedback: + dependency: "direct main" + description: + name: feedback + sha256: "26769f73de6215add72074d24e4a23542e4c02a8fd1a873e7c93da5dc9c1d362" + url: "https://pub.dev" + source: hosted + version: "3.1.0" ffi: dependency: transitive description: @@ -307,6 +315,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_native_splash: dependency: "direct dev" description: @@ -447,6 +460,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 5b97c9b..fefc335 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: url_launcher: ^6.3.1 intersperse: ^2.0.0 http: ^1.4.0 + feedback: ^3.1.0 dev_dependencies: flutter_test: