diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..ba4de9f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -22,7 +22,7 @@ linter: # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/assets/icons/1.5x/histogram.png b/assets/icons/1.5x/histogram.png new file mode 100644 index 0000000..aec5d1e Binary files /dev/null and b/assets/icons/1.5x/histogram.png differ diff --git a/assets/icons/2.0x/histogram.png b/assets/icons/2.0x/histogram.png new file mode 100644 index 0000000..49bcbab Binary files /dev/null and b/assets/icons/2.0x/histogram.png differ diff --git a/assets/icons/3.0x/histogram.png b/assets/icons/3.0x/histogram.png new file mode 100644 index 0000000..3f10d8f Binary files /dev/null and b/assets/icons/3.0x/histogram.png differ diff --git a/assets/icons/4.0x/histogram.png b/assets/icons/4.0x/histogram.png new file mode 100644 index 0000000..afed00e Binary files /dev/null and b/assets/icons/4.0x/histogram.png differ diff --git a/assets/icons/histogram.png b/assets/icons/histogram.png new file mode 100644 index 0000000..888c2b5 Binary files /dev/null and b/assets/icons/histogram.png differ diff --git a/lib/editor_page.dart b/lib/editor_page.dart index e1c6463..2e06d67 100644 --- a/lib/editor_page.dart +++ b/lib/editor_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:crop_image/crop_image.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'utils.dart'; import 'image.dart'; @@ -15,6 +16,7 @@ import 'slider_row.dart'; import 'switch.dart'; import 'toggle_buttons.dart'; import 'spinner.dart'; +import 'histogram.dart'; import 'dialog.dart'; import 'snack_bar.dart'; import 'title_bar.dart'; @@ -33,6 +35,8 @@ class SlyEditorPage extends StatefulWidget { } class _SlyEditorPageState extends State { + final Future prefs = SharedPreferences.getInstance(); + final GlobalKey _saveButtonKey = GlobalKey(); final GlobalKey _imageWidgetKey = GlobalKey(); int _controlsWidgetKeyValue = 0; @@ -67,6 +71,7 @@ class _SlyEditorPageState extends State { bool _canRedo = false; int _selectedPageIndex = 0; + bool _showHistogram = false; final String _saveButtonLabel = !kIsWeb && Platform.isIOS ? 'Save to Photos' : 'Save'; @@ -226,6 +231,15 @@ class _SlyEditorPageState extends State { @override void initState() { + prefs.then((value) { + final showHistogram = value.getBool('showHistogram'); + if (showHistogram == null) return; + + setState(() { + _showHistogram = showHistogram; + }); + }); + _editedImage = SlyImage.from(_originalImage); subscription = _editedImage.controller.stream.listen(_onImageUpdate); updateImage(); @@ -995,7 +1009,7 @@ class _SlyEditorPageState extends State { ); final controlsWidget = AnimatedSize( - key: Key("controlsWidget $_controlsWidgetKeyValue"), + key: Key('controlsWidget $_controlsWidgetKeyValue'), duration: const Duration(milliseconds: 300), curve: Curves.easeOutQuint, child: AnimatedSwitcher( @@ -1044,7 +1058,30 @@ class _SlyEditorPageState extends State { alignment: constraints.maxWidth > 600 ? WrapAlignment.start : WrapAlignment.center, - children: [ + children: [ + [0, 1].contains(_selectedPageIndex) + ? Tooltip( + message: _showHistogram + ? 'Hide Histogram' + : 'Show Histogram', + child: IconButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + icon: const ImageIcon( + color: Colors.white54, + AssetImage('assets/icons/histogram.png'), + ), + onPressed: () async { + await (await prefs) + .setBool('showHistogram', !_showHistogram); + + setState(() { + _showHistogram = !_showHistogram; + }); + }, + ), + ) + : null, Tooltip( message: 'Show Original', child: IconButton( @@ -1114,8 +1151,32 @@ class _SlyEditorPageState extends State { }, ), ), - ], + ].whereType().toList(), + ), + ); + + final histogram = AnimatedSize( + duration: Duration( + milliseconds: _selectedPageIndex == 3 ? 0 : 300, ), + curve: Curves.easeOutQuint, + child: [0, 1].contains(_selectedPageIndex) && _showHistogram + ? Padding( + padding: EdgeInsets.only( + bottom: constraints.maxWidth > 600 ? 12 : 0, + top: (constraints.maxWidth > 600 && + !kIsWeb && + (Platform.isLinux || Platform.isMacOS)) + ? 0 + : 8, + ), + child: SizedBox( + height: constraints.maxWidth > 600 ? 40 : 30, + width: constraints.maxWidth > 600 ? null : 150, + child: getHistogram(_editedImage), + ), + ) + : Container(), ); if (constraints.maxWidth > 600) { @@ -1201,6 +1262,7 @@ class _SlyEditorPageState extends State { child: Container(), ), ), + histogram, Expanded(child: controlsWidget), _selectedPageIndex != 3 && _selectedPageIndex != 4 @@ -1265,7 +1327,11 @@ class _SlyEditorPageState extends State { ] : [ imageWidget, - toolbar, + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [toolbar, histogram], + ), controlsWidget, ], ), diff --git a/lib/histogram.dart b/lib/histogram.dart new file mode 100644 index 0000000..9827670 --- /dev/null +++ b/lib/histogram.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +import 'package:fl_chart/fl_chart.dart'; + +import 'image.dart'; + +LineChart getHistogram(SlyImage image) { + final imageData = image.getHistogramData(); + + final List> spots = [ + List.generate(16, (index) => FlSpot(index.toDouble(), 0)), + List.generate(16, (index) => FlSpot(index.toDouble(), 0)), + List.generate(16, (index) => FlSpot(index.toDouble(), 0)) + ]; + + int channel = 0; + for (int pixel in imageData) { + int i = pixel >> 4; + if (i == 0 && pixel != 0) { + i = 1; + } else if (i == 15 && pixel != 255) { + i = 14; + } + + spots[channel][i] = FlSpot((i).toDouble(), spots[channel][i].y + 1); + channel = (channel + 1) % 3; + } + + final List> colors = [ + [Colors.red.shade900, Colors.red], + [Colors.green.shade900, Colors.green], + [Colors.blue.shade900, Colors.blue], + ]; + + final List lineBarsData = []; + + for (int i = 0; i < 3; i++) { + lineBarsData.add( + LineChartBarData( + spots: spots[i], + isCurved: true, + gradient: LinearGradient( + colors: [colors[i][0], colors[i][1]], + ), + barWidth: 2, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [colors[i][0], colors[i][1]] + .map((color) => color.withOpacity(1 / 3)) + .toList(), + ), + ), + ), + ); + } + + return LineChart( + LineChartData( + titlesData: const FlTitlesData(show: false), + lineTouchData: const LineTouchData(enabled: false), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + lineBarsData: lineBarsData, + ), + ); +} diff --git a/lib/image.dart b/lib/image.dart index 703d122..7b697cd 100644 --- a/lib/image.dart +++ b/lib/image.dart @@ -312,6 +312,14 @@ class SlyImage { return (await cmd.executeThread()).outputBytes!; } + /// Returns a short list representing lightness across the image, + /// useful for building a histogram. + Uint8List getHistogramData() { + final resizedImage = img.copyResize(_image, width: 20, height: 20); + + return resizedImage.buffer.asUint8List(); + } + void dispose() { controller.close(); _editsApplied = double.infinity; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c3f34d2..badba5e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import file_selector_macos import gal import screen_retriever +import shared_preferences_foundation import url_launcher_macos import window_manager @@ -15,6 +16,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 378ffdf..d3c893b 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -7,6 +7,9 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - window_manager (0.2.0): @@ -17,6 +20,7 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) @@ -29,6 +33,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos window_manager: @@ -39,6 +45,7 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 diff --git a/pubspec.lock b/pubspec.lock index 02ecb9f..54d4481 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.5" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -105,6 +113,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" file_selector: dependency: "direct main" description: @@ -169,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+2" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" + url: "https://pub.dev" + source: hosted + version: "0.69.0" flutter: dependency: "direct main" description: flutter @@ -400,6 +432,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -408,6 +464,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -432,6 +496,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.9" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -589,6 +709,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c516ee4..f6be386 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,8 @@ dependencies: crop_image: ^1.0.13 handy_window: ^0.4.0 window_manager: ^0.4.2 + fl_chart: ^0.69.0 + shared_preferences: ^2.3.2 dev_dependencies: flutter_test: