From 8199139b90f25447b87e7d7dd07b88d67b0e12f7 Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Wed, 2 Jul 2025 16:10:47 +0800 Subject: [PATCH 01/12] feat: cover image reposition add baisc code --- .../editor_plugins/header/desktop_cover.dart | 71 ++++-- .../header/desktop_cover_align.dart | 219 ++++++++++++++++++ .../header/document_cover_widget.dart | 181 +++++++++++++-- .../lib/shared/appflowy_network_image.dart | 6 + 4 files changed, 448 insertions(+), 29 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart index 7265ef6f82..7429b9bfd0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart'; @@ -24,6 +25,8 @@ class DesktopCover extends StatefulWidget { required this.node, required this.coverType, this.coverDetails, + this.enableAlign = false, + this.onAlignControllerCreated, }); final ViewPB view; @@ -31,7 +34,9 @@ class DesktopCover extends StatefulWidget { final EditorState editorState; final CoverType coverType; final String? coverDetails; - + final bool enableAlign; + final Function(DesktopCoverAlignController? alignController)? + onAlignControllerCreated; @override State createState() => _DesktopCoverState(); } @@ -42,6 +47,19 @@ class _DesktopCoverState extends State { ); String? get coverDetails => widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; + String? get coverAlign => + widget.node.attributes[DocumentHeaderBlockKeys.align]; + + late final DesktopCoverAlignController coverAlignController; + + @override + void initState() { + super.initState(); + coverAlignController = DesktopCoverAlignController(coverAlign); + if (widget.onAlignControllerCreated != null) { + widget.onAlignControllerCreated!(coverAlignController); + } + } @override Widget build(BuildContext context) { @@ -74,6 +92,13 @@ class _DesktopCoverState extends State { child: FlowyNetworkImage( url: cover.value, userProfilePB: userProfilePB, + imageBuilder: (context, provider) { + return DesktopCoverAlign( + controller: coverAlignController, + imageProvider: provider, + enableAlign: widget.enableAlign, + ); + }, ), ); } @@ -82,9 +107,12 @@ class _DesktopCoverState extends State { return SizedBox( height: height, width: double.infinity, - child: Image.asset( - PageStyleCoverImageType.builtInImagePath(cover.value), - fit: BoxFit.cover, + child: DesktopCoverAlign( + controller: coverAlignController, + imageProvider: AssetImage( + PageStyleCoverImageType.builtInImagePath(cover.value), + ), + enableAlign: widget.enableAlign, ), ); } @@ -115,9 +143,12 @@ class _DesktopCoverState extends State { return SizedBox( height: height, width: double.infinity, - child: Image.file( - File(cover.value), - fit: BoxFit.cover, + child: DesktopCoverAlign( + controller: coverAlignController, + imageProvider: FileImage( + File(cover.value), + ), + enableAlign: widget.enableAlign, ), ); } @@ -134,6 +165,7 @@ class _DesktopCoverState extends State { if (detail == null) { return const SizedBox.shrink(); } + switch (widget.coverType) { case CoverType.file: if (isURL(detail)) { @@ -144,20 +176,33 @@ class _DesktopCoverState extends State { userProfilePB: userProfilePB, errorWidgetBuilder: (context, url, error) => const SizedBox.shrink(), + imageBuilder: (context, provider) { + return DesktopCoverAlign( + controller: coverAlignController, + imageProvider: provider, + enableAlign: widget.enableAlign , + ); + }, ); } final imageFile = File(detail); if (!imageFile.existsSync()) { return const SizedBox.shrink(); } - return Image.file( - imageFile, - fit: BoxFit.cover, + final provider = FileImage(imageFile); + return DesktopCoverAlign( + controller: coverAlignController, + imageProvider: provider, + enableAlign: widget.enableAlign, ); + case CoverType.asset: - return Image.asset( - PageStyleCoverImageType.builtInImagePath(detail), - fit: BoxFit.cover, + final provider = + AssetImage(PageStyleCoverImageType.builtInImagePath(detail)); + return DesktopCoverAlign( + controller: coverAlignController, + imageProvider: provider, + enableAlign: widget.enableAlign, ); case CoverType.color: final color = widget.coverDetails?.tryToColor() ?? Colors.white; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart new file mode 100644 index 0000000000..6d502a9e63 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; + +class DesktopCoverAlignController extends ChangeNotifier { + DesktopCoverAlignController(String? offset) { + double x = 0; + double y = 0; + if (offset != null) { + final splits = offset.split(','); + + try { + x = double.parse(splits.first); + } catch (e) { + x = 0; + } + try { + y = double.parse(splits.last); + } catch (e) { + y = 0; + } + } + + _initialAlignment = Alignment(x, y); + _adjustedAlign = _initialAlignment; + } + + late final Alignment _initialAlignment; + + late Alignment _adjustedAlign; + + Alignment get alignment => _adjustedAlign; + + void reset() { + _adjustedAlign = Alignment.center; + } + + void cancel() { + _adjustedAlign = _initialAlignment; + notifyListeners(); + } + + void changeAlign(double x, double y) { + _adjustedAlign = Alignment(x, y); + } + + bool get isModified => _adjustedAlign != _initialAlignment; + + String getAlignAttribute() { + return "${_adjustedAlign.x.toStringAsFixed(1)},${_adjustedAlign.y.toStringAsFixed(1)}"; + } +} + +class DesktopCoverAlign extends StatefulWidget { + const DesktopCoverAlign({ + super.key, + required this.controller, + required this.imageProvider, + this.fit = BoxFit.cover, + this.enableAlign = false, + }); + final DesktopCoverAlignController controller; + final ImageProvider imageProvider; + final BoxFit fit; + final bool enableAlign; + + @override + State createState() => _DesktopCoverAlignState(); +} + +class _DesktopCoverAlignState extends State { + ImageStreamListener? _imageStreamListener; + ImageStream? _imageStream; + ImageInfo? _imageInfo; + Size? _imageSize; + + Size? _frameSize; + + double x = 0; + double y = 0; + late final DesktopCoverAlignController controller; + + @override + void initState() { + super.initState(); + controller = widget.controller; + final alignment = controller.alignment; + x = alignment.x; + y = alignment.y; + controller.addListener(() { + setState(() { + x = controller.alignment.x; + y = controller.alignment.y; + }); + }); + } + + @override + void dispose() { + super.dispose(); + _stopImageStream(); + } + + @override + void didChangeDependencies() { + _resolveImage(); + super.didChangeDependencies(); + } + + @override + void didUpdateWidget(DesktopCoverAlign oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.imageProvider != oldWidget.imageProvider) { + _resolveImage(); + } + } + + void _resolveImage() { + final ImageStream newStream = widget.imageProvider.resolve( + const ImageConfiguration(), + ); + _updateSourceStream(newStream); + } + + ImageStreamListener _getOrCreateListener() { + void handleImageFrame(ImageInfo info, bool synchronousCall) { + void setupCB() { + _imageSize = Size( + info.image.width.toDouble(), + info.image.height.toDouble(), + ); + _imageInfo = _imageInfo; + } + + synchronousCall ? setupCB() : setState(setupCB); + } + + _imageStreamListener = ImageStreamListener( + handleImageFrame, + ); + + return _imageStreamListener!; + } + + void _updateSourceStream(ImageStream newStream) { + if (_imageStream?.key == newStream.key) { + return; + } + _imageStream?.removeListener(_imageStreamListener!); + _imageStream = newStream; + _imageStream!.addListener(_getOrCreateListener()); + } + + void _stopImageStream() { + _imageStream?.removeListener(_imageStreamListener!); + } + + void _changeAlignOffset(Offset offset) { + setState(() { + if (_imageSize == null || _frameSize == null) return; + + final imageRatio = _imageSize!.aspectRatio; + final frameRatio = _frameSize!.aspectRatio; + final isVertical = imageRatio < frameRatio; + + final imageFrameHeight = + _frameSize!.width / _imageSize!.width * _imageSize!.height; + final imageFrameWidth = + _frameSize!.height / _imageSize!.height * _imageSize!.width; + final exceedWidth = imageFrameWidth - _frameSize!.width; + final exceedHeight = imageFrameHeight - _frameSize!.height; + + if (isVertical) { + final targetY = y + offset.dy / exceedHeight * 2; + if (targetY >= -1 && targetY <= 1) { + y = targetY; + } + } else { + final targetX = x + offset.dx / exceedWidth * 2; + if (targetX >= -1 && targetX <= 1) { + x = targetX; + } + } + widget.controller.changeAlign(x, y); + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + _frameSize = + Size(constraints.biggest.width, constraints.biggest.height); + _imageSize ??= _frameSize; + + Widget child = Image( + image: widget.imageProvider, + width: _frameSize!.width, + height: _frameSize!.height, + fit: widget.fit, + alignment: Alignment(-x, -y), + ); + if (widget.enableAlign && _imageSize != null) { + child = GestureDetector( + onHorizontalDragUpdate: (details) { + final delta = details.delta; + _changeAlignOffset(delta); + }, + onVerticalDragUpdate: (details) { + final delta = details.delta; + _changeAlignOffset(delta); + }, + child: child, + ); + } + return child; + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 7c8287e550..87ba0aef0e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; @@ -32,6 +33,7 @@ import 'package:string_validator/string_validator.dart'; import 'package:universal_platform/universal_platform.dart'; import 'cover_title.dart'; +import 'desktop_cover_align.dart'; const double kCoverHeight = 280.0; const double kIconHeight = 60.0; @@ -44,6 +46,7 @@ class DocumentHeaderBlockKeys { static const String coverType = 'cover_selection_type'; static const String coverDetails = 'cover_selection'; static const String icon = 'selected_icon'; + static const String align = 'cover_selection_align'; } // for the version under 0.5.5, including 0.5.5 @@ -187,8 +190,8 @@ class _DocumentCoverWidgetState extends State { node: widget.node, coverType: coverType, coverDetails: coverDetails, - onChangeCover: (type, details) => - _saveIconOrCover(cover: (type, details)), + onChangeCover: (type, details, align) => + _saveIconOrCover(cover: (type, details, align)), ), _buildAlignedCoverIcon(context), ], @@ -301,7 +304,7 @@ class _DocumentCoverWidgetState extends State { } void _saveIconOrCover({ - (CoverType, String?)? cover, + (CoverType, String?, String?)? cover, EmojiIconData? icon, }) async { if (!widget.editorState.editable) { @@ -322,6 +325,7 @@ class _DocumentCoverWidgetState extends State { if (cover != null) { attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString(); attributes[DocumentHeaderBlockKeys.coverDetails] = cover.$2; + attributes[DocumentHeaderBlockKeys.align] = cover.$3; } if (icon != null) { attributes[DocumentHeaderBlockKeys.icon] = icon.emoji; @@ -378,8 +382,9 @@ class DocumentHeaderToolbar extends StatefulWidget { final EditorState editorState; final bool hasCover; final bool hasIcon; - final void Function({(CoverType, String?)? cover, EmojiIconData? icon}) - onIconOrCoverChanged; + final void Function( + {(CoverType, String?, String?)? cover, + EmojiIconData? icon}) onIconOrCoverChanged; final double offset; final String? documentId; final ValueNotifier isCoverTitleHovered; @@ -443,8 +448,8 @@ class _DocumentHeaderToolbarState extends State { leftIconSize: const Size.square(18), onTap: () => widget.onIconOrCoverChanged( cover: UniversalPlatform.isDesktopOrWeb - ? (CoverType.asset, '1') - : (CoverType.color, '0xffe8e0ff'), + ? (CoverType.asset, '1', null) + : (CoverType.color, '0xffe8e0ff', null), ), useIntrinsicWidth: true, leftIcon: const FlowySvg(FlowySvgs.add_cover_s), @@ -544,7 +549,8 @@ class DocumentCover extends StatefulWidget { final EditorState editorState; final CoverType coverType; final String? coverDetails; - final void Function(CoverType type, String? details) onChangeCover; + final void Function(CoverType type, String? details, String? align) + onChangeCover; @override State createState() => DocumentCoverState(); @@ -555,6 +561,8 @@ class DocumentCoverState extends State { bool isOverlayButtonsHidden = true; bool isPopoverOpen = false; + bool isAlignOpen = false; + DesktopCoverAlignController? coverAlignController; @override Widget build(BuildContext context) { @@ -581,8 +589,13 @@ class DocumentCoverState extends State { node: widget.node, coverType: widget.coverType, coverDetails: widget.coverDetails, + enableAlign: isAlignOpen, + onAlignControllerCreated: (alignController) { + coverAlignController = alignController; + }, ), ), + if (isAlignOpen) _buildConverAlignOverlayButtons(context), if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context), ], ), @@ -642,6 +655,7 @@ class DocumentCoverState extends State { widget.onChangeCover( CoverType.file, files.first.path, + null, ); }, onSelectedAIImage: (_) { @@ -649,11 +663,13 @@ class DocumentCoverState extends State { }, onSelectedNetworkImage: (url) async { context.pop(); - widget.onChangeCover(CoverType.file, url); + widget.onChangeCover( + CoverType.file, url, null); }, onSelectedColor: (color) { context.pop(); - widget.onChangeCover(CoverType.color, color); + widget.onChangeCover( + CoverType.color, color, null); }, ), ), @@ -673,7 +689,8 @@ class DocumentCoverState extends State { SizedBox.square( dimension: 32.0, child: DeleteCoverButton( - onTap: () => widget.onChangeCover(CoverType.none, null), + onTap: () => + widget.onChangeCover(CoverType.none, null, null), ), ), ], @@ -704,7 +721,7 @@ class DocumentCoverState extends State { final imageFile = File(detail); if (!imageFile.existsSync()) { WidgetsBinding.instance.addPostFrameCallback((_) { - widget.onChangeCover(CoverType.none, null); + widget.onChangeCover(CoverType.none, null, null); }); return const SizedBox.shrink(); } @@ -795,6 +812,29 @@ class DocumentCoverState extends State { DeleteCoverButton( onTap: () => onCoverChanged(CoverType.none, null), ), + const HSpace(10), + AlignCoverButton( + onTap: switchAlignMode, + ), + ], + ), + ); + } + + Widget _buildConverAlignOverlayButtons(BuildContext context) { + return Positioned( + bottom: 20, + right: 50, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AlignCoverCancelButton( + onTap: cancelCoverAlign, + ), + const HSpace(10), + AlignCoverSaveButton( + onTap: saveCoverAlign, + ), ], ), ); @@ -818,7 +858,7 @@ class DocumentCoverState extends State { (details, _) = await saveImageToCloudStorage(details!, widget.view.id); } } - widget.onChangeCover(type, details); + widget.onChangeCover(type, details, null); // After cover change,delete from localstorage if previous cover was image type if (isFileType(previousType, previousDetails) && _isLocalMode()) { @@ -826,13 +866,41 @@ class DocumentCoverState extends State { } } - void setOverlayButtonsHidden(bool value) { - if (isOverlayButtonsHidden == value) return; + void setOverlayButtonsHidden(bool isHidden) { + if (isHidden && isAlignOpen) { + cancelCoverAlign(); + setState(() { + isAlignOpen = false; + }); + } + if (isOverlayButtonsHidden == isHidden) return; setState(() { - isOverlayButtonsHidden = value; + isOverlayButtonsHidden = isHidden; }); } + void switchAlignMode() { + setState(() { + isAlignOpen = !isAlignOpen; + isOverlayButtonsHidden = isAlignOpen; + }); + } + + void cancelCoverAlign() { + if (coverAlignController != null) { + coverAlignController!.cancel(); + } + saveCoverAlign(); + } + + void saveCoverAlign() { + if (coverAlignController != null && coverAlignController!.isModified) { + final alignAttr = coverAlignController!.getAlignAttribute(); + widget.onChangeCover(widget.coverType, widget.coverDetails, alignAttr); + } + switchAlignMode(); + } + bool _isLocalMode() { return context.read().isLocalMode; } @@ -940,3 +1008,84 @@ class _DocumentIconState extends State { return child; } } + +@visibleForTesting +class AlignCoverButton extends StatelessWidget { + const AlignCoverButton({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final fillColor = UniversalPlatform.isDesktopOrWeb + ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) + : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); + final svgColor = UniversalPlatform.isDesktopOrWeb + ? Theme.of(context).colorScheme.tertiary + : Theme.of(context).colorScheme.onPrimary; + return FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.surface, + fillColor: fillColor, + iconPadding: const EdgeInsets.all(5), + width: 28, + icon: FlowySvg( + FlowySvgs.table_align_center_s, + color: svgColor, + ), + onPressed: onTap, + ); + } +} + +@visibleForTesting +class AlignCoverSaveButton extends StatelessWidget { + const AlignCoverSaveButton({ + required this.onTap, + super.key, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final backgroundColor = UniversalPlatform.isDesktopOrWeb + ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) + : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); + + return FlowyButton( + onTap: onTap, + useIntrinsicWidth: true, + hoverColor: Theme.of(context).colorScheme.surface, + backgroundColor: backgroundColor, + text: FlowyText(LocaleKeys.button_save.tr()), + ); + } +} + +@visibleForTesting +class AlignCoverCancelButton extends StatelessWidget { + const AlignCoverCancelButton({ + required this.onTap, + super.key, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final backgroundColor = UniversalPlatform.isDesktopOrWeb + ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) + : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); + + return FlowyButton( + onTap: onTap, + useIntrinsicWidth: true, + hoverColor: Theme.of(context).colorScheme.surface, + backgroundColor: backgroundColor, + text: FlowyText(LocaleKeys.button_cancel.tr()), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart index 090db27ddc..e4af266420 100644 --- a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart +++ b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart @@ -24,6 +24,7 @@ class FlowyNetworkImage extends StatefulWidget { this.fit = BoxFit.cover, this.progressIndicatorBuilder, this.errorWidgetBuilder, + this.imageBuilder, required this.url, this.maxRetries = 5, this.retryDuration = const Duration(seconds: 6), @@ -63,6 +64,9 @@ class FlowyNetworkImage extends StatefulWidget { /// Retry error codes. final Set retryErrorCodes; + /// Optional builder to further customize the display of the image. + final ImageWidgetBuilder? imageBuilder; + final void Function(bool isImageInCache)? onImageLoaded; @override @@ -139,7 +143,9 @@ class FlowyNetworkImageState extends State { fit: widget.fit, width: widget.width, height: widget.height, + alignment: Alignment(0, 0), progressIndicatorBuilder: widget.progressIndicatorBuilder, + imageBuilder: widget.imageBuilder, errorWidget: _errorWidgetBuilder, errorListener: (value) async { Log.error( From bed09e0432f834568f5a1c9fa9d187d8de258d26 Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Wed, 2 Jul 2025 17:08:38 +0800 Subject: [PATCH 02/12] feat: cover image reposition change cover auto reset to Aliginment.center --- .../editor_plugins/header/desktop_cover.dart | 10 +++++++++- .../header/desktop_cover_align.dart | 20 ++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart index 7429b9bfd0..d1e442f788 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart @@ -61,6 +61,14 @@ class _DesktopCoverState extends State { } } + @override + void didUpdateWidget(covariant DesktopCover oldWidget) { + if (widget.coverDetails != oldWidget.coverDetails) { + coverAlignController.reset(); + } + super.didUpdateWidget(oldWidget); + } + @override Widget build(BuildContext context) { if (widget.view.extra.isEmpty) { @@ -180,7 +188,7 @@ class _DesktopCoverState extends State { return DesktopCoverAlign( controller: coverAlignController, imageProvider: provider, - enableAlign: widget.enableAlign , + enableAlign: widget.enableAlign, ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart index 6d502a9e63..dbb8ed634d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart @@ -31,6 +31,7 @@ class DesktopCoverAlignController extends ChangeNotifier { void reset() { _adjustedAlign = Alignment.center; + notifyListeners(); } void cancel() { @@ -85,17 +86,14 @@ class _DesktopCoverAlignState extends State { final alignment = controller.alignment; x = alignment.x; y = alignment.y; - controller.addListener(() { - setState(() { - x = controller.alignment.x; - y = controller.alignment.y; - }); - }); + controller.addListener(updateAlign); } @override void dispose() { + controller.removeListener(updateAlign); super.dispose(); + _stopImageStream(); } @@ -107,10 +105,18 @@ class _DesktopCoverAlignState extends State { @override void didUpdateWidget(DesktopCoverAlign oldWidget) { - super.didUpdateWidget(oldWidget); if (widget.imageProvider != oldWidget.imageProvider) { + controller.reset(); _resolveImage(); } + super.didUpdateWidget(oldWidget); + } + + void updateAlign() { + setState(() { + x = controller.alignment.x; + y = controller.alignment.y; + }); } void _resolveImage() { From cda7c13875f34bf9b53ded7678c7730d02fd5776 Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Wed, 2 Jul 2025 17:17:02 +0800 Subject: [PATCH 03/12] feat: cover image reposition add coverAlignController dispose --- .../editor_plugins/header/document_cover_widget.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 87ba0aef0e..21ca8fb4dd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -564,6 +564,12 @@ class DocumentCoverState extends State { bool isAlignOpen = false; DesktopCoverAlignController? coverAlignController; + @override + void dispose() { + coverAlignController?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return UniversalPlatform.isDesktopOrWeb From 6e1f9b65a059c582d66da203d0940f28ed120bbd Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Wed, 2 Jul 2025 17:22:23 +0800 Subject: [PATCH 04/12] feat: cover image reposition rename to alignEnable --- .../editor_plugins/header/desktop_cover.dart | 12 ++++++------ .../editor_plugins/header/desktop_cover_align.dart | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart index d1e442f788..a1e57bd5e1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart @@ -104,7 +104,7 @@ class _DesktopCoverState extends State { return DesktopCoverAlign( controller: coverAlignController, imageProvider: provider, - enableAlign: widget.enableAlign, + alignEnable: widget.enableAlign, ); }, ), @@ -120,7 +120,7 @@ class _DesktopCoverState extends State { imageProvider: AssetImage( PageStyleCoverImageType.builtInImagePath(cover.value), ), - enableAlign: widget.enableAlign, + alignEnable: widget.enableAlign, ), ); } @@ -156,7 +156,7 @@ class _DesktopCoverState extends State { imageProvider: FileImage( File(cover.value), ), - enableAlign: widget.enableAlign, + alignEnable: widget.enableAlign, ), ); } @@ -188,7 +188,7 @@ class _DesktopCoverState extends State { return DesktopCoverAlign( controller: coverAlignController, imageProvider: provider, - enableAlign: widget.enableAlign, + alignEnable: widget.enableAlign, ); }, ); @@ -201,7 +201,7 @@ class _DesktopCoverState extends State { return DesktopCoverAlign( controller: coverAlignController, imageProvider: provider, - enableAlign: widget.enableAlign, + alignEnable: widget.enableAlign, ); case CoverType.asset: @@ -210,7 +210,7 @@ class _DesktopCoverState extends State { return DesktopCoverAlign( controller: coverAlignController, imageProvider: provider, - enableAlign: widget.enableAlign, + alignEnable: widget.enableAlign, ); case CoverType.color: final color = widget.coverDetails?.tryToColor() ?? Colors.white; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart index dbb8ed634d..588be6c350 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart @@ -56,12 +56,12 @@ class DesktopCoverAlign extends StatefulWidget { required this.controller, required this.imageProvider, this.fit = BoxFit.cover, - this.enableAlign = false, + this.alignEnable = false, }); final DesktopCoverAlignController controller; final ImageProvider imageProvider; final BoxFit fit; - final bool enableAlign; + final bool alignEnable; @override State createState() => _DesktopCoverAlignState(); @@ -205,7 +205,7 @@ class _DesktopCoverAlignState extends State { fit: widget.fit, alignment: Alignment(-x, -y), ); - if (widget.enableAlign && _imageSize != null) { + if (widget.alignEnable && _imageSize != null) { child = GestureDetector( onHorizontalDragUpdate: (details) { final delta = details.delta; From a445eb15e553a39575cbb5eeb03692d8cec2d74d Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Wed, 2 Jul 2025 18:18:16 +0800 Subject: [PATCH 05/12] feat: conver image reposition add support method --- .../page_style/document_page_style_bloc.dart | 1 + .../header/document_cover_widget.dart | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart index 650fbf1d85..da9f3cc05d 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart @@ -434,6 +434,7 @@ class PageStyleCover { bool get isPresets => isPureColor || isGradient || isBuiltInImage; bool get isPhoto => isCustomImage || isLocalImage; + bool get isAlignEnable => isPhoto || isUnsplashImage || isBuiltInImage; bool get isNone => type == PageStyleCoverImageType.none; bool get isPureColor => type == PageStyleCoverImageType.pureColor; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 21ca8fb4dd..263c4fafa7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -65,6 +65,8 @@ enum CoverType { orElse: () => CoverType.none, ); } + + bool get isPhoto => this == file || this == asset; } // This key is used to intercept the selection event in the document cover widget. @@ -564,6 +566,14 @@ class DocumentCoverState extends State { bool isAlignOpen = false; DesktopCoverAlignController? coverAlignController; + bool get isCoverAlignSupport { + if (widget.view.extra.isEmpty) { + // version <= 0.5.5 + return widget.coverType.isPhoto; + } + return widget.view.cover?.isAlignEnable ?? false ; + } + @override void dispose() { coverAlignController?.dispose(); @@ -818,10 +828,16 @@ class DocumentCoverState extends State { DeleteCoverButton( onTap: () => onCoverChanged(CoverType.none, null), ), - const HSpace(10), - AlignCoverButton( - onTap: switchAlignMode, - ), + if (isCoverAlignSupport) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(10), + AlignCoverButton( + onTap: switchAlignMode, + ), + ], + ), ], ), ); From 4bd86386f7832c6564b6a20076d5b24dfe090bc7 Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Wed, 2 Jul 2025 18:33:36 +0800 Subject: [PATCH 06/12] fix: ViewListener in the DocumentCoverWidget use the shaowed value --- .../editor_plugins/header/document_cover_widget.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 263c4fafa7..ec01207a9d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -138,11 +138,11 @@ class _DocumentCoverWidgetState extends State { viewListener = ViewListener(viewId: widget.view.id) ..start( - onViewUpdated: (view) { + onViewUpdated: (value) { setState(() { - viewIcon = EmojiIconData.fromViewIconPB(view.icon); - cover = view.cover; - view = view; + viewIcon = EmojiIconData.fromViewIconPB(value.icon); + cover = value.cover; + view = value; }); }, ); @@ -571,7 +571,7 @@ class DocumentCoverState extends State { // version <= 0.5.5 return widget.coverType.isPhoto; } - return widget.view.cover?.isAlignEnable ?? false ; + return widget.view.cover?.isAlignEnable ?? false; } @override From 15b408919c32879db1d97527aa96746c7a3429db Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Thu, 3 Jul 2025 11:08:07 +0800 Subject: [PATCH 07/12] feat: cover image reposition rename the align to coverOffset and add comment on it --- .../presentation/editor_plugins/header/desktop_cover.dart | 2 +- .../editor_plugins/header/document_cover_widget.dart | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart index a1e57bd5e1..118ef7040e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart @@ -48,7 +48,7 @@ class _DesktopCoverState extends State { String? get coverDetails => widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; String? get coverAlign => - widget.node.attributes[DocumentHeaderBlockKeys.align]; + widget.node.attributes[DocumentHeaderBlockKeys.coverOffset]; late final DesktopCoverAlignController coverAlignController; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index ec01207a9d..116950cc9f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -46,7 +46,11 @@ class DocumentHeaderBlockKeys { static const String coverType = 'cover_selection_type'; static const String coverDetails = 'cover_selection'; static const String icon = 'selected_icon'; - static const String align = 'cover_selection_align'; + // CoverOffset​​ indicates the offset of the cover image within the container, + // expressed as a comma-separated pair. The default value is "0.0,0.0", which + // is equivalent to Alignment.center. Values must be comma-separated, precise + // to ​​one decimal place​​, and within the range of ​​-1 to 1​​. + static const String coverOffset = 'cover_selection_offset'; } // for the version under 0.5.5, including 0.5.5 @@ -327,7 +331,7 @@ class _DocumentCoverWidgetState extends State { if (cover != null) { attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString(); attributes[DocumentHeaderBlockKeys.coverDetails] = cover.$2; - attributes[DocumentHeaderBlockKeys.align] = cover.$3; + attributes[DocumentHeaderBlockKeys.coverOffset] = cover.$3; } if (icon != null) { attributes[DocumentHeaderBlockKeys.icon] = icon.emoji; From e72f53bb2abef94610e999e81fb680381a12891d Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Thu, 3 Jul 2025 12:51:33 +0800 Subject: [PATCH 08/12] feat: cover image reposition remove unused ImageInfo? _imageInfo in _DesktopCoverAlignState --- .../presentation/editor_plugins/header/desktop_cover_align.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart index 588be6c350..2ce5df8f17 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart @@ -70,7 +70,6 @@ class DesktopCoverAlign extends StatefulWidget { class _DesktopCoverAlignState extends State { ImageStreamListener? _imageStreamListener; ImageStream? _imageStream; - ImageInfo? _imageInfo; Size? _imageSize; Size? _frameSize; @@ -133,7 +132,6 @@ class _DesktopCoverAlignState extends State { info.image.width.toDouble(), info.image.height.toDouble(), ); - _imageInfo = _imageInfo; } synchronousCall ? setupCB() : setState(setupCB); From 3094dee4a13a5cb0df9b760e600ac7ea817a04ec Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Thu, 3 Jul 2025 14:10:34 +0800 Subject: [PATCH 09/12] feat: cover image reposition resolve potential null dereference of _imageStreamListener. --- .../editor_plugins/header/desktop_cover_align.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart index 2ce5df8f17..f58823b0ae 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover_align.dart @@ -148,13 +148,17 @@ class _DesktopCoverAlignState extends State { if (_imageStream?.key == newStream.key) { return; } - _imageStream?.removeListener(_imageStreamListener!); + if (_imageStreamListener != null) { + _imageStream?.removeListener(_imageStreamListener!); + } _imageStream = newStream; _imageStream!.addListener(_getOrCreateListener()); } void _stopImageStream() { - _imageStream?.removeListener(_imageStreamListener!); + if (_imageStreamListener != null) { + _imageStream?.removeListener(_imageStreamListener!); + } } void _changeAlignOffset(Offset offset) { From 23c0b15a9ed65556cdea932ff7e9f2e0c8942f61 Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Thu, 3 Jul 2025 14:40:51 +0800 Subject: [PATCH 10/12] chore: code style in document_cover_widget.dart --- .../header/document_cover_widget.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 116950cc9f..c1462a3d52 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; @@ -388,9 +387,10 @@ class DocumentHeaderToolbar extends StatefulWidget { final EditorState editorState; final bool hasCover; final bool hasIcon; - final void Function( - {(CoverType, String?, String?)? cover, - EmojiIconData? icon}) onIconOrCoverChanged; + final void Function({ + (CoverType, String?, String?)? cover, + EmojiIconData? icon, + }) onIconOrCoverChanged; final double offset; final String? documentId; final ValueNotifier isCoverTitleHovered; @@ -684,12 +684,18 @@ class DocumentCoverState extends State { onSelectedNetworkImage: (url) async { context.pop(); widget.onChangeCover( - CoverType.file, url, null); + CoverType.file, + url, + null, + ); }, onSelectedColor: (color) { context.pop(); widget.onChangeCover( - CoverType.color, color, null); + CoverType.color, + color, + null, + ); }, ), ), From b6625b8a630fbcca925769696cb5716051ca40da Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Thu, 3 Jul 2025 15:44:52 +0800 Subject: [PATCH 11/12] chore: remove useless FlowyNetworkImage aligment --- frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart index e4af266420..7645ebf097 100644 --- a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart +++ b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart @@ -143,7 +143,6 @@ class FlowyNetworkImageState extends State { fit: widget.fit, width: widget.width, height: widget.height, - alignment: Alignment(0, 0), progressIndicatorBuilder: widget.progressIndicatorBuilder, imageBuilder: widget.imageBuilder, errorWidget: _errorWidgetBuilder, From 20586e70f2e809f994a005ab95a375974fa3ab07 Mon Sep 17 00:00:00 2001 From: PirateBrook <779389629@qq.com> Date: Wed, 16 Jul 2025 10:26:05 +0800 Subject: [PATCH 12/12] feat: add overlay button overlay button can be used on image --- .../header/document_cover_widget.dart | 74 +++--------- .../example/lib/src/buttons/buttons_page.dart | 53 +++++++++ .../lib/src/component/button/button.dart | 4 + .../button/overlay_button/overlay_button.dart | 98 ++++++++++++++++ .../overlay_button/overlay_button_const.dart | 3 + .../overlay_button/overlay_icon_button.dart | 108 ++++++++++++++++++ .../overlay_button/overlay_text_button.dart | 106 +++++++++++++++++ 7 files changed, 388 insertions(+), 58 deletions(-) create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_button_const.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_icon_button.dart create mode 100644 frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_text_button.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index c1462a3d52..f69e9440b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -22,6 +22,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; @@ -787,18 +788,9 @@ class DocumentCoverState extends State { ), margin: EdgeInsets.zero, onClose: () => isPopoverOpen = false, - child: IntrinsicWidth( - child: RoundedTextButton( - height: 28.0, - onPressed: () => popoverController.show(), - hoverColor: Theme.of(context).colorScheme.surface, - textColor: Theme.of(context).colorScheme.tertiary, - fillColor: Theme.of(context) - .colorScheme - .surface - .withValues(alpha: 0.5), - title: LocaleKeys.document_plugins_cover_changeCover.tr(), - ), + child: AFOverlayTextButton.primary( + onTap: () => popoverController.show(), + text: LocaleKeys.document_plugins_cover_changeCover.tr(), ), popupBuilder: (BuildContext popoverContext) { isPopoverOpen = true; @@ -946,22 +938,12 @@ class DeleteCoverButton extends StatelessWidget { @override Widget build(BuildContext context) { - final fillColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) - : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); - final svgColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.tertiary - : Theme.of(context).colorScheme.onPrimary; - return FlowyIconButton( - hoverColor: Theme.of(context).colorScheme.surface, - fillColor: fillColor, - iconPadding: const EdgeInsets.all(5), - width: 28, - icon: FlowySvg( + return AFOverlayIconButton.primary( + iconBuilder: (context, isHovering, __) => FlowySvg( FlowySvgs.delete_s, - color: svgColor, + color: Theme.of(context).colorScheme.tertiary, ), - onPressed: onTap, + onTap: onTap, ); } } @@ -1052,22 +1034,12 @@ class AlignCoverButton extends StatelessWidget { @override Widget build(BuildContext context) { - final fillColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) - : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); - final svgColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.tertiary - : Theme.of(context).colorScheme.onPrimary; - return FlowyIconButton( - hoverColor: Theme.of(context).colorScheme.surface, - fillColor: fillColor, - iconPadding: const EdgeInsets.all(5), - width: 28, - icon: FlowySvg( + return AFOverlayIconButton.primary( + iconBuilder: (context, isHovering, __) => FlowySvg( FlowySvgs.table_align_center_s, - color: svgColor, + color: Theme.of(context).colorScheme.tertiary, ), - onPressed: onTap, + onTap: onTap, ); } } @@ -1083,16 +1055,9 @@ class AlignCoverSaveButton extends StatelessWidget { @override Widget build(BuildContext context) { - final backgroundColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) - : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); - - return FlowyButton( + return AFOverlayTextButton.primary( onTap: onTap, - useIntrinsicWidth: true, - hoverColor: Theme.of(context).colorScheme.surface, - backgroundColor: backgroundColor, - text: FlowyText(LocaleKeys.button_save.tr()), + text: LocaleKeys.button_save.tr(), ); } } @@ -1108,16 +1073,9 @@ class AlignCoverCancelButton extends StatelessWidget { @override Widget build(BuildContext context) { - final backgroundColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) - : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); - - return FlowyButton( + return AFOverlayTextButton.primary( onTap: onTap, - useIntrinsicWidth: true, - hoverColor: Theme.of(context).colorScheme.surface, - backgroundColor: backgroundColor, - text: FlowyText(LocaleKeys.button_cancel.tr()), + text: LocaleKeys.button_cancel.tr(), ); } } diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart index 0d0c018222..29a7f48f57 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart @@ -200,6 +200,59 @@ class ButtonsPage extends StatelessWidget { ], ), const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + image: DecorationImage( + image: NetworkImage( + "https://images.unsplash.com/photo-1493612276216-ee3925520721?q=80&w=1528&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), + fit: BoxFit.cover, + ), + ), + padding: EdgeInsets.all(8), + child: _buildSection( + 'Overlay Buttons', + [ + AFOverlayButton.normal( + onTap: () {}, + builder: (context, isHovering, disabled) { + return Text( + "Overlay Button", + style: TextStyle( + color: Theme.of(context).primaryColorDark, + ), + ); + }, + ), + const SizedBox(width: 16), + AFOverlayTextButton.primary( + text: 'Overlay Primary Text Button', + onTap: () {}, + ), + const SizedBox(width: 16), + AFOverlayIconButton.primary( + onTap: () {}, + iconBuilder: (context, isHovering, disabled) { + return Icon( + Icons.delete, + size: 20, + color: AppFlowyTheme.of(context).textColorScheme.primary, + ); + }, + ), + const SizedBox(width: 16), + AFOverlayIconButton.disabled( + iconBuilder: (context, isHovering, disabled) { + return Icon( + Icons.block, + size: 20, + color: AppFlowyTheme.of(context).textColorScheme.tertiary, + ); + }, + ), + ], + ), + ), + const SizedBox(height: 32), _buildSection( 'Button with alignment', [ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart index 31a3a20b5f..3a7989d9f3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart @@ -14,3 +14,7 @@ export 'ghost_button/ghost_text_button.dart'; export 'outlined_button/outlined_button.dart'; export 'outlined_button/outlined_icon_text_button.dart'; export 'outlined_button/outlined_text_button.dart'; +// Overlay buttons +export 'overlay_button/overlay_button.dart'; +export 'overlay_button/overlay_text_button.dart'; +export 'overlay_button/overlay_icon_button.dart'; \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_button.dart new file mode 100644 index 0000000000..c9cf2c48af --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_button.dart @@ -0,0 +1,98 @@ +import 'package:appflowy_ui/src/component/button/overlay_button/overlay_button_const.dart'; +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:flutter/material.dart'; + +typedef AFOverlayButtonWidgetBuilder = Widget Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFOverlayButton extends StatelessWidget { + const AFOverlayButton._({ + super.key, + required this.onTap, + required this.backgroundColor, + required this.builder, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + this.disabled = false, + }); + + /// Normal overlay button. + factory AFOverlayButton.normal({ + Key? key, + required VoidCallback onTap, + required AFOverlayButtonWidgetBuilder builder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + }) { + return AFOverlayButton._( + key: key, + builder: builder, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + backgroundColor: (context, isHovering, disabled) { + final theme = Theme.of(context); + if (disabled) { + return theme.colorScheme.surface.withAlpha(overlayButtonDisableAlpha); + } + if (isHovering) { + return theme.colorScheme.surface.withAlpha(overlayButtonHoverAlpha); + } + return theme.colorScheme.surface.withAlpha(overlayButtonAlpha); + }, + ); + } + + /// Disabled overlay button. + factory AFOverlayButton.disabled({ + Key? key, + required AFOverlayButtonWidgetBuilder builder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFOverlayButton._( + key: key, + builder: builder, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + backgroundColor: (context, isHovering, disabled) => Theme.of(context) + .colorScheme + .surface + .withAlpha(overlayButtonDisableAlpha), + ); + } + + final VoidCallback onTap; + final bool disabled; + final AFButtonSize size; + final EdgeInsetsGeometry? padding; + final double? borderRadius; + + final AFBaseButtonColorBuilder? backgroundColor; + final AFGhostButtonWidgetBuilder builder; + + @override + Widget build(BuildContext context) { + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: (_, __, ___, ____) => Colors.transparent, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: builder, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_button_const.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_button_const.dart new file mode 100644 index 0000000000..ac5a3c64e6 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_button_const.dart @@ -0,0 +1,3 @@ +const int overlayButtonAlpha = 0xCC; +const int overlayButtonDisableAlpha = 0x66; +const int overlayButtonHoverAlpha = 0xE5; \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_icon_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_icon_button.dart new file mode 100644 index 0000000000..3d8726528e --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_icon_button.dart @@ -0,0 +1,108 @@ +import 'package:appflowy_ui/src/component/button/overlay_button/overlay_button_const.dart'; +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +typedef AFOverlayIconBuilder = Widget Function( + BuildContext context, + bool isHovering, + bool disabled, +); + +class AFOverlayIconButton extends StatelessWidget { + const AFOverlayIconButton({ + super.key, + required this.onTap, + required this.iconBuilder, + this.backgroundColor, + this.size = AFButtonSize.m, + this.padding, + this.borderRadius, + this.disabled = false, + }); + + /// Primary overlay text button. + factory AFOverlayIconButton.primary({ + Key? key, + required VoidCallback onTap, + required AFOverlayIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + }) { + return AFOverlayIconButton( + key: key, + onTap: onTap, + iconBuilder: iconBuilder, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + backgroundColor: (context, isHovering, disabled) { + final theme = Theme.of(context); + if (disabled) { + return theme.colorScheme.surface.withAlpha(overlayButtonDisableAlpha); + } + if (isHovering) { + return theme.colorScheme.surface.withAlpha(overlayButtonHoverAlpha); + } + return theme.colorScheme.surface.withAlpha(overlayButtonAlpha); + }, + ); + } + + /// Disabled overlay text button. + factory AFOverlayIconButton.disabled({ + Key? key, + required AFOverlayIconBuilder iconBuilder, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + }) { + return AFOverlayIconButton( + key: key, + iconBuilder: iconBuilder, + onTap: () {}, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: true, + backgroundColor: (context, isHovering, disabled) { + return Theme.of(context).colorScheme.surface.withAlpha(overlayButtonDisableAlpha); + }, + ); + } + + final bool disabled; + final VoidCallback onTap; + final AFButtonSize size; + final EdgeInsetsGeometry? padding; + final double? borderRadius; + + final AFOverlayIconBuilder iconBuilder; + + final AFBaseButtonColorBuilder? backgroundColor; + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + return AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: (context, isHovering, disabled, isFocused) { + return Colors.transparent; + }, + padding: padding ?? EdgeInsets.all(theme.spacing.m), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: (context, isHovering, disabled) { + return iconBuilder( + context, + isHovering, + disabled, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_text_button.dart new file mode 100644 index 0000000000..a3f761ef91 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/overlay_button/overlay_text_button.dart @@ -0,0 +1,106 @@ +import 'package:appflowy_ui/src/component/button/overlay_button/overlay_button_const.dart'; +import 'package:appflowy_ui/src/component/component.dart'; +import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; +import 'package:flutter/material.dart'; + +class AFOverlayTextButton extends AFBaseTextButton { + const AFOverlayTextButton({ + super.key, + required super.text, + required super.onTap, + super.textColor, + super.backgroundColor, + super.size = AFButtonSize.m, + super.padding, + super.borderRadius, + super.disabled = false, + super.alignment, + super.textStyle, + }); + + /// Normal overlay text button. + factory AFOverlayTextButton.primary({ + Key? key, + required String text, + required VoidCallback onTap, + AFButtonSize size = AFButtonSize.m, + EdgeInsetsGeometry? padding, + double? borderRadius, + bool disabled = false, + Alignment? alignment, + TextStyle? textStyle, + }) { + return AFOverlayTextButton( + key: key, + text: text, + onTap: onTap, + size: size, + padding: padding, + borderRadius: borderRadius, + disabled: disabled, + alignment: alignment, + textStyle: textStyle, + backgroundColor: (context, isHovering, disabled) { + final theme = Theme.of(context); + if (disabled) { + return theme.colorScheme.surface.withAlpha(overlayButtonDisableAlpha); + } + if (isHovering) { + return theme.colorScheme.surface.withAlpha(overlayButtonHoverAlpha); + } + return theme.colorScheme.surface.withAlpha(overlayButtonAlpha); + }, + textColor: (context, isHovering, disabled) { + final theme = AppFlowyTheme.of(context); + if (disabled) { + return theme.textColorScheme.tertiary; + } + if (isHovering) { + return theme.textColorScheme.primary; + } + return theme.textColorScheme.primary; + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return ConstrainedBox( + constraints: BoxConstraints( + minWidth: 76, + ), + child: AFBaseButton( + disabled: disabled, + backgroundColor: backgroundColor, + borderColor: (_, __, ___, ____) => Colors.transparent, + padding: padding ?? size.buildPadding(context), + borderRadius: borderRadius ?? size.buildBorderRadius(context), + onTap: onTap, + builder: (context, isHovering, disabled) { + final textColor = + this.textColor?.call(context, isHovering, disabled) ?? + theme.textColorScheme.primary; + + Widget child = Text( + text, + style: textStyle ?? + size.buildTextStyle(context).copyWith(color: textColor), + textAlign: TextAlign.center, + ); + + final alignment = this.alignment; + if (alignment != null) { + child = Align( + alignment: alignment, + child: child, + ); + } + + return child; + }, + ), + ); + } +}