Implemented new image editor
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user