diff --git a/assets/icons/1.5x/delete.webp b/assets/icons/1.5x/delete.webp new file mode 100644 index 0000000..d66b421 Binary files /dev/null and b/assets/icons/1.5x/delete.webp differ diff --git a/assets/icons/2.0x/delete.webp b/assets/icons/2.0x/delete.webp new file mode 100644 index 0000000..7d54338 Binary files /dev/null and b/assets/icons/2.0x/delete.webp differ diff --git a/assets/icons/3.0x/delete.webp b/assets/icons/3.0x/delete.webp new file mode 100644 index 0000000..805b038 Binary files /dev/null and b/assets/icons/3.0x/delete.webp differ diff --git a/assets/icons/4.0x/delete.webp b/assets/icons/4.0x/delete.webp new file mode 100644 index 0000000..d634c83 Binary files /dev/null and b/assets/icons/4.0x/delete.webp differ diff --git a/assets/icons/cog.webp b/assets/icons/cog.webp deleted file mode 100644 index d30f8bf..0000000 Binary files a/assets/icons/cog.webp and /dev/null differ diff --git a/assets/icons/delete.webp b/assets/icons/delete.webp new file mode 100644 index 0000000..736cdb1 Binary files /dev/null and b/assets/icons/delete.webp differ diff --git a/lib/history.dart b/lib/history.dart index b5f9a16..53364b8 100644 --- a/lib/history.dart +++ b/lib/history.dart @@ -34,13 +34,9 @@ class HistoryManager { canRedo = _redoList.isNotEmpty; } - void undo() { - _undoOrRedo(redo: false); - } + void undo() => _undoOrRedo(redo: false); - void redo() { - _undoOrRedo(redo: true); - } + void redo() => _undoOrRedo(redo: true); void _undoOrRedo({required bool redo}) { final list = redo ? _redoList : _undoList; diff --git a/lib/preferences.dart b/lib/preferences.dart index defe7f2..79edc00 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '/widgets/dialog.dart'; -import '/widgets/button.dart'; import '/widgets/toggle_buttons.dart'; final Future prefs = SharedPreferences.getInstance(); @@ -28,39 +27,32 @@ Future initPreferences() async { void showSlyPreferencesDialog(BuildContext context) { showSlyDialog(context, 'Appearance', [ - Padding( - padding: const EdgeInsets.only(bottom: 32), - child: SlyToggleButtons( - compact: true, - defaultItem: themeNotifier.value == ThemeMode.dark - ? 0 - : themeNotifier.value == ThemeMode.system - ? 1 - : 2, - onSelected: (index) async { - (await prefs).setInt('theme', index); + SlyToggleButtons( + compact: true, + defaultItem: themeNotifier.value == ThemeMode.dark + ? 0 + : themeNotifier.value == ThemeMode.system + ? 1 + : 2, + onSelected: (index) async { + (await prefs).setInt('theme', index); - switch (index) { - case 0: - themeNotifier.value = ThemeMode.dark; - case 1: - themeNotifier.value = ThemeMode.system; - case 2: - themeNotifier.value = ThemeMode.light; - } - }, - children: const [ - Text('Dark'), - Text('System'), - Text('Light'), - ], - ), - ), - SlyButton( - onPressed: () { - Navigator.pop(context); + switch (index) { + case 0: + themeNotifier.value = ThemeMode.dark; + case 1: + themeNotifier.value = ThemeMode.system; + case 2: + themeNotifier.value = ThemeMode.light; + } }, - child: const Text('Done'), + children: const [ + Text('Dark'), + Text('System'), + Text('Light'), + ], ), + const SizedBox(height: 8), + const SlyCancelButton(label: 'Done'), ]); } diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 71d87b5..f5acf95 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -96,13 +96,10 @@ class SlyButtonState extends State { return elevatedButton!; } - void setChild(Widget newChild) { - setState(() { - elevatedButtonChild = SizedBox( - key: UniqueKey(), - height: 40, - child: Center(child: newChild), - ); - }); - } + void setChild(Widget newChild) => + setState(() => elevatedButtonChild = SizedBox( + key: UniqueKey(), + height: 40, + child: Center(child: newChild), + )); } diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index 9851739..3a983c1 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -1,5 +1,17 @@ import 'package:flutter/material.dart'; +import '/widgets/button.dart'; + +/// Presents a dialog with `title` and `children` underneath it. +/// +/// On smaller screens, the dialog is presented as a bottom sheet. +/// +/// If it is not required to complete the action inside the dialog, +/// consider adding a `SlyCancelButton` to the end of `children` +/// for the user to be able to exit the dialog conveniently. +/// +/// Children have pre-baked spacing between them, if that is not desired, +/// consider passing in a single child with your own padding. Future showSlyDialog( BuildContext context, String title, @@ -28,7 +40,7 @@ Future showSlyDialog( ), titlePadding: const EdgeInsets.only( top: 32, - bottom: 24, + bottom: 20, left: 16, right: 16, ), @@ -39,7 +51,16 @@ Future showSlyDialog( style: const TextStyle(fontWeight: FontWeight.bold), ), ), - children: children, + children: children + .asMap() + .entries + .map((entry) => entry.key == children.length - 1 + ? entry.value + : Padding( + padding: const EdgeInsets.only(bottom: 12), + child: entry.value, + )) + .toList(), ); }, transitionDuration: const Duration(milliseconds: 300), @@ -94,7 +115,7 @@ Future showSlyDialog( Padding( padding: const EdgeInsets.only( top: 32, - bottom: 24, + bottom: 20, left: 16, right: 16, ), @@ -109,15 +130,18 @@ Future showSlyDialog( ), ), ), - ListView( + ListView.separated( padding: const EdgeInsets.only( - bottom: 36, + bottom: 32, left: 24, right: 24, ), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - children: children, + itemCount: children.length, + separatorBuilder: (BuildContext context, int index) => + const SizedBox(height: 12), + itemBuilder: (BuildContext context, int index) => children[index], ), ], ); @@ -125,3 +149,19 @@ Future showSlyDialog( ); } } + +class SlyCancelButton extends StatelessWidget { + final String? label; + + /// A button to dismiss a dialog. + /// + /// If the `label` parameter is not provided, it will be 'Cancel'. + const SlyCancelButton({super.key, this.label}); + + @override + Widget build(BuildContext context) => SlyButton( + suggested: true, + onPressed: () => Navigator.pop(context), + child: Text(label ?? 'Cancel'), + ); +} diff --git a/lib/widgets/title_bar.dart b/lib/widgets/title_bar.dart index cfed738..42f8baa 100644 --- a/lib/widgets/title_bar.dart +++ b/lib/widgets/title_bar.dart @@ -55,9 +55,7 @@ class SlyTitleBar extends StatelessWidget { icon: const ImageIcon( AssetImage('assets/icons/window-close.webp'), ), - onPressed: () { - exit(0); - }, + onPressed: () => exit(0), ), ), ], diff --git a/lib/widgets/unique/about.dart b/lib/widgets/unique/about.dart index 0c370c9..67e2577 100644 --- a/lib/widgets/unique/about.dart +++ b/lib/widgets/unique/about.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '/widgets/dialog.dart'; -import '/widgets/button.dart'; import '/widgets/markup_text.dart'; import '/widgets/title_bar.dart'; @@ -53,13 +52,8 @@ can be viewed [here](slycallback://0). ), ) ]), - const SizedBox(height: 24), - SlyButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Done'), - ), + const SizedBox(height: 12), + const SlyCancelButton(label: 'Done'), ], ), ), diff --git a/lib/widgets/unique/carousel.dart b/lib/widgets/unique/carousel.dart index ce26632..12d7441 100644 --- a/lib/widgets/unique/carousel.dart +++ b/lib/widgets/unique/carousel.dart @@ -7,7 +7,7 @@ import '/widgets/tooltip.dart'; import '/juggler.dart'; class SlyCarouselData extends InheritedWidget { - final (bool, bool, SlyJuggler, GlobalKey, VoidCallback?) data; + final (bool, bool, SlyJuggler, GlobalKey) data; const SlyCarouselData({ super.key, @@ -43,7 +43,6 @@ class _SlyImageCarouselState extends State { final wideLayout = data.$2; final juggler = data.$3; final globalKey = data.$4; - final exportAll = data.$5; final buttonStyle = IconButton.styleFrom( backgroundColor: wideLayout @@ -85,20 +84,18 @@ class _SlyImageCarouselState extends State { icon: const ImageIcon(AssetImage( 'assets/icons/add.webp', )), - onPressed: () { - juggler.editImages( - context: context, - loadingCallback: () => showSlySnackBar( - context, - 'Loading', - loading: true, - ), - ); - }, + onPressed: () => juggler.editImages( + context: context, + loadingCallback: () => showSlySnackBar( + context, + 'Loading', + loading: true, + ), + ), ), ), SlyTooltip( - message: 'Image Options', + message: 'Remove Image', child: IconButton( visualDensity: const VisualDensity( vertical: -2, @@ -109,37 +106,29 @@ class _SlyImageCarouselState extends State { splashColor: Colors.transparent, highlightColor: Colors.transparent, icon: const ImageIcon(AssetImage( - 'assets/icons/cog.webp', + 'assets/icons/delete.webp', )), onPressed: () => - showSlyDialog(context, 'Image Options', [ - juggler.images.length > 1 - ? Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SlyButton( - onPressed: () { - juggler.remove(juggler.selected); - Navigator.pop(context); - }, - child: const Text('Remove'), - ), - ) - : Container(), + showSlyDialog(context, 'Remove Image?', [ Padding( padding: const EdgeInsets.only(bottom: 12), - child: SlyButton( - onPressed: () { - Navigator.pop(context); - if (exportAll != null) exportAll(); - }, - child: const Text('Save All'), + child: ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 240), + child: const Text( + 'The original will not be deleted, but unsaved edits will be lost.', + textAlign: TextAlign.center, + ), ), ), SlyButton( - suggested: true, - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ) + onPressed: () { + juggler.remove(juggler.selected); + Navigator.pop(context); + }, + child: const Text('Remove'), + ), + const SlyCancelButton(), ]), ), ), diff --git a/lib/widgets/unique/editor.dart b/lib/widgets/unique/editor.dart index 59a72ea..244d3ca 100644 --- a/lib/widgets/unique/editor.dart +++ b/lib/widgets/unique/editor.dart @@ -168,43 +168,28 @@ class _SlyEditorPageState extends State { context, 'Choose a Quality', [ - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SlyButton( - onPressed: () { - format = SlyImageFormat.jpeg75; - Navigator.pop(context); - }, - child: const Text('For Sharing'), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SlyButton( - onPressed: () { - format = SlyImageFormat.jpeg90; - Navigator.pop(context); - }, - child: const Text('For Storing'), - ), + SlyButton( + onPressed: () { + format = SlyImageFormat.jpeg75; + Navigator.pop(context); + }, + child: const Text('For Sharing'), ), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: SlyButton( - onPressed: () { - format = SlyImageFormat.png; - Navigator.pop(context); - }, - child: const Text('Lossless'), - ), + SlyButton( + onPressed: () { + format = SlyImageFormat.jpeg90; + Navigator.pop(context); + }, + child: const Text('For Storing'), ), SlyButton( - suggested: true, onPressed: () { + format = SlyImageFormat.png; Navigator.pop(context); }, - child: const Text('Cancel'), + child: const Text('Lossless'), ), + const SlyCancelButton(), ], ); @@ -350,20 +335,16 @@ class _SlyEditorPageState extends State { }); } - void toggleCarousel() { - if (juggler.images.length <= 1) { - juggler.editImages( - context: context, - loadingCallback: () => showSlySnackBar( - context, - 'Loading', - loading: true, - ), - ); - } else { - setState(() => _showCarousel = !_showCarousel); - } - } + void toggleCarousel() => juggler.images.length <= 1 + ? juggler.editImages( + context: context, + loadingCallback: () => showSlySnackBar( + context, + 'Loading', + loading: true, + ), + ) + : setState(() => _showCarousel = !_showCarousel); void showOriginal() async { if (_editedImageData == _originalImageData) return; @@ -418,17 +399,22 @@ class _SlyEditorPageState extends State { getPortraitCrop: () => _portraitCrop, setPortraitCrop: (value) => setState(() => _portraitCrop = value), rotation: _geometryAttributes['rotation']!, - rotate: (value) => setState(() { - _geometryAttributes['rotation']!.value = value; - }), + rotate: (value) => setState( + () => _geometryAttributes['rotation']!.value = value, + ), flipImage: flipImage, ); case 4: return SlyExportControls( - saveButton: _saveButton, wideLayout: _wideLayout, getSaveMetadata: () => _saveMetadata, setSaveMetadata: (value) => _saveMetadata = value, + multipleImages: juggler.images.length > 1, + saveButton: _saveButton, + exportAll: () { + _saveAll = true; + _startSave(); + }, ); default: return SlyControlsListView( @@ -450,9 +436,7 @@ class _SlyEditorPageState extends State { _selectedPageIndex = index; - setState(() { - _controlsChild = getControlsChild(index); - }); + setState(() => _controlsChild = getControlsChild(index)); } @override @@ -577,18 +561,7 @@ class _SlyEditorPageState extends State { navigationDestinationSelected(index), ), imageCarousel: SlyCarouselData( - data: ( - _showCarousel, - _wideLayout, - juggler, - _carouselKey, - () { - navigationDestinationSelected(4); - - _saveAll = true; - _startSave(); - }, - ), + data: (_showCarousel, _wideLayout, juggler, _carouselKey), child: const SlyImageCarousel(), ), showCarousel: _showCarousel, diff --git a/lib/widgets/unique/export_controls.dart b/lib/widgets/unique/export_controls.dart index efae23a..c92c1a4 100644 --- a/lib/widgets/unique/export_controls.dart +++ b/lib/widgets/unique/export_controls.dart @@ -1,20 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:sly/widgets/button.dart'; import '/widgets/switch.dart'; import '/widgets/unique/save_button.dart'; class SlyExportControls extends StatelessWidget { - final SlySaveButton? saveButton; final bool wideLayout; final Function getSaveMetadata; final Function setSaveMetadata; + final bool multipleImages; + final SlySaveButton? saveButton; + final VoidCallback? exportAll; const SlyExportControls({ super.key, - this.saveButton, required this.wideLayout, required this.getSaveMetadata, required this.setSaveMetadata, + required this.multipleImages, + this.saveButton, + this.exportAll, }); @override @@ -58,12 +63,26 @@ class SlyExportControls extends StatelessWidget { Padding( padding: const EdgeInsets.only( top: 6, - bottom: 40, + bottom: 6, left: 24, right: 24, ), child: saveButton, ), + multipleImages + ? Padding( + padding: const EdgeInsets.only( + top: 6, + bottom: 40, + left: 24, + right: 24, + ), + child: SlyButton( + onPressed: exportAll, + child: const Text('Save All'), + ), + ) + : Container(), ], ); } diff --git a/lib/widgets/unique/geometry_controls.dart b/lib/widgets/unique/geometry_controls.dart index e688c39..c3bc2f3 100644 --- a/lib/widgets/unique/geometry_controls.dart +++ b/lib/widgets/unique/geometry_controls.dart @@ -50,82 +50,44 @@ class SlyGeometryControls extends StatelessWidget { AssetImage('assets/icons/aspect-ratio.webp'), ), padding: const EdgeInsets.all(12), - onPressed: () { - showSlyDialog(context, 'Select Aspect Ratio', [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: SlyToggleButtons( - defaultItem: getPortraitCrop() ? 1 : 0, - onSelected: (index) { - setPortraitCrop(index == 1); - }, - children: const [ - Text('Landscape'), - Text('Portrait'), - ], - ), + onPressed: () => + showSlyDialog(context, 'Select Aspect Ratio', [ + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: SlyToggleButtons( + defaultItem: getPortraitCrop() ? 1 : 0, + onSelected: (index) => setPortraitCrop(index == 1), + children: const [ + Text('Landscape'), + Text('Portrait'), + ], ), - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SlyButton( - onPressed: () { - onAspectRatioSelected(null); - }, - child: const Text('Free'), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SlyButton( - onPressed: () { - onAspectRatioSelected(1); - }, - child: const Text('Square'), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SlyButton( - onPressed: () { - onAspectRatioSelected( - getPortraitCrop() ? 3 / 4 : 4 / 3, - ); - }, - child: const Text('4:3'), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: SlyButton( - onPressed: () { - onAspectRatioSelected( - getPortraitCrop() ? 2 / 3 : 3 / 2, - ); - }, - child: const Text('3:2'), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: SlyButton( - onPressed: () { - onAspectRatioSelected( - getPortraitCrop() ? 9 / 16 : 16 / 9, - ); - }, - child: const Text('16:9'), - ), - ), - SlyButton( - suggested: true, - onPressed: () { - if (cropController == null) return; - onAspectRatioSelected(cropController!.aspectRatio); - }, - child: const Text('Cancel'), - ), - ]); - }, + ), + SlyButton( + child: const Text('Free'), + onPressed: () => onAspectRatioSelected(null), + ), + SlyButton( + child: const Text('Square'), + onPressed: () => onAspectRatioSelected(1), + ), + SlyButton( + child: const Text('4:3'), + onPressed: () => + onAspectRatioSelected(getPortraitCrop() ? 3 / 4 : 4 / 3), + ), + SlyButton( + child: const Text('3:2'), + onPressed: () => + onAspectRatioSelected(getPortraitCrop() ? 2 / 3 : 3 / 2), + ), + SlyButton( + child: const Text('16:9'), + onPressed: () => + onAspectRatioSelected(getPortraitCrop() ? 9 / 16 : 16 / 9), + ), + const SlyCancelButton(), + ]), ), ), SlyTooltip( @@ -137,9 +99,7 @@ class SlyGeometryControls extends StatelessWidget { AssetImage('assets/icons/rotate-left.webp'), ), padding: const EdgeInsets.all(12), - onPressed: () { - rotate(rotation.value - 1); - }, + onPressed: () => rotate(rotation.value - 1), ), ), SlyTooltip( @@ -151,9 +111,7 @@ class SlyGeometryControls extends StatelessWidget { AssetImage('assets/icons/rotate-right.webp'), ), padding: const EdgeInsets.all(12), - onPressed: () { - rotate(rotation.value + 1); - }, + onPressed: () => rotate(rotation.value + 1), ), ), SlyTooltip( @@ -165,9 +123,7 @@ class SlyGeometryControls extends StatelessWidget { AssetImage('assets/icons/flip-horizontal.webp'), ), padding: const EdgeInsets.all(12), - onPressed: () { - flipImage(SlyImageFlipDirection.horizontal); - }, + onPressed: () => flipImage(SlyImageFlipDirection.horizontal), ), ), SlyTooltip( @@ -179,9 +135,7 @@ class SlyGeometryControls extends StatelessWidget { AssetImage('assets/icons/flip-vertical.webp'), ), padding: const EdgeInsets.all(12), - onPressed: () { - flipImage(SlyImageFlipDirection.vertical); - }, + onPressed: () => flipImage(SlyImageFlipDirection.vertical), ), ), ]; diff --git a/lib/widgets/unique/home.dart b/lib/widgets/unique/home.dart index b7c0a56..7d0107b 100644 --- a/lib/widgets/unique/home.dart +++ b/lib/widgets/unique/home.dart @@ -67,9 +67,7 @@ class _SlyHomePageState extends State { const AssetImage('assets/icons/preferences.webp'), ), padding: const EdgeInsets.all(12), - onPressed: () { - showSlyPreferencesDialog(context); - }, + onPressed: () => showSlyPreferencesDialog(context), ), ), ); @@ -136,9 +134,7 @@ class _SlyHomePageState extends State { Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: SlyButton( - onPressed: () { - showSlyAboutDialog(context); - }, + onPressed: () => showSlyAboutDialog(context), child: const Text('About Sly'), ), ), diff --git a/lib/widgets/unique/toolbar.dart b/lib/widgets/unique/toolbar.dart index e79a7cc..6284f13 100644 --- a/lib/widgets/unique/toolbar.dart +++ b/lib/widgets/unique/toolbar.dart @@ -84,9 +84,7 @@ class SlyToolbar extends StatelessWidget { : Theme.of(context).disabledColor, const AssetImage('assets/icons/undo.webp'), ), - onPressed: () { - history.undo(); - }, + onPressed: () => history.undo(), ), ), SlyTooltip( @@ -100,9 +98,7 @@ class SlyToolbar extends StatelessWidget { : Theme.of(context).disabledColor, const AssetImage('assets/icons/redo.webp'), ), - onPressed: () { - history.redo(); - }, + onPressed: () => history.redo(), ), ), ].whereType().toList(),