Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions lib/src/core/pro_image_editor/story_editor_profile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:pro_image_editor/pro_image_editor.dart';
import 'package:spark/src/core/pro_image_editor/story_sticker_editor.dart';
import 'package:spark/src/core/pro_image_editor/ui/widgets/story_editor_bottom_section.dart';
import 'package:spark/src/core/pro_image_editor/ui/widgets/story_editor_top_section.dart';

/// Canonical product-profile configuration shared by Story image and video
/// editors.
class StoryEditorProfile {
const StoryEditorProfile._();

static const safeArea = EditorSafeArea.symmetric(vertical: true);
static const outsideCaptureAreaLayerOpacity = 0.0;
static const tools = [
SubEditorMode.paint,
SubEditorMode.text,
SubEditorMode.filter,
SubEditorMode.blur,
SubEditorMode.emoji,
SubEditorMode.sticker,
];

static const _previewBorderRadius = BorderRadius.vertical(
top: Radius.circular(20),
bottom: Radius.circular(20),
);

static MainEditorWidgets buildMainEditorWidgets({
Future<void> Function()? onMention,
void Function(ProImageEditorState editor)? onDone,
Widget Function(ProImageEditorState editor)? contextualControlBuilder,
}) {
return MainEditorWidgets(
removeLayerArea:
(removeAreaKey, editor, rebuildStream, isLayerBeingTransformed) =>
VideoEditorRemoveArea(
removeAreaKey: removeAreaKey,
editor: editor,
rebuildStream: rebuildStream,
isLayerBeingTransformed: isLayerBeingTransformed,
),
appBar: (editor, rebuildStream) => null,
bottomBar: (editor, rebuildStream, key) => ReactiveWidget(
key: key,
stream: rebuildStream,
builder: (_) => StoryEditorBottomSection(
onShare: onDone != null ? () => onDone(editor) : editor.doneEditing,
contextualControl: contextualControlBuilder?.call(editor),
),
),
wrapBody: (editor, rebuildStream, content) => ClipRRect(
borderRadius: _previewBorderRadius,
child: ColoredBox(color: Colors.black, child: content),
),
bodyItems: (editor, rebuildStream) => [
ReactiveWidget(
stream: rebuildStream,
builder: (_) => Positioned(
top: 0,
left: 0,
right: 0,
child: StoryEditorTopSection(
onClose: editor.closeEditor,
onMention: onMention,
onPaint: editor.openPaintEditor,
onText: editor.openTextEditor,
onFilter: editor.openFilterEditor,
onBlur: editor.openBlurEditor,
onEmoji: editor.openEmojiEditor,
onStickers: () => openStoryStickerEditor(editor),
),
),
),
],
);
}
}
79 changes: 12 additions & 67 deletions lib/src/core/pro_image_editor/story_image_editor_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,14 @@ import 'package:pro_image_editor/pro_image_editor.dart';
import 'package:spark/src/core/design_system/theme/color_scheme.dart';
import 'package:spark/src/core/design_system/theme/text_theme.dart';
import 'package:spark/src/core/design_system/tokens/colors.dart';
import 'package:spark/src/core/pro_image_editor/ui/widgets/story_editor_bottom_section.dart';
import 'package:spark/src/core/pro_image_editor/ui/widgets/story_editor_header.dart';
import 'package:spark/src/core/pro_image_editor/story_editor_profile.dart';
import 'package:spark/src/core/pro_video_editor/ui/widgets/blur/blur_editor_bar.dart';
import 'package:spark/src/core/pro_video_editor/ui/widgets/common/build_stickers.dart';
import 'package:spark/src/core/pro_video_editor/ui/widgets/filter/filter_editor_bar.dart';
import 'package:spark/src/core/pro_video_editor/ui/widgets/paint/paint_editor_bar.dart';
import 'package:spark/src/core/pro_video_editor/ui/widgets/text/text_editor_bar.dart';
import 'package:spark/src/core/pro_video_editor/ui/widgets/text/text_editor_color_picker.dart';

/// Border radius for the story editor preview area (top and bottom).
const _storyEditorBorderRadius = BorderRadius.vertical(
top: Radius.circular(20),
bottom: Radius.circular(20),
);

