Implemented new image editor

This commit is contained in:
AmanRajSinghMourya
2025-07-23 23:47:03 +05:30
parent 774292bdea
commit 7a6fb1ba31
8 changed files with 2105 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import "package:pro_image_editor/mixins/converted_configs.dart";
import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
/// A mixin providing access to simple editor configurations.
mixin SimpleConfigsAccess on StatefulWidget {
ProImageEditorConfigs get configs;
ProImageEditorCallbacks get callbacks;
}
mixin SimpleConfigsAccessState<T extends StatefulWidget>
on State<T>, ImageEditorConvertedConfigs {
SimpleConfigsAccess get _widget => (widget as SimpleConfigsAccess);
@override
ProImageEditorConfigs get configs => _widget.configs;
ProImageEditorCallbacks get callbacks => _widget.callbacks;
}

View File

@@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
import "package:pro_image_editor/modules/crop_rotate_editor/crop_rotate_editor.dart";
import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
enum CropAspectRatioType {
original(
label: "Original",
ratio: null,
svg: "assets/image-editor/image-editor-crop-original.svg",
),
free(
label: "Free",
ratio: null,
svg: "assets/video-editor/video-crop-free-action.svg",
),
square(
label: "1:1",
ratio: 1.0,
svg: "assets/video-editor/video-crop-ratio_1_1-action.svg",
),
widescreen(
label: "16:9",
ratio: 16.0 / 9.0,
svg: "assets/video-editor/video-crop-ratio_16_9-action.svg",
),
portrait(
label: "9:16",
ratio: 9.0 / 16.0,
svg: "assets/video-editor/video-crop-ratio_9_16-action.svg",
),
photo(
label: "4:3",
ratio: 4.0 / 3.0,
svg: "assets/video-editor/video-crop-ratio_4_3-action.svg",
),
photo_3_4(
label: "3:4",
ratio: 3.0 / 4.0,
svg: "assets/video-editor/video-crop-ratio_3_4-action.svg",
);
const CropAspectRatioType({
required this.label,
required this.ratio,
required this.svg,
});
final String label;
final String svg;
final double? ratio;
}
class ImageEditorCropRotateBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorCropRotateBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
});
final CropRotateEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
@override
State<ImageEditorCropRotateBar> createState() =>
_ImageEditorCropRotateBarState();
}
class _ImageEditorCropRotateBarState extends State<ImageEditorCropRotateBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
CropAspectRatioType selectedAspectRatio = CropAspectRatioType.original;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return BottomAppBar(
color: getEnteColorScheme(context).backgroundBase,
height: editorBottomBarHeight,
child: Align(
alignment: Alignment.bottomCenter,
child: FadeInUp(
duration: fadeInDuration,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop-rotate.svg",
label: "Rotate",
onTap: () {
widget.editor.rotate();
},
),
const SizedBox(width: 12),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-flip.svg",
label: "Flip",
onTap: () {
widget.editor.flip();
},
),
],
),
const SizedBox(height: 20),
SizedBox(
height: 48,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: CropAspectRatioType.values.length,
itemBuilder: (context, index) {
final aspectRatio = CropAspectRatioType.values[index];
final isSelected = selectedAspectRatio == aspectRatio;
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: CropAspectChip(
label: aspectRatio.label,
svg: aspectRatio.svg,
isSelected: isSelected,
onTap: () {
setState(() {
selectedAspectRatio = aspectRatio;
});
widget.editor
.updateAspectRatio(aspectRatio.ratio ?? -1);
},
),
);
},
),
),
],
),
),
),
);
}
}
class CropAspectChip extends StatelessWidget {
final String? label;
final IconData? icon;
final String? svg;
final bool isSelected;
final VoidCallback? onTap;
const CropAspectChip({
super.key,
this.label,
this.icon,
this.svg,
required this.isSelected,
this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: isSelected
? colorScheme.fillBasePressed
: colorScheme.backgroundElevated2,
borderRadius: BorderRadius.circular(25),
),
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (svg != null) ...[
SvgPicture.asset(
svg!,
height: 40,
colorFilter: ColorFilter.mode(
isSelected ? colorScheme.backdropBase : colorScheme.tabIcon,
BlendMode.srcIn,
),
),
],
const SizedBox(width: 4),
if (label != null)
Text(
label!,
style: TextStyle(
color: isSelected
? colorScheme.backdropBase
: colorScheme.tabIcon,
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,101 @@
import 'package:figma_squircle/figma_squircle.dart';
import 'package:flutter/material.dart';
import "package:photos/ente_theme_data.dart";
import "package:photos/theme/ente_theme.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class ImageEditorFilterBar extends StatefulWidget {
const ImageEditorFilterBar({
required this.filterModel,
required this.isSelected,
required this.onSelectFilter,
required this.editorImage,
this.filterKey,
super.key,
});
final FilterModel filterModel;
final bool isSelected;
final VoidCallback onSelectFilter;
final Widget editorImage;
final Key? filterKey;
@override
State<ImageEditorFilterBar> createState() => _ImageEditorFilterBarState();
}
class _ImageEditorFilterBarState extends State<ImageEditorFilterBar> {
@override
Widget build(BuildContext context) {
return buildFilteredOptions(
widget.editorImage,
widget.isSelected,
widget.filterModel.name,
widget.onSelectFilter,
widget.filterKey ?? ValueKey(widget.filterModel.name),
);
}
Widget buildFilteredOptions(
Widget editorImage,
bool isSelected,
String filterName,
VoidCallback onSelectFilter,
Key filterKey,
) {
return GestureDetector(
onTap: () => onSelectFilter(),
child: SizedBox(
height: 90,
width: 80,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
height: 60,
width: 60,
decoration: ShapeDecoration(
shape: SmoothRectangleBorder(
borderRadius: SmoothBorderRadius(
cornerRadius: 12,
cornerSmoothing: 0.6,
),
side: BorderSide(
color: isSelected
? Theme.of(context).colorScheme.imageEditorPrimaryColor
: Colors.transparent,
width: 1.5,
),
),
),
child: Padding(
padding: isSelected ? const EdgeInsets.all(2) : EdgeInsets.zero,
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: 9.69,
cornerSmoothing: 0.4,
),
child: SizedBox(
height: isSelected ? 56 : 60,
width: isSelected ? 56 : 60,
child: editorImage,
),
),
),
),
const SizedBox(height: 8),
Text(
filterName,
style: isSelected
? getEnteTextTheme(context).smallBold
: getEnteTextTheme(context).smallMuted,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,143 @@
import 'dart:math';
import 'package:flutter/material.dart';
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class ImageEditorMainBottomBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorMainBottomBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
});
final ProImageEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
@override
State<ImageEditorMainBottomBar> createState() =>
ImageEditorMainBottomBarState();
}
class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return BottomAppBar(
height: editorBottomBarHeight,
padding: EdgeInsets.zero,
clipBehavior: Clip.none,
child: AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) => Stack(
clipBehavior: Clip.none,
alignment: Alignment.bottomCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
),
duration: const Duration(milliseconds: 400),
reverseDuration: const Duration(milliseconds: 0),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axis: Axis.vertical,
axisAlignment: -1,
child: child,
),
);
},
switchInCurve: Curves.ease,
child: widget.editor.isSubEditorOpen &&
!widget.editor.isSubEditorClosing
? const SizedBox.shrink()
: Align(
alignment: Alignment.center,
child: SingleChildScrollView(
clipBehavior: Clip.none,
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: min(constraints.maxWidth, 600),
maxWidth: 600,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop.svg",
label: "Crop",
onTap: () {
widget.editor.openCropRotateEditor();
},
),
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-filter.svg",
label: "Filter",
onTap: () {
widget.editor.openFilterEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text.svg",
label: "Text",
onTap: () {
widget.editor.openTextEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-tune.svg",
label: "Adjust",
onTap: () {
widget.editor.openTuneEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-paint.svg",
label: "Draw",
onTap: () {
widget.editor.openPaintEditor();
},
),
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-sticker.svg",
label: "Sticker",
onTap: () {
widget.editor.openEmojiEditor();
},
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,513 @@
import "dart:async";
import "dart:io";
import "dart:math";
import "dart:typed_data";
import 'dart:ui' as ui show Image;
import 'package:flutter/material.dart';
import "package:flutter_image_compress/flutter_image_compress.dart";
import "package:google_fonts/google_fonts.dart";
import "package:logging/logging.dart";
import 'package:path/path.dart' as path;
import "package:photo_manager/photo_manager.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/db/files_db.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/events/local_photos_updated_event.dart";
import "package:photos/generated/l10n.dart";
import 'package:photos/models/file/file.dart' as ente;
import "package:photos/models/location/location.dart";
import "package:photos/services/sync/sync_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/action_sheet_widget.dart";
import "package:photos/ui/components/buttons/button_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import "package:photos/ui/notification/toast.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_app_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_crop_rotate.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_filter_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_main_bottom_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_paint_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_text_bar.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_tune_bar.dart";
import "package:photos/ui/viewer/file/detail_page.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/navigation_util.dart";
import "package:pro_image_editor/models/editor_configs/utils/editor_safe_area.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class NewImageEditor extends StatefulWidget {
final ente.EnteFile originalFile;
final File file;
final DetailPageConfiguration detailPageConfig;
const NewImageEditor({
super.key,
required this.file,
required this.originalFile,
required this.detailPageConfig,
});
@override
State<NewImageEditor> createState() => _NewImageEditorState();
}
class _NewImageEditorState extends State<NewImageEditor> {
final _mainEditorBarKey = GlobalKey<ImageEditorMainBottomBarState>();
final editorKey = GlobalKey<ProImageEditorState>();
final _logger = Logger("ImageEditor");
Future<void> saveImage(Uint8List? bytes) async {
if (bytes == null) return;
final dialog = createProgressDialog(context, S.of(context).saving);
await dialog.show();
debugPrint("Image saved with size: ${bytes.length} bytes");
final DateTime start = DateTime.now();
final ui.Image decodedResult = await decodeImageFromList(bytes);
final result = await FlutterImageCompress.compressWithList(
bytes,
minWidth: decodedResult.width,
minHeight: decodedResult.height,
);
_logger.info('Size after compression = ${result.length}');
final Duration diff = DateTime.now().difference(start);
_logger.info('image_editor time : $diff');
try {
final fileName =
path.basenameWithoutExtension(widget.originalFile.title!) +
"_edited_" +
DateTime.now().microsecondsSinceEpoch.toString() +
".JPEG";
//Disabling notifications for assets changing to insert the file into
//files db before triggering a sync.
await PhotoManager.stopChangeNotify();
final AssetEntity newAsset =
await (PhotoManager.editor.saveImage(result, filename: fileName));
final newFile = await ente.EnteFile.fromAsset(
widget.originalFile.deviceFolder ?? '',
newAsset,
);
newFile.creationTime = widget.originalFile.creationTime;
newFile.collectionID = widget.originalFile.collectionID;
newFile.location = widget.originalFile.location;
if (!newFile.hasLocation && widget.originalFile.localID != null) {
final assetEntity = await widget.originalFile.getAsset;
if (assetEntity != null) {
final latLong = await assetEntity.latlngAsync();
newFile.location = Location(
latitude: latLong.latitude,
longitude: latLong.longitude,
);
}
}
newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile);
Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
unawaited(SyncService.instance.sync());
showShortToast(context, S.of(context).editsSaved);
_logger.info("Original file " + widget.originalFile.toString());
_logger.info("Saved edits to file " + newFile.toString());
final files = widget.detailPageConfig.files;
// the index could be -1 if the files fetched doesn't contain the newly
// edited files
int selectionIndex =
files.indexWhere((file) => file.generatedID == newFile.generatedID);
if (selectionIndex == -1) {
files.add(newFile);
selectionIndex = files.length - 1;
}
await dialog.hide();
replacePage(
context,
DetailPage(
widget.detailPageConfig.copyWith(
files: files,
selectedIndex: min(selectionIndex, files.length - 1),
),
),
);
} catch (e, s) {
await dialog.hide();
showToast(context, S.of(context).oopsCouldNotSaveEdits);
_logger.severe(e, s);
} finally {
await PhotoManager.startChangeNotify();
}
}
Future<void> _showExitConfirmationDialog(BuildContext context) async {
final actionResult = await showActionSheet(
context: context,
buttons: [
ButtonWidget(
labelText: S.of(context).yesDiscardChanges,
buttonType: ButtonType.critical,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
isInAlert: true,
),
ButtonWidget(
labelText: S.of(context).no,
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
body: S.of(context).doYouWantToDiscardTheEditsYouHaveMade,
actionSheetType: ActionSheetType.defaultActionSheet,
);
if (actionResult?.action != null &&
actionResult!.action == ButtonAction.first) {
replacePage(context, DetailPage(widget.detailPageConfig));
}
}
@override
Widget build(BuildContext context) {
final isLightMode = Theme.of(context).brightness == Brightness.light;
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
backgroundColor: colorScheme.backgroundBase,
body: ProImageEditor.file(
key: editorKey,
widget.file,
callbacks: ProImageEditorCallbacks(
mainEditorCallbacks: MainEditorCallbacks(
onStartCloseSubEditor: (value) {
_mainEditorBarKey.currentState?.setState(() {});
},
),
),
configs: ProImageEditorConfigs(
layerInteraction: const LayerInteractionConfigs(
hideToolbarOnInteraction: false,
),
theme: ThemeData(
scaffoldBackgroundColor: colorScheme.backgroundBase,
appBarTheme: AppBarTheme(
titleTextStyle: textTheme.body,
backgroundColor: colorScheme.backgroundBase,
),
bottomAppBarTheme: BottomAppBarTheme(
color: colorScheme.backgroundBase,
),
brightness: isLightMode ? Brightness.light : Brightness.dark,
),
mainEditor: MainEditorConfigs(
style: MainEditorStyle(
appBarBackground: colorScheme.backgroundBase,
background: colorScheme.backgroundBase,
bottomBarBackground: colorScheme.backgroundBase,
),
widgets: MainEditorWidgets(
removeLayerArea: (removeAreaKey, editor, rebuildStream) {
return Align(
alignment: Alignment.bottomCenter,
child: StreamBuilder(
stream: rebuildStream,
builder: (_, __) {
final isHovered =
editor.layerInteractionManager.hoverRemoveBtn;
return AnimatedContainer(
key: removeAreaKey,
duration: const Duration(milliseconds: 150),
height: 56,
width: 56,
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
color: isHovered
? const Color.fromARGB(255, 255, 197, 197)
: const Color.fromARGB(255, 255, 255, 255),
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(12),
child: const Center(
child: Icon(
Icons.delete_forever_outlined,
size: 28,
color: Color(0xFFF44336),
),
),
);
},
),
);
},
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
enableRedo: editor.canRedo,
enableUndo: editor.canUndo,
key: const Key('image_editor_app_bar'),
redo: () => editor.redoAction(),
undo: () => editor.undoAction(),
configs: editor.configs,
done: () async {
final Uint8List bytes =
await editorKey.currentState!.captureEditorImage();
await saveImage(bytes);
},
close: () {
_showExitConfirmationDialog(context);
},
isMainEditor: true,
);
},
stream: rebuildStream,
);
},
bottomBar: (editor, rebuildStream, key) => ReactiveCustomWidget(
key: key,
builder: (context) {
return ImageEditorMainBottomBar(
key: _mainEditorBarKey,
editor: editor,
configs: editor.configs,
callbacks: editor.callbacks,
);
},
stream: rebuildStream,
),
),
),
paintEditor: PaintEditorConfigs(
style: PaintEditorStyle(
background: colorScheme.backgroundBase,
initialStrokeWidth: 5,
),
widgets: PaintEditorWidgets(
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
enableRedo: editor.canRedo,
enableUndo: editor.canUndo,
key: const Key('image_editor_app_bar'),
redo: () => editor.redoAction(),
undo: () => editor.undoAction(),
configs: editor.configs,
done: () => editor.done(),
close: () => editor.close(),
);
},
stream: rebuildStream,
);
},
colorPicker:
(paintEditor, rebuildStream, currentColor, setColor) => null,
bottomBar: (editorState, rebuildStream) {
return ReactiveCustomWidget(
builder: (context) {
return ImageEditorPaintBar(
configs: editorState.configs,
callbacks: editorState.callbacks,
editor: editorState,
i18nColor: 'Color',
);
},
stream: rebuildStream,
);
},
),
),
textEditor: TextEditorConfigs(
canToggleTextAlign: true,
customTextStyles: [
GoogleFonts.inter(),
GoogleFonts.giveYouGlory(),
GoogleFonts.dmSerifText(),
GoogleFonts.comicNeue(),
],
safeArea: const EditorSafeArea(
bottom: false,
top: false,
),
style: const TextEditorStyle(
background: Colors.transparent,
textFieldMargin: EdgeInsets.only(top: kToolbarHeight),
),
widgets: TextEditorWidgets(
appBar: (textEditor, rebuildStream) => null,
colorPicker:
(textEditor, rebuildStream, currentColor, setColor) => null,
bottomBar: (editorState, rebuildStream) {
return ReactiveCustomWidget(
builder: (context) {
return ImageEditorTextBar(
configs: editorState.configs,
callbacks: editorState.callbacks,
editor: editorState,
);
},
stream: rebuildStream,
);
},
),
),
cropRotateEditor: CropRotateEditorConfigs(
safeArea: const EditorSafeArea(
bottom: false,
top: false,
),
style: CropRotateEditorStyle(
background: colorScheme.backgroundBase,
cropCornerColor:
Theme.of(context).colorScheme.imageEditorPrimaryColor,
),
widgets: CropRotateEditorWidgets(
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
key: const Key('image_editor_app_bar'),
configs: editor.configs,
done: () => editor.done(),
close: () => editor.close(),
enableRedo: editor.canRedo,
enableUndo: editor.canUndo,
redo: () => editor.redoAction(),
undo: () => editor.undoAction(),
);
},
stream: rebuildStream,
);
},
bottomBar: (cropRotateEditor, rebuildStream) =>
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => ImageEditorCropRotateBar(
configs: cropRotateEditor.configs,
callbacks: cropRotateEditor.callbacks,
editor: cropRotateEditor,
),
),
),
),
filterEditor: FilterEditorConfigs(
fadeInUpDuration: fadeInDuration,
fadeInUpStaggerDelayDuration: fadeInDelay,
safeArea: const EditorSafeArea(top: false),
style: FilterEditorStyle(
filterListSpacing: 7,
background: colorScheme.backgroundBase,
),
widgets: FilterEditorWidgets(
slider: (
editorState,
rebuildStream,
value,
onChanged,
onChangeEnd,
) =>
ReactiveCustomWidget(
builder: (context) {
return const SizedBox.shrink();
},
stream: rebuildStream,
),
filterButton: (
filter,
isSelected,
scaleFactor,
onSelectFilter,
editorImage,
filterKey,
) {
return ImageEditorFilterBar(
filterModel: filter,
isSelected: isSelected,
onSelectFilter: onSelectFilter,
editorImage: editorImage,
filterKey: filterKey,
);
},
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
key: const Key('image_editor_app_bar'),
configs: editor.configs,
done: () => editor.done(),
close: () => editor.close(),
);
},
stream: rebuildStream,
);
},
),
),
tuneEditor: TuneEditorConfigs(
safeArea: const EditorSafeArea(top: false),
style: TuneEditorStyle(
background: colorScheme.backgroundBase,
),
widgets: TuneEditorWidgets(
appBar: (editor, rebuildStream) {
return ReactiveCustomAppbar(
builder: (context) {
return ImageEditorAppBar(
enableRedo: editor.canRedo,
enableUndo: editor.canUndo,
key: const Key('image_editor_app_bar'),
redo: () => editor.redo(),
undo: () => editor.undo(),
configs: editor.configs,
done: () => editor.done(),
close: () => editor.close(),
);
},
stream: rebuildStream,
);
},
bottomBar: (editorState, rebuildStream) {
return ReactiveCustomWidget(
builder: (context) {
return ImageEditorTuneBar(
configs: editorState.configs,
callbacks: editorState.callbacks,
editor: editorState,
);
},
stream: rebuildStream,
);
},
),
),
blurEditor: const BlurEditorConfigs(
enabled: false,
),
emojiEditor: EmojiEditorConfigs(
icons: const EmojiEditorIcons(),
style: EmojiEditorStyle(
backgroundColor: colorScheme.backgroundBase,
emojiViewConfig: const EmojiViewConfig(
gridPadding: EdgeInsets.zero,
horizontalSpacing: 0,
verticalSpacing: 0,
recentsLimit: 40,
loadingIndicator: Center(child: CircularProgressIndicator()),
replaceEmojiOnLimitExceed: false,
),
bottomActionBarConfig: const BottomActionBarConfig(
enabled: false,
),
),
),
stickerEditor: const StickerEditorConfigs(enabled: false),
),
),
);
}
}

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
import "package:pro_image_editor/modules/paint_editor/paint_editor.dart";
import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
class ImageEditorPaintBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorPaintBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
required this.i18nColor,
});
final PaintEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
final String i18nColor;
@override
State<ImageEditorPaintBar> createState() => _ImageEditorPaintBarState();
}
class _ImageEditorPaintBarState extends State<ImageEditorPaintBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
double colorSliderValue = 0.5;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return BottomAppBar(
height: editorBottomBarHeight,
padding: EdgeInsets.zero,
child: Align(
alignment: Alignment.center,
child: FadeInUp(
duration: fadeInDuration,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text(
"Brush Color",
style: getEnteTextTheme(context).body,
),
),
const SizedBox(height: 24),
ImageEditorColorPicker(
value: colorSliderValue,
onChanged: (value) {
setState(() {
colorSliderValue = value;
});
final hue = value * 360;
final color = HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor();
widget.editor.colorChanged(color);
},
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,366 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class ImageEditorTextBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorTextBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
});
final TextEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
@override
State<ImageEditorTextBar> createState() => _ImageEditorTextBarState();
}
class _ImageEditorTextBarState extends State<ImageEditorTextBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
int selectedActionIndex = -1;
double colorSliderValue = 0.5;
void _selectAction(int index) {
setState(() {
selectedActionIndex = selectedActionIndex == index ? -1 : index;
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return BottomAppBar(
padding: EdgeInsets.zero,
height: editorBottomBarHeight,
child: FadeInUp(
duration: fadeInDuration,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildMainActionButtons(),
_buildHelperWidget(),
],
),
),
);
}
Widget _buildMainActionButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-color.svg",
label: "Color",
isSelected: selectedActionIndex == 0,
onTap: () {
_selectAction(0);
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-font.svg",
label: "Font",
isSelected: selectedActionIndex == 1,
onTap: () {
_selectAction(1);
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-background.svg",
label: "Background",
isSelected: selectedActionIndex == 2,
onTap: () {
setState(() {
selectedActionIndex = 2;
});
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-align-left.svg",
label: "Align",
isSelected: selectedActionIndex == 3,
onTap: () {
setState(() {
selectedActionIndex = 3;
});
},
),
],
);
}
Widget _buildHelperWidget() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: switch (selectedActionIndex) {
0 => ImageEditorColorPicker(
value: colorSliderValue,
onChanged: (value) {
setState(() {
colorSliderValue = value;
});
final hue = value * 360;
final color = HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor();
widget.editor.primaryColor = color;
},
),
1 => _FontPickerWidget(editor: widget.editor),
2 => _BackgroundPickerWidget(editor: widget.editor),
3 => _AlignPickerWidget(editor: widget.editor),
_ => const SizedBox.shrink(),
},
);
}
}
class _FontPickerWidget extends StatelessWidget {
final TextEditorState editor;
const _FontPickerWidget({required this.editor});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
if (editor.textEditorConfigs.customTextStyles == null) {
return const SizedBox.shrink();
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: editor.textEditorConfigs.customTextStyles!
.asMap()
.entries
.map((entry) {
final item = entry.value;
final selected = editor.selectedTextStyle;
final bool isSelected = selected.hashCode == item.hashCode;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () {
editor.setTextStyle(item);
},
child: Container(
height: 40,
width: 48,
decoration: BoxDecoration(
color: isSelected
? colorScheme.fillBasePressed
: colorScheme.backgroundElevated2,
borderRadius: BorderRadius.circular(25),
),
child: Center(
child: Text(
'Aa',
style: item.copyWith(
color: isSelected
? colorScheme.backdropBase
: colorScheme.tabIcon,
),
),
),
),
),
);
}).toList(),
);
}
}
class _BackgroundPickerWidget extends StatelessWidget {
final TextEditorState editor;
const _BackgroundPickerWidget({required this.editor});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final isLightMode = Theme.of(context).brightness == Brightness.light;
final backgroundStyles = {
LayerBackgroundMode.background: {
'text': 'Aa',
'backgroundColor': isLightMode ? colorScheme.fillFaint : Colors.white,
'border': null,
'textColor': Colors.white,
'innerBackgroundColor': Colors.black,
},
LayerBackgroundMode.backgroundAndColor: {
'text': 'Aa',
'backgroundColor': isLightMode ? colorScheme.fillFaint : Colors.white,
'border': null,
'textColor': Colors.black,
'innerBackgroundColor': Colors.transparent,
},
LayerBackgroundMode.backgroundAndColorWithOpacity: {
'text': 'Aa',
'backgroundColor': isLightMode ? colorScheme.fillFaint : Colors.white,
'border': null,
'textColor': Colors.black,
'innerBackgroundColor': Colors.black.withOpacity(0.11),
},
LayerBackgroundMode.onlyColor: {
'text': 'Aa',
'backgroundColor': isLightMode ? colorScheme.fillFaint : Colors.black,
'border':
isLightMode ? null : Border.all(color: Colors.white, width: 2),
'textColor': Colors.black,
'innerBackgroundColor': Colors.white,
},
};
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: backgroundStyles.entries.map((entry) {
final mode = entry.key;
final style = entry.value;
final isSelected = editor.backgroundColorMode == mode;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () {
editor.setState(() {
editor.backgroundColorMode = mode;
});
},
child: SizedBox(
height: 40,
width: 48,
child: Container(
decoration: BoxDecoration(
color: isSelected
? style['backgroundColor'] as Color
: colorScheme.backgroundElevated2,
borderRadius: BorderRadius.circular(25),
border: isSelected ? style['border'] as Border? : null,
),
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: Container(
height: 20,
width: 22,
decoration: ShapeDecoration(
color: isSelected
? style['innerBackgroundColor'] as Color
: colorScheme.backgroundElevated2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
),
),
Center(
child: Text(
style['text'] as String,
style: TextStyle(
color: isSelected
? style['textColor'] as Color
: colorScheme.tabIcon,
),
),
),
],
),
),
),
),
);
}).toList(),
);
}
}
class _AlignPickerWidget extends StatelessWidget {
final TextEditorState editor;
const _AlignPickerWidget({required this.editor});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final alignments = [
(TextAlign.left, "assets/image-editor/image-editor-text-align-left.svg"),
(
TextAlign.center,
"assets/image-editor/image-editor-text-align-center.svg"
),
(
TextAlign.right,
"assets/image-editor/image-editor-text-align-right.svg"
),
];
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: alignments.map((alignmentData) {
final (alignment, svgPath) = alignmentData;
final isSelected = editor.align == alignment;
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: GestureDetector(
onTap: () {
editor.setState(() {
editor.align = alignment;
});
},
child: Container(
height: 40,
width: 48,
decoration: BoxDecoration(
color: isSelected
? colorScheme.fillBasePressed
: colorScheme.backgroundElevated2,
borderRadius: BorderRadius.circular(25),
border: isSelected
? Border.all(color: Colors.black, width: 2)
: null,
),
child: Center(
child: SvgPicture.asset(
svgPath,
width: 22,
height: 22,
fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
isSelected ? colorScheme.backdropBase : colorScheme.tabIcon,
BlendMode.srcIn,
),
),
),
),
),
);
}).toList(),
);
}
}

