diff --git a/assets/img/animated_light_bulb.gif b/assets/img/animated_light_bulb.gif new file mode 100644 index 00000000..0c56df63 Binary files /dev/null and b/assets/img/animated_light_bulb.gif differ diff --git a/lib/ui/pages/landing_page/components/main_overflow_menu.dart b/lib/ui/pages/landing_page/components/main_overflow_menu.dart index 1b231252..43e31872 100644 --- a/lib/ui/pages/landing_page/components/main_overflow_menu.dart +++ b/lib/ui/pages/landing_page/components/main_overflow_menu.dart @@ -8,16 +8,22 @@ import 'package:paintroid/core/utils/open_url.dart'; import 'package:paintroid/ui/shared/dialogs/about_dialog.dart'; import 'package:paintroid/ui/shared/pop_menu_button.dart'; import 'package:paintroid/ui/theme/theme.dart'; +import 'package:shared_preferences/shared_preferences.dart'; enum MainOverflowMenuOption { rate('Rate us!'), help('Help'), about('About'), - feedback('Feedback'); + feedback('Feedback'), + tip('Disable tip of the day'); const MainOverflowMenuOption(this.label); final String label; + + static String getTipLabel(bool showTip) { + return showTip ? 'Disable tip of the day' : 'Enable tip of the day'; + } } class MainOverflowMenu extends ConsumerStatefulWidget { @@ -31,6 +37,28 @@ class _MainOverFlowMenuState extends ConsumerState { final feedbackUrl = 'mailto:support-paintroid@catrobat.org'; final iOSAppId = 'org.catrobat.paintroidflutter'; final androidAppId = 'org.catrobat.paintroid'; + bool showTip = true; + + @override + void initState() { + super.initState(); + _loadShowTipPreference(); + } + + Future _loadShowTipPreference() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + showTip = prefs.getBool('showTip') ?? true; + }); + } + + Future _toggleShowTipPreference() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + showTip = !showTip; + prefs.setBool('showTip', showTip); + }); + } @override Widget build(BuildContext context) { @@ -41,7 +69,7 @@ class _MainOverFlowMenuState extends ConsumerState { (option) => PopupMenuItem( value: option, child: Text( - option.label, + option == MainOverflowMenuOption.tip ? MainOverflowMenuOption.getTipLabel(showTip) : option.label, style: PaintroidTheme.of(context).textTheme.bodyMedium, ), ), @@ -70,6 +98,9 @@ class _MainOverFlowMenuState extends ConsumerState { case MainOverflowMenuOption.feedback: openUrl(feedbackUrl); break; + case MainOverflowMenuOption.tip: + await _toggleShowTipPreference(); + break; } } } diff --git a/lib/ui/pages/landing_page/landing_page.dart b/lib/ui/pages/landing_page/landing_page.dart index fa481f23..8547e4d1 100644 --- a/lib/ui/pages/landing_page/landing_page.dart +++ b/lib/ui/pages/landing_page/landing_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:oxidized/oxidized.dart'; +import 'package:paintroid/ui/shared/dialogs/tip_of_the_day_dialog.dart'; import 'package:toast/toast.dart'; import 'package:paintroid/core/database/project_database.dart'; @@ -69,8 +70,7 @@ class _LandingPageState extends ConsumerState { ref.read(workspaceStateProvider.notifier).updateLastSavedCommandCount(); } - Future _openProject( - Project? project, IOHandler ioHandler, WidgetRef ref) async { + Future _openProject(Project? project, IOHandler ioHandler, WidgetRef ref) async { if (project != null) { ref.read(workspaceStateProvider.notifier).performIOTask(() async { await ref.read(IDeviceService.sizeProvider.future); @@ -80,6 +80,12 @@ class _LandingPageState extends ConsumerState { } } + @override + void initState() { + super.initState(); + showTipIfEnabled(context); + } + @override Widget build(BuildContext context) { ToastContext().init(context); @@ -87,8 +93,7 @@ class _LandingPageState extends ConsumerState { final db = ref.watch(ProjectDatabase.provider); db.when( data: (value) => database = value, - error: (err, stacktrace) => - ToastUtils.showShortToast(message: 'Error: $err'), + error: (err, stacktrace) => ToastUtils.showShortToast(message: 'Error: $err'), loading: () {}, ); final ioHandler = ref.watch(IOHandler.provider); @@ -105,8 +110,7 @@ class _LandingPageState extends ConsumerState { body: FutureBuilder( future: _getProjects(), builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { + if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { if (snapshot.data!.isNotEmpty) { latestModifiedProject = snapshot.data![0]; } @@ -182,8 +186,7 @@ class _LandingPageState extends ConsumerState { icon: Icons.file_download, hint: 'Load image', onPressed: () async { - final bool imageLoaded = - await ioHandler.loadImage(context, this, false); + final bool imageLoaded = await ioHandler.loadImage(context, this, false); if (imageLoaded && mounted) { _navigateToPocketPaint(); } @@ -247,10 +250,7 @@ class _ProjectPreview extends StatelessWidget { height: 170.0, width: 170.0, decoration: BoxDecoration( - shape: BoxShape.circle, - color: PaintroidTheme.of(context) - .outlineColor - .withAlpha(180)), + shape: BoxShape.circle, color: PaintroidTheme.of(context).outlineColor.withAlpha(180)), child: Center( child: Icon( Icons.add, diff --git a/lib/ui/shared/dialogs/tip_of_the_day_dialog.dart b/lib/ui/shared/dialogs/tip_of_the_day_dialog.dart new file mode 100644 index 00000000..eac458ee --- /dev/null +++ b/lib/ui/shared/dialogs/tip_of_the_day_dialog.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:paintroid/ui/shared/images/animated_light_bulb.dart'; +import 'package:paintroid/ui/theme/theme.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +Future showTipDialog(BuildContext context) => showGeneralDialog( + context: context, + barrierDismissible: false, + barrierLabel: 'Tip of the Day', + pageBuilder: (_, __, ___) => const TipOfTheDayDialog(), + ); + +Future showTipIfEnabled(BuildContext context) async { + final prefs = await SharedPreferences.getInstance(); + final showTip = prefs.getBool('showTip') ?? true; + if (showTip && context.mounted) { + await showTipDialog(context); + } +} + +final List> tips = [ + { + 'title': 'Layer menu', + 'content': + 'Shows the current layers, you can add new, delete, show and hide layers and set transparency of the layer.', + }, + { + 'title': 'Antialiasing', + 'content': + 'Antialiasing is the reduction of unwanted effects that can arise from the limited pixel grid. You can turn antialiasing on and off in the Advanced Settings.', + }, + { + 'title': 'Smoothing', + 'content': + 'The idea is that the smaller details in the image are smoothed out. You can turn smoothing on and off in the Advanced Settings.', + }, + { + 'title': 'Zoom window settings', + 'content': 'The zoom window can be turned on or off and the zoom can be changed here.', + }, + { + 'title': 'Export', + 'content': + 'The created image can be saved in various image formats (png, jpg, ...). It is also possible to save it as a catrobat-image in order to continue editing later.', + }, + { + 'title': 'Load image', + 'content': + 'Upload an image or a catrobat-image. The uploaded image can replace the current one or be added to the current layer.', + }, + { + 'title': 'Color Picker', + 'content': + 'The color picker can be used to set the color and transparency for the tool. Use a predefined standard color or define a color yourself with RGB values and HEX codes. With the pipette you can choose an already existing color on the canvas.', + }, + { + 'title': 'Hide Buttons', + 'content': + 'You can hide the header and the toolbar. You can display them again using the back button on your phone.', + }, + { + 'title': 'Share image', + 'content': 'You can share your picture via messaging apps or email.', + }, + { + 'title': 'Help', + 'content': + 'You can use \'Help\' in the menu to start the Paintroid tutorial, where the tools and functions are explained.', + }, + { + 'title': 'Calligraphy', + 'content': + 'The calligraphy pen and chalk pen can be selected in the brush tool. The inclination of the pen can be adjusted. There are also templates for practicing calligraphy, which can be found under Import Image.', + }, +]; + +class TipOfTheDayDialog extends StatefulWidget { + const TipOfTheDayDialog({super.key}); + + @override + State createState() => _TipOfTheDayDialogState(); +} + +class _TipOfTheDayDialogState extends State { + int currentTipIndex = 0; + + @override + void initState() { + super.initState(); + _loadTipIndex(); + } + + Future _loadTipIndex() async { + final prefs = await SharedPreferences.getInstance(); + final tipIndex = prefs.getInt('tipIndex') ?? 0; + setState(() { + currentTipIndex = tipIndex; + }); + } + + Future _incrementTipIndex() async { + final prefs = await SharedPreferences.getInstance(); + final tipIndex = prefs.getInt('tipIndex') ?? 0; + prefs.setInt('tipIndex', (tipIndex + 1) % tips.length); + currentTipIndex = (tipIndex + 1) % tips.length; + } + + Future _closeTipDialog(BuildContext context) async { + Navigator.of(context).pop(); + await _incrementTipIndex(); + } + + @override + Widget build(BuildContext context) { + final currentTip = tips[currentTipIndex]; + return AlertDialog( + contentPadding: EdgeInsets.zero, + backgroundColor: PaintroidTheme.of(context).onSurfaceColor, + titlePadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Tip of the Day', + style: PaintroidTheme.of(context).titleTheme.titleMedium, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + await _closeTipDialog(context); + }, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Divider(height: 1, thickness: 1), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + child: Column( + children: [ + const SizedBox(height: 100, child: AnimatedLightBulb()), + const SizedBox(height: 16), + Text( + currentTip['title']!, + textAlign: TextAlign.center, + style: PaintroidTheme.of(context).textTheme.bodyMedium!.apply(fontWeightDelta: 2), + ), + const SizedBox(height: 8), + Text( + currentTip['content']!, + textAlign: TextAlign.center, + style: PaintroidTheme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + const Divider(height: 1, thickness: 1), + ], + ), + actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + actions: [ + TextButton( + onPressed: () async { + await _closeTipDialog(context); + }, + child: Text( + 'Close', + style: TextStyle(color: PaintroidTheme.of(context).primaryColor), + ), + ), + TextButton( + onPressed: () async { + await _incrementTipIndex(); + setState(() {}); + }, + child: Text( + 'Next', + style: TextStyle(color: PaintroidTheme.of(context).primaryColor), + ), + ), + ], + ); + } +} diff --git a/lib/ui/shared/images/animated_light_bulb.dart b/lib/ui/shared/images/animated_light_bulb.dart new file mode 100644 index 00000000..2a4d7fc7 --- /dev/null +++ b/lib/ui/shared/images/animated_light_bulb.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class AnimatedLightBulb extends StatelessWidget { + const AnimatedLightBulb({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + child: Image.asset( + 'assets/img/animated_light_bulb.gif', + repeat: ImageRepeat.repeat, + cacheWidth: 50, + cacheHeight: 50, + filterQuality: FilterQuality.none, + ), + ); + } +}