/// Configuration builder for the Story Image Editor.
///
/// Creates a fixed 9:16 aspect ratio editor optimized for stories.
Expand Down Expand Up @@ -60,72 +53,21 @@ class StoryImageEditorConfigs {
maxOutputSize: storySize,
),
mainEditor: MainEditorConfigs(
// Story-appropriate tools only - NO crop/rotate
tools: const [
SubEditorMode.paint,
SubEditorMode.text,
SubEditorMode.filter,
SubEditorMode.blur,
SubEditorMode.emoji,
SubEditorMode.sticker,
],
widgets: MainEditorWidgets(
removeLayerArea:
(removeAreaKey, editor, rebuildStream, isLayerBeingTransformed) =>
VideoEditorRemoveArea(
removeAreaKey: removeAreaKey,
editor: editor,
rebuildStream: rebuildStream,
isLayerBeingTransformed: isLayerBeingTransformed,
),
appBar: (editor, rebuildStream) => null,
bottomBar: (editor, rebuildStream, key) => ReactiveWidget(
key: key,
builder: (_) =>
StoryEditorBottomSection(editor: editor, onMention: onMention),
stream: rebuildStream,
),
wrapBody: (editor, rebuildStream, content) {
return ClipRRect(
borderRadius: _storyEditorBorderRadius,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black,
child: content,
),
);
},
bodyItems: (editor, rebuildStream) => [
ReactiveWidget(
stream: rebuildStream,
builder: (_) => Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
bottom: false,
child: StoryEditorHeader(
onBack: editor.closeEditor,
onDone: onDone != null
? () => onDone(editor)
: editor.doneEditing,
canUndo: editor.canUndo,
canRedo: editor.canRedo,
onUndo: editor.undoAction,
onRedo: editor.redoAction,
),
),
),
),
],
safeArea: StoryEditorProfile.safeArea,
tools: StoryEditorProfile.tools,
widgets: StoryEditorProfile.buildMainEditorWidgets(
onMention: onMention,
onDone: onDone,
),
style: const MainEditorStyle(
background: Colors.black,
bottomBarBackground: AppColors.grey800,
outsideCaptureAreaLayerOpacity:
StoryEditorProfile.outsideCaptureAreaLayerOpacity,
),
),
paintEditor: PaintEditorConfigs(
safeArea: StoryEditorProfile.safeArea,
style: const PaintEditorStyle(
background: AppColors.greyBlack,
bottomBarBackground: AppColors.grey800,
Expand All @@ -152,6 +94,7 @@ class StoryImageEditorConfigs {
),
),
textEditor: TextEditorConfigs(
safeArea: StoryEditorProfile.safeArea,
customTextStyles: [
GoogleFonts.roboto(),
GoogleFonts.averiaLibre(),
Expand Down Expand Up @@ -207,6 +150,7 @@ class StoryImageEditorConfigs {
),
),
filterEditor: FilterEditorConfigs(
safeArea: StoryEditorProfile.safeArea,
style: const FilterEditorStyle(
filterListSpacing: 7,
filterListMargin: EdgeInsets.fromLTRB(8, 0, 8, 8),
Expand Down Expand Up @@ -240,6 +184,7 @@ class StoryImageEditorConfigs {
),
),
blurEditor: BlurEditorConfigs(
safeArea: StoryEditorProfile.safeArea,
maxBlur: 25,
style: const BlurEditorStyle(background: AppColors.greyBlack),
widgets: BlurEditorWidgets(
Expand Down
51 changes: 51 additions & 0 deletions lib/src/core/pro_image_editor/story_sticker_editor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:pro_image_editor/pro_image_editor.dart';

/// Opens the Story sticker picker edge-to-edge while preserving the editor's
/// selection, keyboard, and callback lifecycle.
Future<void> openStoryStickerEditor(ProImageEditorState editor) async {
final configs = editor.configs;
final stickerStyle = configs.stickerEditor.style;
final sheetStyle = stickerStyle.draggableSheetStyle;
final constraints = stickerStyle.editorBoxConstraintsBuilder?.call(
editor.context,
configs,
);

editor
..unselectAllLayers()
..removeKeyEventListener();
editor.interactiveViewer.currentState?.setEnableInteraction(true);

WidgetLayer? layer;
try {
layer = await showModalBottomSheet<WidgetLayer>(
context: editor.context,
backgroundColor: Colors.transparent,
constraints: constraints,
isScrollControlled: true,
showDragHandle: stickerStyle.showDragHandle,
useSafeArea: false,
builder: (context) => DraggableScrollableSheet(
expand: sheetStyle.expand,
initialChildSize: sheetStyle.initialChildSize,
maxChildSize: sheetStyle.maxChildSize,
minChildSize: sheetStyle.minChildSize,
shouldCloseOnMinExtent: sheetStyle.shouldCloseOnMinExtent,
snap: sheetStyle.snap,
snapAnimationDuration: sheetStyle.snapAnimationDuration,
snapSizes: sheetStyle.snapSizes,
builder: (_, scrollController) => StickerEditor(
configs: configs,
callbacks: editor.callbacks,
scrollController: scrollController,
),
),
);
} finally {
if (editor.mounted) editor.initKeyEventListener();
}

if (layer == null || !editor.mounted) return;
editor.addLayer(layer);
}
16 changes: 3 additions & 13 deletions lib/src/core/pro_image_editor/ui/story_image_editor_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,7 @@ class _StoryImageEditorPageState extends State<StoryImageEditorPage>
}
}

void _onCloseEditor(EditorMode editorMode) {
if (editorMode == EditorMode.main) {
Navigator.of(context).pop();
}
}
void _onCloseEditor(EditorMode _) => Navigator.of(context).pop();

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -323,11 +319,7 @@ class _StoryBlankCanvasEditorPageState extends State<StoryBlankCanvasEditorPage>
}
}

void _onCloseEditor(EditorMode editorMode) {
if (editorMode == EditorMode.main) {
Navigator.of(context).pop();
}
}
void _onCloseEditor(EditorMode _) => Navigator.of(context).pop();

double _computeInitialBackgroundScale(Size previewSize) {
final imageSize = _imageSize;
Expand Down Expand Up @@ -439,10 +431,8 @@ class _StoryBlankCanvasEditorPageState extends State<StoryBlankCanvasEditorPage>
initStateHistory: _initialStateHistory,
),
mainEditor: _configs.mainEditor.copyWith(
style: MainEditorStyle(
style: _configs.mainEditor.style.copyWith(
background: widget.backgroundColor,
bottomBarBackground:
_configs.mainEditor.style.bottomBarBackground,
),
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import 'package:flutter/material.dart';
import 'package:pro_image_editor/pro_image_editor.dart';
import 'package:spark/src/core/design_system/components/atoms/buttons/app_button.dart';
import 'package:spark/src/core/design_system/tokens/colors.dart';
import 'package:spark/src/core/pro_image_editor/ui/widgets/story_editor_toolbar.dart';
import 'package:spark/src/core/l10n/app_localizations.dart';

/// Bottom section for the Story Image Editor.
/// Bottom section for Story image and video editors.
///
/// Contains the toolbar with editing tools.
/// Keeps contextual video controls above an Instagram-style share action.
class StoryEditorBottomSection extends StatelessWidget {
const StoryEditorBottomSection({
required this.editor,
this.onMention,
required this.onShare,
this.contextualControl,
super.key,
});

final ProImageEditorState editor;
final Future<void> Function()? onMention;
final VoidCallback onShare;
final Widget? contextualControl;

@override
Expand All @@ -28,14 +26,16 @@ class StoryEditorBottomSection extends StatelessWidget {
?contextualControl,
SafeArea(
top: false,
child: StoryEditorToolbar(
onMention: onMention,
onPaint: editor.openPaintEditor,
onText: editor.openTextEditor,
onFilter: editor.openFilterEditor,
onBlur: editor.openBlurEditor,
onEmoji: editor.openEmojiEditor,
onStickers: editor.openStickerEditor,
minimum: const EdgeInsets.fromLTRB(16, 10, 16, 12),
child: AppButton(
key: const ValueKey('story-editor-share-button'),
label: AppLocalizations.of(context).buttonShare,
onPressed: onShare,
size: AppButtonSize.large,
fullWidth: true,
minHeight: 54,
borderRadius: BorderRadius.circular(999),
trailing: const Icon(Icons.arrow_forward_rounded, size: 22),
),
),
],
Expand Down
Loading
Loading