View File

@@ -0,0 +1,649 @@
import 'dart:math';
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
import "package:pro_image_editor/mixins/converted_configs.dart";
import "package:pro_image_editor/models/editor_callbacks/pro_image_editor_callbacks.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
import "package:pro_image_editor/modules/tune_editor/tune_editor.dart";
import "package:pro_image_editor/widgets/animated/fade_in_up.dart";
class ImageEditorTuneBar extends StatefulWidget with SimpleConfigsAccess {
const ImageEditorTuneBar({
super.key,
required this.configs,
required this.callbacks,
required this.editor,
});
final TuneEditorState editor;
@override
final ProImageEditorConfigs configs;
@override
final ProImageEditorCallbacks callbacks;
@override
State<ImageEditorTuneBar> createState() => _ImageEditorTuneBarState();
}
class _ImageEditorTuneBarState extends State<ImageEditorTuneBar>
with ImageEditorConvertedConfigs, SimpleConfigsAccessState {
TuneEditorState get tuneEditor => widget.editor;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFunctions(constraints),
],
);
},
);
}
Widget _buildFunctions(BoxConstraints constraints) {
return SizedBox(
width: double.infinity,
height: editorBottomBarHeight,
child: FadeInUp(
duration: fadeInDuration,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
height: 90,
child: SingleChildScrollView(
controller: tuneEditor.bottomBarScrollCtrl,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: List.generate(
tuneEditor.tuneAdjustmentMatrix.length, (index) {
final item = tuneEditor.tuneAdjustmentList[index];
return TuneItem(
icon: item.icon,
label: item.label,
isSelected: tuneEditor.selectedIndex == index,
value: tuneEditor.tuneAdjustmentMatrix[index].value,
max: item.max,
min: item.min,
onTap: () {
tuneEditor.setState(() {
tuneEditor.selectedIndex = index;
});
},
);
}),
),
),
),
RepaintBoundary(
child: StreamBuilder(
stream: tuneEditor.uiStream.stream,
builder: (context, snapshot) {
final activeOption =
tuneEditor.tuneAdjustmentList[tuneEditor.selectedIndex];
final activeMatrix =
tuneEditor.tuneAdjustmentMatrix[tuneEditor.selectedIndex];
return _TuneAdjustWidget(
min: activeOption.min,
max: activeOption.max,
value: activeMatrix.value,
onChanged: tuneEditor.onChanged,
);
},
),
),
],
),
),
);
}
}
class TuneItem extends StatelessWidget {
final IconData icon;
final String label;
final bool isSelected;
final double value;
final double min;
final double max;
final VoidCallback onTap;
const TuneItem({
super.key,
required this.icon,
required this.label,
required this.isSelected,
required this.value,
required this.min,
required this.max,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
width: 90,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressWithValue(
value: value,
min: min,
max: max,
size: 60,
icon: icon,
isSelected: isSelected,
progressColor:
Theme.of(context).colorScheme.imageEditorPrimaryColor,
svgPath:
"assets/image-editor/image-editor-${label.toLowerCase()}.svg",
),
const SizedBox(height: 8),
Text(
label,
style: getEnteTextTheme(context).small,
textAlign: TextAlign.center,
),
],
),
),
);
}
}
class CircularProgressWithValue extends StatefulWidget {
final double value;
final double min;
final double max;
final IconData icon;
final bool isSelected;
final double size;
final Color progressColor;
final String? svgPath;
const CircularProgressWithValue({
super.key,
required this.value,
required this.min,
required this.max,
required this.icon,
required this.progressColor,
this.isSelected = false,
this.size = 60,
this.svgPath,
});
@override
State<CircularProgressWithValue> createState() =>
_CircularProgressWithValueState();
}
class _CircularProgressWithValueState extends State<CircularProgressWithValue>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _progressAnimation;
double _previousValue = 0.0;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
animationBehavior: AnimationBehavior.preserve,
);
_progressAnimation = Tween<double>(
begin: 0.0,
end: widget.value,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_animationController.forward();
}
@override
void didUpdateWidget(CircularProgressWithValue oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.value != widget.value) {
_previousValue = oldWidget.value;
_progressAnimation = Tween<double>(
begin: _previousValue,
end: widget.value,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_animationController.forward(from: 0.0);
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
int _normalizeValueForDisplay(double value, double min, double max) {
if (min == -0.5 && max == 0.5) {
return (value * 200).round();
} else if (min == 0 && max == 1) {
return (value * 100).round();
} else if (min == -0.25 && max == 0.25) {
return (value * 400).round();
} else {
return (value * 100).round();
}
}
double _normalizeValueForProgress(double value, double min, double max) {
if (min == -0.5 && max == 0.5) {
return (value.abs() / 0.5).clamp(0.0, 1.0);
} else if (min == 0 && max == 1) {
return (value / 1.0).clamp(0.0, 1.0);
} else if (min == -0.25 && max == 0.25) {
return (value.abs() / 0.25).clamp(0.0, 1.0);
} else {
return (value.abs() / 1.0).clamp(0.0, 1.0);
}
}
bool _isClockwise(double value, double min, double max) {
if (min >= 0) {
return true;
} else {
return value >= 0;
}
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
final displayValue =
_normalizeValueForDisplay(widget.value, widget.min, widget.max);
final displayText = displayValue.toString();
final prefix = displayValue > 0 ? "+" : "";
final progressColor = widget.progressColor;
final showValue = displayValue != 0 || widget.isSelected;
return SizedBox(
width: widget.size,
height: widget.size,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: showValue || widget.isSelected
? progressColor.withOpacity(0.2)
: colorTheme.backgroundElevated2,
border: Border.all(
color: widget.isSelected
? progressColor.withOpacity(0.4)
: colorTheme.backgroundElevated2,
width: 2,
),
),
),
AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
final animatedValue = _progressAnimation.value;
final isClockwise =
_isClockwise(animatedValue, widget.min, widget.max);
final progressValue = _normalizeValueForProgress(
animatedValue,
widget.min,
widget.max,
);
return SizedBox(
width: widget.size,
height: widget.size,
child: CustomPaint(
painter: CircularProgressPainter(
progress: progressValue,
isClockwise: isClockwise,
color: progressColor,
),
),
);
},
),
Align(
alignment: Alignment.center,
child: showValue
? Text(
"$prefix$displayText",
style: textTheme.smallBold,
)
: widget.svgPath != null
? SvgPicture.asset(
widget.svgPath!,
width: 22,
height: 22,
fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
colorTheme.tabIcon,
BlendMode.srcIn,
),
)
: Icon(
widget.icon,
color: colorTheme.tabIcon,
size: 20,
),
),
],
),
);
}
}
class _TuneAdjustWidget extends StatelessWidget {
final double min;
final double max;
final double value;
final ValueChanged<double> onChanged;
const _TuneAdjustWidget({
required this.min,
required this.max,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return SizedBox(
height: 40,
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: colorScheme.backgroundElevated2,
),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
thumbShape: const _ColorPickerThumbShape(),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 0),
activeTrackColor:
Theme.of(context).colorScheme.imageEditorPrimaryColor,
inactiveTrackColor: colorScheme.backgroundElevated2,
trackShape: const _CenterBasedTrackShape(),
trackHeight: 24,
),
child: Slider(
value: value,
onChanged: onChanged,
min: min,
max: max,
),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 38),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.fillBase.withAlpha(30),
),
),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.fillBase.withAlpha(30),
),
),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.fillBase.withAlpha(30),
),
),
],
),
),
],
),
);
}
}
class _ColorPickerThumbShape extends SliderComponentShape {
const _ColorPickerThumbShape();
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return const Size(20, 20);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required Size sizeWithOverflow,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double textScaleFactor,
required double value,
}) {
final canvas = context.canvas;
final trackRect = sliderTheme.trackShape!.getPreferredRect(
parentBox: parentBox,
offset: Offset.zero,
sliderTheme: sliderTheme,
isEnabled: true,
isDiscrete: isDiscrete,
);
final constrainedCenter = Offset(
center.dx.clamp(trackRect.left + 15, trackRect.right - 15),
center.dy,
);
final paint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
canvas.drawCircle(constrainedCenter, 15, paint);
final innerPaint = Paint()
..color = const Color.fromRGBO(8, 194, 37, 1)
..style = PaintingStyle.fill;
canvas.drawCircle(constrainedCenter, 12.5, innerPaint);
}
}
class _CenterBasedTrackShape extends SliderTrackShape {
const _CenterBasedTrackShape();
static const double horizontalPadding = 6.0;
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final double trackHeight = sliderTheme.trackHeight ?? 8;
final double trackLeft = offset.dx + horizontalPadding;
final double trackTop =
offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackWidth = parentBox.size.width - (horizontalPadding * 2);
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
Offset? secondaryOffset,
bool isEnabled = false,
bool isDiscrete = false,
double? additionalActiveTrackHeight,
}) {
final Canvas canvas = context.canvas;
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final double centerX = trackRect.left + trackRect.width / 2;
final double clampedThumbDx = thumbCenter.dx.clamp(
trackRect.left,
trackRect.right,
);
final Paint inactivePaint = Paint()
..color = sliderTheme.inactiveTrackColor!
..style = PaintingStyle.fill;
final RRect inactiveRRect = RRect.fromRectAndRadius(
trackRect,
Radius.circular(trackRect.height / 2),
);
canvas.drawRRect(inactiveRRect, inactivePaint);
if (clampedThumbDx != centerX) {
final Paint activePaint = Paint()
..color = sliderTheme.activeTrackColor!
..style = PaintingStyle.fill;
final Rect activeRect = clampedThumbDx >= centerX
? Rect.fromLTWH(
centerX,
trackRect.top,
clampedThumbDx - centerX,
trackRect.height,
)
: Rect.fromLTWH(
clampedThumbDx,
trackRect.top,
centerX - clampedThumbDx,
trackRect.height,
);
final RRect activeRRect = RRect.fromRectAndRadius(
activeRect,
Radius.circular(trackRect.height / 2),
);
canvas.drawRRect(activeRRect, activePaint);
}
}
}
class CircularProgressPainter extends CustomPainter {
final double progress;
final bool isClockwise;
final Color color;
CircularProgressPainter({
required this.progress,
required this.isClockwise,
required this.color,
});
@override
void paint(Canvas canvas, Size size) {
const strokeWidth = 2.5;
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width - strokeWidth) / 2;
final backgroundPaint = Paint()
..color = Colors.transparent
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
final foregroundPaint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, backgroundPaint);
if (progress > 0) {
const startAngle = -pi / 2;
final sweepAngle = 2 * pi * progress * (isClockwise ? 1 : -1);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
foregroundPaint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}