From fe4170574cc597fe3efc9b19b60d4c4f718c2f55 Mon Sep 17 00:00:00 2001 From: Dev Ttangkong Date: Mon, 6 Jul 2026 08:10:18 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B8=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/config/app_typeface_editor.dart | 30 +-- lib/app/enum/post_type.enum.dart | 7 + .../album_organize/album_organize_page.dart | 2 - .../album_organize/album_organize_view.dart | 2 - .../view/album_organize_fab_view.dart | 100 --------- .../layout/grimity_uploading_layout.dart | 17 +- .../grimity_select_modal_bottom_sheet.dart | 173 --------------- .../widget/popup/grimity_select_popup.dart | 62 ++++++ .../feed_upload/feed_upload_view.dart | 7 +- .../post_detail/view/post_content_view.dart | 3 +- .../post_upload/post_upload_page.dart | 5 - .../post_upload/post_upload_view.dart | 26 +-- .../provider/post_upload_provider.dart | 25 ++- .../view/post_upload_body_view.dart | 34 +-- .../widget/post_add_link_bottom_sheet.dart | 169 ++++++--------- .../widget/post_upload_app_bar.dart | 118 ++++------ .../widget/post_upload_complete_dialog.dart | 110 ---------- .../post_upload_content_text_field.dart | 42 ++-- .../post_upload_deletable_image_builder.dart | 31 +-- .../widget/post_upload_title_text_field.dart | 11 +- .../widget/post_upload_toolbar.dart | 204 ++++++++++++++++++ .../widget/toolbar/image_delete_toolbar.dart | 48 ----- .../widget/toolbar/parts/color_menu.dart | 146 ------------- .../widget/toolbar/parts/header_menu.dart | 42 ---- .../widget/toolbar/parts/import_menu.dart | 71 ------ .../toolbar/parts/menu_archor_square.dart | 49 ----- .../toolbar/parts/quill_toolbar_utils.dart | 87 -------- .../toolbar/parts/text_style_group.dart | 49 ----- .../widget/toolbar/parts/toolbar_buttons.dart | 29 --- .../toolbar/parts/toolbar_container.dart | 20 -- .../widget/toolbar/parts/undo_redo_group.dart | 32 --- .../toolbar/quill_bottom_sheet_toolbar.dart | 55 ----- 32 files changed, 508 insertions(+), 1298 deletions(-) delete mode 100644 lib/presentation/album_organize/view/album_organize_fab_view.dart delete mode 100644 lib/presentation/common/widget/popup/grimity_select_modal_bottom_sheet.dart create mode 100644 lib/presentation/common/widget/popup/grimity_select_popup.dart delete mode 100644 lib/presentation/post_upload/widget/post_upload_complete_dialog.dart create mode 100644 lib/presentation/post_upload/widget/post_upload_toolbar.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/image_delete_toolbar.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/color_menu.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/header_menu.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/import_menu.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/menu_archor_square.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/text_style_group.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/toolbar_buttons.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/toolbar_container.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/parts/undo_redo_group.dart delete mode 100644 lib/presentation/post_upload/widget/toolbar/quill_bottom_sheet_toolbar.dart diff --git a/lib/app/config/app_typeface_editor.dart b/lib/app/config/app_typeface_editor.dart index e89d1ab4..b6fd2402 100644 --- a/lib/app/config/app_typeface_editor.dart +++ b/lib/app/config/app_typeface_editor.dart @@ -1,37 +1,29 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_quill/flutter_quill.dart'; +import 'package:gds/gds.dart'; import 'package:grimity/app/config/app_color.dart'; -abstract class AppTypefaceEditor { - const AppTypefaceEditor._(); +class AppTypefaceEditor { + const AppTypefaceEditor({required this.context}); - static TextStyle get editorFontFamily => TextStyle(fontFamily: 'Pretendard'); + final BuildContext context; - static TextStyle get paragraph => - editorFontFamily.copyWith(fontSize: 16, fontWeight: FontWeight.w500, height: 24 / 16, color: AppColor.gray800); + TextStyle get paragraph => GdsTypography.label2.copyWith(color: context.gdsColors.text.grayBold); + TextStyle get h1 => GdsTypography.subtitle1.copyWith(color: context.gdsColors.text.grayBold); + TextStyle get h2 => GdsTypography.body1SB.copyWith(color: context.gdsColors.text.grayBold); - static TextStyle get h1 => - editorFontFamily.copyWith(fontSize: 32, fontWeight: FontWeight.w700, height: 38 / 32, color: AppColor.gray800); - - static TextStyle get h2 => - editorFontFamily.copyWith(fontSize: 24, fontWeight: FontWeight.w600, height: 30 / 24, color: AppColor.gray800); - - // p { padding-bottom: 6px } static const _pBottom = VerticalSpacing(0, 6); - - // h1/h2 { padding-bottom: 14px } static const _hBottom = VerticalSpacing(0, 14); - static const _lineSpacing = VerticalSpacing.zero; - static const _hSpacing = HorizontalSpacing.zero; - static DefaultStyles get quillDefaultStyles => DefaultStyles( + DefaultStyles get defaultStyles => DefaultStyles( paragraph: DefaultTextBlockStyle(paragraph, _hSpacing, _pBottom, _lineSpacing, null), h1: DefaultTextBlockStyle(h1, _hSpacing, _hBottom, _lineSpacing, null), h2: DefaultTextBlockStyle(h2, _hSpacing, _hBottom, _lineSpacing, null), + link: paragraph.copyWith(color: context.gdsColors.text.primaryNormal), placeHolder: DefaultTextBlockStyle( - paragraph.copyWith(color: AppColor.gray500), + paragraph.copyWith(color: context.gdsColors.text.graySubtle), _hSpacing, _pBottom, _lineSpacing, diff --git a/lib/app/enum/post_type.enum.dart b/lib/app/enum/post_type.enum.dart index 249e3c8a..b8d573de 100644 --- a/lib/app/enum/post_type.enum.dart +++ b/lib/app/enum/post_type.enum.dart @@ -21,6 +21,13 @@ enum PostType { final String jsonKey; const PostType(this.displayName, this.jsonKey); + /// 사용자가 업로드 가능한 [PostType] 목록 + static const List uploadableTypes = [ + PostType.normal, + PostType.question, + PostType.feedback, + ]; + static PostType fromString(String value) { return PostType.values.firstWhere((e) => e.toJson() == value, orElse: () => PostType.normal); } diff --git a/lib/presentation/album_organize/album_organize_page.dart b/lib/presentation/album_organize/album_organize_page.dart index 32f608f3..6b54632e 100644 --- a/lib/presentation/album_organize/album_organize_page.dart +++ b/lib/presentation/album_organize/album_organize_page.dart @@ -4,7 +4,6 @@ import 'package:grimity/domain/entity/user.dart'; import 'package:grimity/presentation/album_organize/album_organize_view.dart'; import 'package:grimity/presentation/album_organize/provider/album_organize_page_argument_provider.dart'; import 'package:grimity/presentation/album_organize/view/album_organize_body_view.dart'; -import 'package:grimity/presentation/album_organize/view/album_organize_fab_view.dart'; import 'package:grimity/presentation/album_organize/widget/album_organize_app_bar.dart'; import 'package:grimity/presentation/common/widget/navigation/grimity_drawer.dart'; @@ -22,7 +21,6 @@ class AlbumOrganizePage extends ConsumerWidget { albumOrganizeAppBar: AlbumOrganizeAppBar(), albumOrganizeDrawer: GrimityDrawer(), albumOrganizeBodyView: AlbumOrganizeBodyView(), - albumOrganizeFabView: AlbumOrganizeFabView(), ), ); } diff --git a/lib/presentation/album_organize/album_organize_view.dart b/lib/presentation/album_organize/album_organize_view.dart index ddced66d..31d684dc 100644 --- a/lib/presentation/album_organize/album_organize_view.dart +++ b/lib/presentation/album_organize/album_organize_view.dart @@ -8,13 +8,11 @@ class AlbumOrganizeView extends ConsumerWidget { required this.albumOrganizeAppBar, required this.albumOrganizeDrawer, required this.albumOrganizeBodyView, - required this.albumOrganizeFabView, }); final Widget albumOrganizeAppBar; final Widget albumOrganizeDrawer; final Widget albumOrganizeBodyView; - final Widget albumOrganizeFabView; @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/presentation/album_organize/view/album_organize_fab_view.dart b/lib/presentation/album_organize/view/album_organize_fab_view.dart deleted file mode 100644 index 1138cbf8..00000000 --- a/lib/presentation/album_organize/view/album_organize_fab_view.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:grimity/app/config/app_typeface.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/presentation/album_organize/provider/album_organize_provider.dart'; -import 'package:grimity/presentation/album_organize/widget/album_organize_fab_button.dart'; -import 'package:grimity/presentation/common/widget/alert/grimity_dialog.dart'; -import 'package:grimity/presentation/common/widget/popup/grimity_select_modal_bottom_sheet.dart'; -import 'package:grimity/app/config/app_color.dart'; - -class AlbumOrganizeFabView extends ConsumerWidget with AlbumOrganizeMixin { - const AlbumOrganizeFabView({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = albumOrganizeState(ref); - final userAlbums = state.userAlbums; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 6, - children: [ - AlbumOrganizeFabButton( - title: '선택 삭제', - asset: Assets.icons.icon.close, - onTap: () => _showDeleteDialog(context, ref), - ), - - // 유저가 생성한 앨범이 있을때 만 표시 - if (userAlbums.isNotEmpty) - AlbumOrganizeFabButton( - title: '앨범 이동', - asset: Assets.icons.icon.deliver, - onTap: () => _showMoveBottomSheet(context, ref, state.ids.length), - ), - ], - ); - } - - void _showDeleteDialog(BuildContext context, WidgetRef ref) { - showDialog( - context: context, - builder: (context) { - return GrimityDialog( - title: '선택한 그림을 삭제할까요?', - content: '삭제 이후 되돌릴 수 없어요', - cancelText: '취소', - onCancel: () => context.pop(), - confirmText: '삭제', - onConfirm: () { - context.pop(); - albumOrganizeNotifier(ref).deleteFeeds(); - }, - ); - }, - ); - } - - void _showMoveBottomSheet(BuildContext context, WidgetRef ref, int count) { - GrimitySelectModalBottomSheet.show( - context, - titleWidget: RichText( - text: TextSpan( - style: AppTypeface.subTitle3, - children: [ - TextSpan(text: '선택한 '), - TextSpan(text: '$count', style: AppTypeface.subTitle3.copyWith(color: AppColor.main)), - TextSpan(text: '개의 그림 이동'), - ], - ), - ), - buttonsBuilder: (ref) { - final state = albumOrganizeState(ref); - final notifier = albumOrganizeNotifier(ref); - - return [ - GrimitySelectModalButtonModel( - title: '전체 앨범', - isSelected: state.targetAlbumId == null, - isDisabled: state.currentAlbumId == null, - onTap: () => notifier.updateTargetAlbumId(null), - ), - ...state.userAlbums.map((album) { - return GrimitySelectModalButtonModel( - title: album.name, - isSelected: state.targetAlbumId == album.id, - isDisabled: state.currentAlbumId == album.id, - onTap: () => notifier.updateTargetAlbumId(album.id), - ); - }), - ]; - }, - onSave: () { - context.pop(); - albumOrganizeNotifier(ref).moveFeeds(); - }, - ); - } -} diff --git a/lib/presentation/common/layout/grimity_uploading_layout.dart b/lib/presentation/common/layout/grimity_uploading_layout.dart index 99ce2af9..8aa32e2e 100644 --- a/lib/presentation/common/layout/grimity_uploading_layout.dart +++ b/lib/presentation/common/layout/grimity_uploading_layout.dart @@ -5,9 +5,17 @@ import 'package:grimity/presentation/common/widget/grimity_modal_loading.dart'; import 'package:grimity/presentation/common/widget/grimity_pop_scope.dart'; class GrimityUploadingLayout extends StatelessWidget { - const GrimityUploadingLayout({super.key, required this.child, required this.uploading}); + const GrimityUploadingLayout({ + super.key, + required this.child, + required this.title, + required this.description, + required this.uploading, + }); final Widget child; + final String title; + final String description; final bool uploading; @override @@ -18,11 +26,12 @@ class GrimityUploadingLayout extends StatelessWidget { child: Stack( children: [ child, - if (uploading) + if (uploading) ...[ GrimityModalLoading( - title: '이미지를 업로드 중이에요', - description: '이미지 업로드 도중 화면을 닫거나\n뒤로가면 업로드가 중단될 수 있어요', + title: title, + description: description, ), + ], ], ), ); diff --git a/lib/presentation/common/widget/popup/grimity_select_modal_bottom_sheet.dart b/lib/presentation/common/widget/popup/grimity_select_modal_bottom_sheet.dart deleted file mode 100644 index cee1804c..00000000 --- a/lib/presentation/common/widget/popup/grimity_select_modal_bottom_sheet.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_typeface.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; - -/// 공통 선택 모달 바텀 시트 -class GrimitySelectModalBottomSheet extends ConsumerWidget { - final String? title; - final Widget? titleWidget; - final List? buttons; - final List Function(WidgetRef ref)? buttonsBuilder; - final VoidCallback? onSave; - - const GrimitySelectModalBottomSheet({ - super.key, - required this.title, - required this.titleWidget, - this.buttons, - this.buttonsBuilder, - this.onSave, - }) : assert(title != null || titleWidget != null, 'Either buttons or buttonsBuilder must be provided'), - assert(buttons != null || buttonsBuilder != null, 'Either buttons or buttonsBuilder must be provided'); - - static void show( - BuildContext context, { - String? title, - Widget? titleWidget, - List? buttons, - List Function(WidgetRef ref)? buttonsBuilder, - VoidCallback? onSave, - }) { - showModalBottomSheet( - context: context, - backgroundColor: Colors.white, - constraints: BoxConstraints(maxHeight: 520), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), - ), - builder: (modalContext) { - final container = ProviderScope.containerOf(context, listen: false); - return UncontrolledProviderScope( - container: container, - child: GrimitySelectModalBottomSheet( - title: title, - titleWidget: titleWidget, - buttons: buttons, - buttonsBuilder: buttonsBuilder, - onSave: onSave, - ), - ); - }, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final titleNode = titleWidget ?? Text(title!, style: AppTypeface.subTitle3); - final models = buttons ?? buttonsBuilder?.call(ref) ?? []; - return Padding( - padding: EdgeInsets.only(left: 16, right: 16, bottom: 24 + MediaQuery.of(context).padding.bottom, top: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - titleNode, - const Spacer(), - GrimityGesture(onTap: () => context.pop(), child: Assets.icons.icon.close.svg(width: 24, height: 24)), - ], - ), - Gap(24), - Flexible( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: - models - .map( - (e) => _SelectBottomSheetButton( - title: e.title, - onTap: e.onTap, - isSelected: e.isSelected, - isDisabled: e.isDisabled, - ), - ) - .toList(), - ), - ), - ), - if (onSave != null) ...[ - Gap(24), - GrimityGesture( - onTap: onSave, - child: Container( - decoration: BoxDecoration(color: AppColor.primary4, borderRadius: BorderRadius.circular(10)), - height: 42, - child: Center(child: Text('저장', style: AppTypeface.label2.copyWith(color: AppColor.gray00))), - ), - ), - ], - ], - ), - ); - } -} - -class GrimitySelectModalButtonModel { - final String title; - final VoidCallback? onTap; - final bool isSelected; - final bool isDisabled; - - GrimitySelectModalButtonModel({required this.title, this.onTap, this.isSelected = false, this.isDisabled = false}); -} - -class _SelectBottomSheetButton extends StatelessWidget { - const _SelectBottomSheetButton({ - required this.title, - required this.onTap, - this.isSelected = false, - this.isDisabled = false, - }); - - final String title; - final VoidCallback? onTap; - final bool isSelected; - final bool isDisabled; - - Color get _borderColor => - isDisabled - ? AppColor.gray300 - : isSelected - ? AppColor.main - : AppColor.gray300; - - Color get _boxColor => - isDisabled - ? AppColor.gray200 - : isSelected - ? AppColor.mainSecondary - : AppColor.gray00; - - Color get _textColor => isDisabled ? AppColor.gray500 : AppColor.gray800; - - @override - Widget build(BuildContext context) { - return GrimityGesture( - onTap: isDisabled ? null : onTap, - child: Container( - height: 52, - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: _borderColor), - color: _boxColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: AppTypeface.label2.copyWith(color: _textColor)), - if (isSelected) Assets.icons.icon.checkMark.svg(), - ], - ), - ), - ); - } -} diff --git a/lib/presentation/common/widget/popup/grimity_select_popup.dart b/lib/presentation/common/widget/popup/grimity_select_popup.dart new file mode 100644 index 00000000..d0b90108 --- /dev/null +++ b/lib/presentation/common/widget/popup/grimity_select_popup.dart @@ -0,0 +1,62 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; + +class GrimitySelectPopupItem { + const GrimitySelectPopupItem({ + required this.label, + required this.onTap, + this.isSelected = false, + this.isDisabled = false, + }); + + final String label; + final VoidCallback onTap; + final bool isSelected; + final bool isDisabled; +} + +class GrimitySelectPopup { + const GrimitySelectPopup({ + required this.title, + required this.items, + }); + + final String title; + final List items; + + Future show(BuildContext context) { + final child = Column( + mainAxisSize: MainAxisSize.min, + spacing: GdsSpacing.spacing8, + children: [ + ...items.map((item) { + GdsListItemState state = GdsListItemState.enabled; + if (item.isSelected) state = GdsListItemState.pressed; + if (item.isDisabled) state = GdsListItemState.disabled; + + return GdsListItem.optionCard( + text: item.label, + onTap: item.onTap, + state: state, + ); + }), + ], + ); + + if (context.isMobile) { + final bottomSheet = GdsBottomSheet( + title: title, + child: child, + ); + + return bottomSheet.open(context); + } else { + final modal = GdsModal( + title: title, + body: child, + ); + + return modal.open(context); + } + } +} diff --git a/lib/presentation/feed_upload/feed_upload_view.dart b/lib/presentation/feed_upload/feed_upload_view.dart index 7aeac146..d2ba0f72 100644 --- a/lib/presentation/feed_upload/feed_upload_view.dart +++ b/lib/presentation/feed_upload/feed_upload_view.dart @@ -23,6 +23,11 @@ class FeedUploadView extends ConsumerWidget { body: feedUploadBodyView, ); - return GrimityUploadingLayout(uploading: uploading, child: child); + return GrimityUploadingLayout( + title: '이미지를 업로드 중이에요', + description: '이미지 업로드 도중 화면을 닫거나\n뒤로가면 업로드가 중단될 수 있어요', + uploading: uploading, + child: child, + ); } } diff --git a/lib/presentation/post_detail/view/post_content_view.dart b/lib/presentation/post_detail/view/post_content_view.dart index f5bb8a92..69085d4b 100644 --- a/lib/presentation/post_detail/view/post_content_view.dart +++ b/lib/presentation/post_detail/view/post_content_view.dart @@ -4,7 +4,6 @@ import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:gap/gap.dart'; import 'package:gds/gds.dart'; import 'package:go_router/go_router.dart'; -import 'package:grimity/app/config/app_color.dart'; import 'package:grimity/app/config/app_config.dart'; import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/app/enum/post_type.enum.dart'; @@ -249,7 +248,7 @@ class _PostContentSection extends StatelessWidget { case 'p': return {'font-size': '16px', 'font-weight': '500', 'line-height': '24px', 'margin': '0 0 6px 0'}; case 'a': - return {'color': AppColor.link.toHexColor(), 'text-decoration': 'none'}; + return {'color': colors.text.primaryNormal.toHexColor(), 'text-decoration': 'none'}; case 'strong': case 'b': return {'font-weight': '700'}; diff --git a/lib/presentation/post_upload/post_upload_page.dart b/lib/presentation/post_upload/post_upload_page.dart index caa84459..1c665e38 100644 --- a/lib/presentation/post_upload/post_upload_page.dart +++ b/lib/presentation/post_upload/post_upload_page.dart @@ -10,14 +10,11 @@ import 'package:grimity/presentation/post_upload/provider/post_upload_page_argum import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; import 'package:grimity/presentation/post_upload/view/post_upload_body_view.dart'; import 'package:grimity/presentation/post_upload/widget/post_upload_app_bar.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/image_delete_toolbar.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/quill_bottom_sheet_toolbar.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class PostUploadPage extends HookConsumerWidget { const PostUploadPage({super.key, this.postToEdit}); - final double bottomSheetHeight = 42; final Post? postToEdit; @override @@ -46,8 +43,6 @@ class PostUploadPage extends HookConsumerWidget { child: PostUploadView( postUploadAppBar: PostUploadAppBar(), postUploadBodyView: PostUploadBodyView(), - bottomSheetHeight: bottomSheetHeight, - postUploadBottomSheet: state.imageEdit ? ImageDeleteToolbar() : QuillBottomSheetToolbar(), ), ); } diff --git a/lib/presentation/post_upload/post_upload_view.dart b/lib/presentation/post_upload/post_upload_view.dart index d858b4d1..2c15334f 100644 --- a/lib/presentation/post_upload/post_upload_view.dart +++ b/lib/presentation/post_upload/post_upload_view.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:grimity/app/config/app_color.dart'; +import 'package:gds/gds.dart'; import 'package:grimity/presentation/common/layout/grimity_uploading_layout.dart'; import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,32 +9,24 @@ class PostUploadView extends ConsumerWidget { super.key, required this.postUploadAppBar, required this.postUploadBodyView, - required this.postUploadBottomSheet, - required this.bottomSheetHeight, }); - final PreferredSizeWidget postUploadAppBar; + final Widget postUploadAppBar; final Widget postUploadBodyView; - final Widget postUploadBottomSheet; - final double bottomSheetHeight; @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(postUploadProvider); final uploading = state.uploading; - final child = Container( - color: AppColor.gray00, - child: SafeArea( - top: false, - child: Scaffold( - appBar: postUploadAppBar, - body: Padding(padding: EdgeInsets.only(bottom: bottomSheetHeight), child: postUploadBodyView), - bottomSheet: postUploadBottomSheet, - ), + return GrimityUploadingLayout( + uploading: uploading, + title: '게시글을 업로드 중이에요', + description: '게시글 업로드 도중 화면을 닫거나\n뒤로가면 업로드가 중단될 수 있어요', + child: GdsScaffold( + appBar: postUploadAppBar, + body: postUploadBodyView, ), ); - - return GrimityUploadingLayout(uploading: uploading, child: child); } } diff --git a/lib/presentation/post_upload/provider/post_upload_provider.dart b/lib/presentation/post_upload/provider/post_upload_provider.dart index c28ecf9d..6c6ff631 100644 --- a/lib/presentation/post_upload/provider/post_upload_provider.dart +++ b/lib/presentation/post_upload/provider/post_upload_provider.dart @@ -84,6 +84,26 @@ class PostUpload extends _$PostUpload { final targets = state.selectedImageUrls; if (targets.isEmpty) return; + _deleteImages(controller: controller, targets: targets); + + state = state.copyWith( + contentDeltaOps: controller.document.toDelta().toJson(), + selectedImageUrls: [], + imageEdit: false, + ); + } + + void deleteImage(QuillController controller, String imageUrl) { + _deleteImages(controller: controller, targets: [imageUrl]); + + state = state.copyWith( + contentDeltaOps: controller.document.toDelta().toJson(), + selectedImageUrls: [], + imageEdit: false, + ); + } + + void _deleteImages({required QuillController controller, required List targets}) { final delta = controller.document.toDelta(); final toDeleteOffsets = []; int offset = 0; @@ -123,11 +143,6 @@ class PostUpload extends _$PostUpload { } // 3) 상태 갱신 (에디터 화면과 provider 상태 동기화) - state = state.copyWith( - contentDeltaOps: controller.document.toDelta().toJson(), - selectedImageUrls: [], - imageEdit: false, - ); } void _setUploading(bool isUploading) { diff --git a/lib/presentation/post_upload/view/post_upload_body_view.dart b/lib/presentation/post_upload/view/post_upload_body_view.dart index c4b977e6..261df7e8 100644 --- a/lib/presentation/post_upload/view/post_upload_body_view.dart +++ b/lib/presentation/post_upload/view/post_upload_body_view.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gap/gap.dart'; +import 'package:gds/gds.dart'; import 'package:grimity/presentation/post_upload/widget/post_upload_content_text_field.dart'; import 'package:grimity/presentation/post_upload/widget/post_upload_title_text_field.dart'; +import 'package:grimity/presentation/post_upload/widget/post_upload_toolbar.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class PostUploadBodyView extends HookConsumerWidget { @@ -12,18 +13,27 @@ class PostUploadBodyView extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final scrollController = useScrollController(); - return SingleChildScrollView( - controller: scrollController, - child: Column( - children: [ - Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: PostUploadTitleTextField()), - Gap(16), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: PostUploadContentTextField(scrollController: scrollController), + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + controller: scrollController, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: GdsSpacing.spacing24, + horizontal: GdsSpacing.spacing20, + ), + child: Column( + children: [ + PostUploadTitleTextField(), + PostUploadContentTextField(scrollController: scrollController), + ], + ), + ), ), - ], - ), + ), + PostUploadToolbar(), + ], ); } } diff --git a/lib/presentation/post_upload/widget/post_add_link_bottom_sheet.dart b/lib/presentation/post_upload/widget/post_add_link_bottom_sheet.dart index eb40777b..6c289926 100644 --- a/lib/presentation/post_upload/widget/post_add_link_bottom_sheet.dart +++ b/lib/presentation/post_upload/widget/post_add_link_bottom_sheet.dart @@ -1,109 +1,82 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_typeface.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; -import 'package:grimity/presentation/common/widget/text_field/grimity_text_field.dart'; +import 'package:gds/gds.dart'; -class PostAddLinkBottomSheet extends HookWidget { - const PostAddLinkBottomSheet({super.key, this.initialText, this.initialUrl, required this.onSubmit}); +typedef PostAddLinkSubmitCallback = void Function(String text, String url); - final String? initialText; - final String? initialUrl; - final void Function(String text, String url) onSubmit; +Future showPostAddLinkPopup( + BuildContext context, { + String? initialText, + String? initialUrl, + required PostAddLinkSubmitCallback onSubmit, +}) async { + final textController = TextEditingController(text: initialText ?? ''); + final urlController = TextEditingController(text: initialUrl ?? ''); - static void show( - BuildContext context, { - String? initialText, - String? initialUrl, - required final void Function(String text, String url) onSubmit, - }) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), - ), - builder: (context) { - final bottomInset = MediaQuery.of(context).viewInsets.bottom; + void submit() { + final text = textController.text.trim(); + final url = _normalizeUrl(urlController.text); + if (url.isEmpty) return; - return AnimatedPadding( - padding: EdgeInsets.only(bottom: bottomInset), - duration: Duration(milliseconds: 200), - curve: Curves.easeOut, - child: PostAddLinkBottomSheet(initialText: initialText, initialUrl: initialUrl, onSubmit: onSubmit), - ); - }, - ); + onSubmit(text, url); + Navigator.of(context).pop(); } - @override - Widget build(BuildContext context) { - final textController = useTextEditingController(text: initialText ?? ''); - final urlController = useTextEditingController(text: initialUrl ?? ''); - - return Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Gap(24), - Row( - children: [ - Text('링크 추가', style: AppTypeface.subTitle3), - const Spacer(), - GrimityGesture(onTap: () => context.pop(), child: Assets.icons.icon.close.svg(width: 24, height: 24)), - ], - ), - Gap(24), - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Text', style: AppTypeface.caption1.copyWith(color: AppColor.gray800)), - Gap(10), - GrimityTextField.small(controller: textController, maxLines: 1, hintText: 'grimity'), - Gap(16), - Text('Link', style: AppTypeface.caption1.copyWith(color: AppColor.gray800)), - Gap(10), - GrimityTextField.small(controller: urlController, maxLines: 1, hintText: 'www.grimity.com'), - ], - ), - ), - Gap(24), - GrimityGesture( - onTap: () { - final text = textController.value.text.trim(); - final url = _normalizeUrl(urlController.value.text); - if (url.isEmpty) return; - onSubmit(text, url); - context.pop(); - }, - child: Container( - height: 42, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppColor.primary4), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6), - child: Center(child: Text('추가', style: AppTypeface.label2.copyWith(color: AppColor.gray00))), - ), - ), - ), - Gap(24), - ], + final child = Column( + mainAxisSize: MainAxisSize.min, + spacing: GdsSpacing.spacing12, + children: [ + GdsInput( + titleText: '링크 주소', + placeholder: 'https://www.grimity.com/board/write', + isRequired: false, + controller: urlController, + textInputAction: TextInputAction.next, ), - ); - } + GdsInput( + titleText: '링크 명', + placeholder: '링크 주소 대신 보일 내용을 입력해주세요', + isRequired: false, + controller: textController, + textInputAction: TextInputAction.done, + onEditingComplete: submit, + ), + ], + ); + + try { + if (context.isMobile) { + final bottomSheet = GdsBottomSheet( + title: '링크 추가', + primaryLabel: '완료', + onPrimaryTap: submit, + secondaryLabel: '닫기', + onSecondaryTap: () => Navigator.of(context).pop(), + child: child, + ); - String _normalizeUrl(String u) { - final s = u.trim(); - if (s.isEmpty) return s; - final hasScheme = RegExp(r'^[a-zA-Z][a-zA-Z0-9+\-.]*://').hasMatch(s); - return hasScheme ? s : 'https://$s'; + return await bottomSheet.open(context); + } else { + final modal = GdsModal( + title: '링크 추가', + body: child, + primaryLabel: '완료', + onPrimary: submit, + secondaryLabel: '닫기', + onSecondary: () => Navigator.of(context).pop(), + ); + + return await modal.open(context); + } + } finally { + textController.dispose(); + urlController.dispose(); } } + +String _normalizeUrl(String url) { + final trimmedUrl = url.trim(); + if (trimmedUrl.isEmpty) return trimmedUrl; + + final hasScheme = RegExp(r'^[a-zA-Z][a-zA-Z0-9+\-.]*://').hasMatch(trimmedUrl); + return hasScheme ? trimmedUrl : 'https://$trimmedUrl'; +} diff --git a/lib/presentation/post_upload/widget/post_upload_app_bar.dart b/lib/presentation/post_upload/widget/post_upload_app_bar.dart index 31c0c289..b3860fad 100644 --- a/lib/presentation/post_upload/widget/post_upload_app_bar.dart +++ b/lib/presentation/post_upload/widget/post_upload_app_bar.dart @@ -1,95 +1,61 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gds/gds.dart'; import 'package:go_router/go_router.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_theme.dart'; -import 'package:grimity/app/config/app_typeface.dart'; +import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/app/enum/post_type.enum.dart'; -import 'package:grimity/gen/assets.gen.dart'; +import 'package:grimity/app/service/toast_service.dart'; import 'package:grimity/presentation/common/dialog/cancel_upload_dialog.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; -import 'package:grimity/presentation/common/widget/popup/grimity_select_modal_bottom_sheet.dart'; +import 'package:grimity/presentation/common/widget/popup/grimity_select_popup.dart'; import 'package:grimity/presentation/post_upload/provider/post_upload_page_argument_provider.dart'; import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; -import 'package:grimity/presentation/post_upload/widget/post_upload_complete_dialog.dart'; -class PostUploadAppBar extends StatelessWidget implements PreferredSizeWidget { +class PostUploadAppBar extends ConsumerWidget { const PostUploadAppBar({super.key}); @override - Widget build(BuildContext context) { - return AppBar( - toolbarHeight: AppTheme.kToolbarHeight.height, - leading: Center( - child: GrimityGesture( - onTap: () => showCancelUploadDialog(context), - child: Assets.icons.icon.close.svg(width: 24, height: 24), - ), - ), - title: Consumer( - builder: (context, ref, child) { - final type = ref.watch(postUploadProvider).type; + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(postUploadProvider); - return GrimityGesture( + return GdsTopNavigation.editor( + title: state.type.displayName, + label: '업로드', + onSave: () => _onSave(context, ref), + onBack: () => showCancelUploadDialog(context), + onTitle: () => _showUploadTypeSelectPopup(context, ref), + saveEnabled: state.canUpload, + ); + } + + void _onSave(BuildContext context, WidgetRef ref) async { + final quillController = ref.read(postUploadQuillControllerArgumentProvider); + final postUpload = ref.read(postUploadProvider.notifier); + final uploadPost = await postUpload.postUpload(quillController); + + if (uploadPost != null && context.mounted) { + PostDetailRoute(id: uploadPost.id).pushReplacement(context); + ToastService.showSuccess('글을 업로드했어요'); + } + } + + Future _showUploadTypeSelectPopup(BuildContext context, WidgetRef ref) { + final state = ref.read(postUploadProvider); + final popup = GrimitySelectPopup( + title: '말머리 선택', + items: [ + ...PostType.uploadableTypes.map((type) { + return GrimitySelectPopupItem( + label: type.displayName, + isSelected: type == state.type, onTap: () { - GrimitySelectModalBottomSheet.show( - context, - title: '카테고리 선택', - buttons: - [PostType.normal, PostType.question, PostType.feedback] - .map( - (e) => GrimitySelectModalButtonModel( - title: e.displayName, - isSelected: e == type, - onTap: () { - context.pop(); - ref.read(postUploadProvider.notifier).updateType(e); - }, - ), - ) - .toList(), - ); + context.pop(); + ref.read(postUploadProvider.notifier).updateType(type); }, - child: Row( - children: [Text(type.displayName, style: AppTypeface.subTitle3), Icon(Icons.expand_more, size: 24)], - ), ); - }, - ), - titleSpacing: 0, - actions: [ - Consumer( - builder: (context, ref, child) { - final canUpload = ref.watch(postUploadProvider).canUpload; - - return TextButton( - onPressed: - canUpload - ? () async { - final uploadedPost = await ref - .read(postUploadProvider.notifier) - .postUpload(ref.read(postUploadQuillControllerArgumentProvider)); - if (uploadedPost != null && context.mounted) { - showUploadCompleteDialog( - context, - uploadedPost, - ref.read(postUploadProvider.notifier).isNewUpload, - ); - } - } - : null, - child: Text( - '등록', - style: AppTypeface.subTitle4.copyWith(color: canUpload ? AppColor.main : AppColor.gray500), - ), - ); - }, - ), + }), ], - actionsPadding: EdgeInsets.zero, ); - } - @override - Size get preferredSize => AppTheme.kToolbarHeight; + return popup.show(context); + } } diff --git a/lib/presentation/post_upload/widget/post_upload_complete_dialog.dart b/lib/presentation/post_upload/widget/post_upload_complete_dialog.dart deleted file mode 100644 index 9dddaeaf..00000000 --- a/lib/presentation/post_upload/widget/post_upload_complete_dialog.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_config.dart'; -import 'package:grimity/app/config/app_router.dart'; -import 'package:grimity/app/config/app_typeface.dart'; -import 'package:grimity/app/util/share_util.dart'; -import 'package:grimity/domain/entity/post.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/presentation/common/widget/alert/grimity_dialog.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; -import 'package:grimity/presentation/common/widget/popup/grimity_share_popup.dart'; - -void showUploadCompleteDialog(BuildContext context, Post post, bool isNewUpload) { - final postUrl = AppConfig.buildPostUrl(post.id); - - showDialog( - context: context, - barrierDismissible: false, - builder: - (dialogContext) => GrimityDialog( - icon: Assets.icons.illust.uploadSuccess, - title: '게시글이 업로드 되었어요', - content: '업로드 소식을 공유해보세요', - confirmText: '닫기', - onConfirm: () { - // 다이얼로그 Pop - dialogContext.pop(); - - // 신규 게시글 업로드 시 - // 게시글 업로드 페이지를 업로드된 페이지로 변경 - if (isNewUpload) { - PostDetailRoute(id: post.id).pushReplacement(context); - } - // 게시글 수정 시 - // 게시글 업로드 페이지 Pop 처리 - else { - context.pop(); - } - }, - linkWidget: GrimityGesture( - onTap: () => ShareUtil.copyLinkToClipboard(postUrl), - child: Container( - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: AppColor.gray500, width: 1))), - child: Row( - spacing: 2, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Assets.icons.icon.linkMinimalistic.svg( - width: 16, - height: 16, - colorFilter: ColorFilter.mode(AppColor.gray600, BlendMode.srcIn), - ), - Text('링크 복사하기', style: AppTypeface.label3.copyWith(color: AppColor.gray600)), - ], - ), - ), - ), - shareWidget: Row( - spacing: 6, - children: [ - Expanded( - child: GrimityGesture( - onTap: () => ShareUtil.shareToTwitter(text: ShareContentType.post.buildShareText(), url: postUrl), - child: Container( - padding: EdgeInsets.symmetric(vertical: 11), - decoration: BoxDecoration( - color: AppColor.gray00, - border: Border.all(color: AppColor.gray300, width: 1), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 4, - children: [ - Assets.icons.brand.x.svg(width: 16, height: 16), - Text('X에 공유', style: AppTypeface.label2.copyWith(color: AppColor.primary5)), - ], - ), - ), - ), - ), - Expanded( - child: GrimityGesture( - onTap: - () => ShareUtil.shareToKakao(description: post.title, imageUrl: post.thumbnail, linkUrl: postUrl), - child: Container( - padding: EdgeInsets.symmetric(vertical: 11), - decoration: BoxDecoration( - color: AppColor.gray00, - border: Border.all(color: AppColor.gray300, width: 1), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 4, - children: [ - Assets.icons.brand.kakaotalk.svg(width: 16, height: 16), - Text('카카오톡에 공유', style: AppTypeface.label2.copyWith(color: AppColor.primary5)), - ], - ), - ), - ), - ), - ], - ), - ), - ); -} diff --git a/lib/presentation/post_upload/widget/post_upload_content_text_field.dart b/lib/presentation/post_upload/widget/post_upload_content_text_field.dart index 8499e118..81a4ae78 100644 --- a/lib/presentation/post_upload/widget/post_upload_content_text_field.dart +++ b/lib/presentation/post_upload/widget/post_upload_content_text_field.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; +import 'package:gds/gds.dart'; import 'package:grimity/app/config/app_typeface_editor.dart'; import 'package:grimity/presentation/post_upload/provider/post_upload_page_argument_provider.dart'; import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; @@ -16,8 +17,10 @@ class PostUploadContentTextField extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final quillController = ref.watch(postUploadQuillControllerArgumentProvider); - final notifier = ref.read(postUploadProvider.notifier); final focusNode = useFocusNode(); + final notifier = ref.read(postUploadProvider.notifier); + final colors = context.gdsColors; + final selectionColor = colors.status.info.withOpacity(0.5); useEffect(() { void listener() { @@ -28,18 +31,31 @@ class PostUploadContentTextField extends HookConsumerWidget { return () => quillController.removeListener(listener); }, [quillController]); - return DefaultTextStyle.merge( - style: AppTypefaceEditor.editorFontFamily, - child: QuillEditor( - focusNode: focusNode, - scrollController: scrollController, - controller: quillController, - config: QuillEditorConfig( - placeholder: '내용을 입력하세요', - scrollable: false, - scrollBottomInset: MediaQuery.of(context).viewInsets.bottom + 12, - customStyles: AppTypefaceEditor.quillDefaultStyles, - embedBuilders: [DeletableImageBuilder(), ...FlutterQuillEmbeds.editorBuilders()], + return Padding( + padding: EdgeInsets.only( + top: GdsSpacing.spacing16, + bottom: GdsSpacing.spacing12, + ), + child: DefaultTextStyle.merge( + style: GdsTypography.label2.copyWith(color: colors.text.grayBold), + child: QuillEditor( + focusNode: focusNode, + scrollController: scrollController, + controller: quillController, + config: QuillEditorConfig( + placeholder: '내용을 입력해주세요', + scrollable: false, + customStyles: AppTypefaceEditor(context: context).defaultStyles, + embedBuilders: [ + DeletableImageBuilder(), + ...FlutterQuillEmbeds.editorBuilders(), + ], + textSelectionThemeData: TextSelectionThemeData( + selectionColor: selectionColor, + cursorColor: colors.status.info, + selectionHandleColor: colors.status.info, + ), + ), ), ), ); diff --git a/lib/presentation/post_upload/widget/post_upload_deletable_image_builder.dart b/lib/presentation/post_upload/widget/post_upload_deletable_image_builder.dart index 9a628d4e..48ccc887 100644 --- a/lib/presentation/post_upload/widget/post_upload_deletable_image_builder.dart +++ b/lib/presentation/post_upload/widget/post_upload_deletable_image_builder.dart @@ -3,9 +3,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gds/gds.dart'; import 'package:grimity/presentation/common/widget/grimity_cached_network_image.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; -import 'package:grimity/presentation/common/widget/system/check/grimity_check_box.dart'; +import 'package:grimity/presentation/post_upload/provider/post_upload_page_argument_provider.dart'; import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; class DeletableImageBuilder extends EmbedBuilder { @@ -37,31 +37,12 @@ class DeletableImageBuilder extends EmbedBuilder { return Consumer( builder: (context, ref, child) { - final state = ref.watch(postUploadProvider); final notifier = ref.read(postUploadProvider.notifier); - final imageEdit = state.imageEdit; - final selected = state.selectedImageUrls.contains(imageUrl); + final quillController = ref.read(postUploadQuillControllerArgumentProvider); - if (!imageEdit) { - return GrimityGesture(onTap: () => notifier.updateImageEdit(true), child: child); - } - - return GrimityGesture( - onTap: () => notifier.toggleSelectedImageUrl(imageUrl), - child: Stack( - children: [ - child!, - Positioned.fill(child: Container(color: Colors.black.withValues(alpha: 0.5))), - Positioned( - right: 16, - top: 16, - child: GrimityCheckBox( - isChecked: selected, - onChanged: (_) => notifier.toggleSelectedImageUrl(imageUrl), - ), - ), - ], - ), + return GdsGesture( + onTap: () => notifier.deleteImage(quillController, imageUrl), + child: child, ); }, child: child, diff --git a/lib/presentation/post_upload/widget/post_upload_title_text_field.dart b/lib/presentation/post_upload/widget/post_upload_title_text_field.dart index 7f9ae9f6..7a28cdb3 100644 --- a/lib/presentation/post_upload/widget/post_upload_title_text_field.dart +++ b/lib/presentation/post_upload/widget/post_upload_title_text_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:grimity/presentation/common/widget/text_field/grimity_underline_text_field.dart'; +import 'package:gds/gds.dart'; import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -20,12 +20,11 @@ class PostUploadTitleTextField extends HookConsumerWidget { return null; }, [title]); - return GrimityUnderlineTextField.normal( - controller: controller, - onChanged: (value) => ref.read(postUploadProvider.notifier).updateTitle(value), - hintText: '제목을 입력하세요', - useBodyTextStyle: true, + return GdsTextField.title( + size: GdsTextFieldSize.small, + placeholder: '제목을 입력해주세요', maxLength: 32, + onChanged: ref.read(postUploadProvider.notifier).updateTitle, ); } } diff --git a/lib/presentation/post_upload/widget/post_upload_toolbar.dart b/lib/presentation/post_upload/widget/post_upload_toolbar.dart new file mode 100644 index 00000000..f0bca74b --- /dev/null +++ b/lib/presentation/post_upload/widget/post_upload_toolbar.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/config/app_router.dart'; +import 'package:grimity/app/util/color_util.dart'; +import 'package:grimity/presentation/common/enum/upload_image_type.dart'; +import 'package:grimity/presentation/post_upload/provider/post_upload_page_argument_provider.dart'; +import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; +import 'package:grimity/presentation/post_upload/widget/post_add_link_bottom_sheet.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class PostUploadToolbar extends HookConsumerWidget { + const PostUploadToolbar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final quillController = ref.watch(postUploadQuillControllerArgumentProvider); + useListenable(quillController); + + final editorType = useState(GdsEditorType.none); + final selectionState = quillController.currentSelectionState; + final fontStyle = _fontStyleFromSelection(selectionState); + final fontColor = _editorColorFromColor(selectionState.textColor); + final fontBgColor = _editorColorFromColor(selectionState.backgroundColor); + + return GdsEditor( + type: editorType.value, + fontStyle: fontStyle, + fontColor: fontColor, + fontBgColor: fontBgColor, + isBoldPressed: selectionState.isBold, + isItalicPressed: selectionState.isItalic, + isUnderlinePressed: selectionState.isUnderline, + isStrikethroughPressed: selectionState.isStrike, + onPlus: () => _togglePanel(editorType, GdsEditorType.plus), + onUndo: quillController.hasUndo ? quillController.undo : null, + onRedo: quillController.hasRedo ? quillController.redo : null, + onFontSize: () => _togglePanel(editorType, GdsEditorType.fontStyle), + onBold: () => quillController.toggle(Attribute.bold), + onItalic: () => quillController.toggle(Attribute.italic), + onUnderline: () => quillController.toggle(Attribute.underline), + onStrikethrough: () => quillController.toggle(Attribute.strikeThrough), + onFontColor: () => _togglePanel(editorType, GdsEditorType.fontColor), + onFontBgColor: () => _togglePanel(editorType, GdsEditorType.fontBgColor), + onKeyboard: () { + editorType.value = GdsEditorType.none; + FocusManager.instance.primaryFocus?.unfocus(); + }, + onImageUpload: () async { + await PhotoSelectRoute(type: UploadImageType.post).push(context); + ref.read(postUploadProvider.notifier).insertImage(controller: quillController); + editorType.value = GdsEditorType.none; + }, + onAddLink: () { + _showLinkPopup(context, quillController); + editorType.value = GdsEditorType.none; + }, + onFontStyleChanged: (style) { + quillController.setHeader(_headerAttributeFromFontStyle(style)); + editorType.value = GdsEditorType.none; + }, + onFontColorChanged: (color) { + quillController.formatSelection(ColorAttribute(_nextColorValue(color, fontColor))); + editorType.value = GdsEditorType.none; + }, + onFontBgColorChanged: (color) { + quillController.formatSelection(BackgroundAttribute(_nextColorValue(color, fontBgColor))); + editorType.value = GdsEditorType.none; + }, + ); + } + + void _togglePanel(ValueNotifier editorType, GdsEditorType nextType) { + editorType.value = editorType.value == nextType ? GdsEditorType.none : nextType; + } + + GdsEditorFontStyle _fontStyleFromSelection(_SelectionState selectionState) { + if (selectionState.isH1) return GdsEditorFontStyle.title1; + if (selectionState.isH2) return GdsEditorFontStyle.title2; + return GdsEditorFontStyle.body; + } + + Attribute? _headerAttributeFromFontStyle(GdsEditorFontStyle style) { + return switch (style) { + GdsEditorFontStyle.title1 => Attribute.h1, + GdsEditorFontStyle.title2 => Attribute.h2, + GdsEditorFontStyle.body => null, + }; + } + + GdsEditorColor? _editorColorFromColor(Color? color) { + if (color == null) return null; + + for (final editorColor in GdsEditorColor.values) { + if (editorColor.color.equalsHexCode(color)) { + return editorColor; + } + } + + return null; + } + + String? _nextColorValue(GdsEditorColor selectedColor, GdsEditorColor? currentColor) { + return selectedColor == currentColor ? null : selectedColor.color.toHexColor(); + } + + void _showLinkPopup(BuildContext context, QuillController controller) { + final selection = controller.selection; + final fullText = controller.document.toPlainText(); + final selectedText = selection.isCollapsed ? '' : fullText.substring(selection.start, selection.end); + final currentUrl = controller.getSelectionStyle().attributes[Attribute.link.key]?.value as String?; + + showPostAddLinkPopup( + context, + initialText: selectedText, + initialUrl: currentUrl, + onSubmit: (text, url) => controller.applyLink(text, url), + ); + } +} + +extension _QuillControllerX on QuillController { + _SelectionState get currentSelectionState { + final attributes = getSelectionStyle().attributes; + final header = attributes[Attribute.header.key]; + final isH1 = header?.value == 1; + final isH2 = header?.value == 2; + + final textColor = attributes[Attribute.color.key]?.value as String?; + final backgroundColor = attributes[Attribute.background.key]?.value as String?; + + return _SelectionState( + isH1: isH1, + isH2: isH2, + isBold: attributes.containsKey(Attribute.bold.key), + isItalic: attributes.containsKey(Attribute.italic.key), + isUnderline: attributes.containsKey(Attribute.underline.key), + isStrike: attributes.containsKey(Attribute.strikeThrough.key), + textColor: ColorUtil.parseHex(textColor), + backgroundColor: ColorUtil.parseHex(backgroundColor), + ); + } + + void toggle(Attribute attribute) { + final attributes = getSelectionStyle().attributes; + final unsetAttribute = Attribute(attribute.key, attribute.scope, null); + formatSelection(attributes.containsKey(attribute.key) ? unsetAttribute : attribute); + } + + void setHeader(Attribute? attribute) { + formatSelection(attribute ?? Attribute(Attribute.header.key, AttributeScope.block, null)); + } + + void applyLink(String text, String url) { + final selection = this.selection; + final start = selection.start; + final end = selection.end; + + if (selection.isCollapsed) { + final label = (text.trim().isEmpty ? url : text).trim(); + if (label.isEmpty) return; + + replaceText(start, 0, label, TextSelection.collapsed(offset: start + label.length)); + updateSelection(TextSelection(baseOffset: start, extentOffset: start + label.length), ChangeSource.local); + formatSelection(LinkAttribute(url)); + return; + } + + final fullText = document.toPlainText(); + final currentText = fullText.substring(start, end); + final label = (text.trim().isEmpty ? currentText : text).trim(); + + if (label != currentText) { + replaceText(start, end - start, label, TextSelection(baseOffset: start, extentOffset: start + label.length)); + } else { + updateSelection(TextSelection(baseOffset: start, extentOffset: start + label.length), ChangeSource.local); + } + + formatSelection(LinkAttribute(url)); + } +} + +class _SelectionState { + const _SelectionState({ + required this.isH1, + required this.isH2, + required this.isBold, + required this.isItalic, + required this.isUnderline, + required this.isStrike, + required this.textColor, + required this.backgroundColor, + }); + + final bool isH1; + final bool isH2; + final bool isBold; + final bool isItalic; + final bool isUnderline; + final bool isStrike; + final Color? textColor; + final Color? backgroundColor; +} diff --git a/lib/presentation/post_upload/widget/toolbar/image_delete_toolbar.dart b/lib/presentation/post_upload/widget/toolbar/image_delete_toolbar.dart deleted file mode 100644 index 090c3821..00000000 --- a/lib/presentation/post_upload/widget/toolbar/image_delete_toolbar.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_typeface.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; -import 'package:grimity/presentation/post_upload/provider/post_upload_page_argument_provider.dart'; -import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; - -class ImageDeleteToolbar extends ConsumerWidget { - const ImageDeleteToolbar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(postUploadProvider); - final notifier = ref.read(postUploadProvider.notifier); - final deletable = state.selectedImageUrls.isEmpty; - - return Container( - padding: EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: AppColor.gray00, - border: Border(top: BorderSide(color: AppColor.gray300, width: 1)), - ), - height: 42, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GrimityGesture( - onTap: () => notifier.updateImageEdit(false), - child: Text('취소', style: AppTypeface.label3.copyWith(color: AppColor.gray700)), - ), - GrimityGesture( - onTap: - deletable - ? null - : () => ref - .read(postUploadProvider.notifier) - .deleteSelectedImage(ref.read(postUploadQuillControllerArgumentProvider)), - child: Text( - '삭제', - style: AppTypeface.label3.copyWith(color: deletable ? AppColor.gray500 : AppColor.accentRed), - ), - ), - ], - ), - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/color_menu.dart b/lib/presentation/post_upload/widget/toolbar/parts/color_menu.dart deleted file mode 100644 index c5b99fa1..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/color_menu.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/util/color_util.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/menu_archor_square.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart'; -import 'package:grimity/gen/assets.gen.dart'; - -enum _Mode { text, background } - -class ColorMenu extends HookWidget { - const ColorMenu._({required this.controller, required this.selectionState, bool isText = true}) - : _mode = isText ? _Mode.text : _Mode.background; - - factory ColorMenu.text({required QuillController controller, required SelectionState selectionState}) => - ColorMenu._(controller: controller, selectionState: selectionState, isText: true); - - factory ColorMenu.background({required QuillController controller, required SelectionState selectionState}) => - ColorMenu._(controller: controller, selectionState: selectionState, isText: false); - - final QuillController controller; - final SelectionState selectionState; - final _Mode _mode; - - static const _colors = [ - Color(0xFF000000), - Color(0xFF999999), - Color(0xFFC4C4C4), - Color(0xFFF34343), - Color(0xFFF89232), - Color(0xFFFFC824), - Color(0xFF3ECF45), - Color(0xFF44ADF8), - Color(0xFF3457EF), - Color(0xFFF13087), - Color(0xFFA724F8), - Color(0xFF754611), - ]; - - @override - Widget build(BuildContext context) { - final menuController = useMemoized(() => MenuController()); - useEffect(() { - return () => menuController.close(); - }, [menuController]); - - final color = _mode == _Mode.text ? selectionState.textColor : selectionState.backgroundColor; - - return MenuAnchorSquare( - menuController: menuController, - padding: EdgeInsets.all(16), - icon: Stack( - children: [ - if (_mode == _Mode.background) - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - color: color ?? Colors.transparent, - border: Border.all(color: AppColor.gray300, width: 1), - ), - ), - ), - Assets.icons.editor.textcolor.svg( - colorFilter: - (_mode == _Mode.text) - ? ColorFilter.mode(AppColor.gray700, BlendMode.srcIn) - : color == null || color.equalsHexCode(_colors[2]) || color.equalsHexCode(_colors[5]) - ? ColorFilter.mode(AppColor.gray700, BlendMode.srcIn) - : ColorFilter.mode(AppColor.gray00, BlendMode.srcIn), - ), - if (_mode == _Mode.text) Positioned(right: 4, bottom: 4, child: Container(width: 4, height: 4, color: color)), - ], - ), - menuChildren: [ - MenuItemButton( - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith((_) => Colors.white), - padding: WidgetStatePropertyAll(EdgeInsets.zero), - ), - child: SizedBox( - width: 94, - child: Wrap( - spacing: 8, - runSpacing: 8, - children: - _colors.map((c) { - final isSelected = c.equalsHexCode(color); - - return GrimityGesture( - onTap: () { - final hex = c.toHexColor(); - - if (_mode == _Mode.text) { - controller.formatSelection(ColorAttribute(isSelected ? null : hex)); - } else { - controller.formatSelection(BackgroundAttribute(isSelected ? null : hex)); - } - menuController.close(); - }, - child: _ColorDot(color: c, isSelected: isSelected), - ); - }).toList(), - ), - ), - ), - ], - ); - } -} - -class _ColorDot extends StatelessWidget { - final Color color; - final bool isSelected; - - const _ColorDot({required this.color, this.isSelected = false}); - - @override - Widget build(BuildContext context) { - final size = 26.0; - - return Container( - width: size, - height: size, - decoration: - isSelected - ? BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: AppColor.gray700, width: 1.5), - color: AppColor.gray00, - ) - : BoxDecoration(shape: BoxShape.circle, color: color), - child: - isSelected - ? Center( - child: Container( - width: size - 3.0 - 3.0, - height: size - 3.0 - 3.0, - decoration: BoxDecoration(shape: BoxShape.circle, color: color), - ), - ) - : null, - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/header_menu.dart b/lib/presentation/post_upload/widget/toolbar/parts/header_menu.dart deleted file mode 100644 index 0b6e399b..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/header_menu.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/menu_archor_square.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart'; - -class HeaderMenu extends StatelessWidget { - const HeaderMenu({super.key, required this.controller, required this.selectionState}); - - final QuillController controller; - final SelectionState selectionState; - - @override - Widget build(BuildContext context) { - return MenuAnchorSquare( - icon: Assets.icons.editor.head.svg(), - menuChildren: [ - _headerItem('제목1', selectionState.isH1, () => controller.setHeader(Attribute.h1)), - _headerItem('제목2', selectionState.isH2, () => controller.setHeader(Attribute.h2)), - _headerItem('본문', selectionState.isNormal, () => controller.setHeader(null)), - ], - ); - } - - MenuItemButton _headerItem(String label, bool selected, VoidCallback onTap) { - return MenuItemButton( - style: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith((_) => selected ? AppColor.main.withValues(alpha: 0.1) : null), - overlayColor: WidgetStateProperty.resolveWith( - (states) => states.contains(WidgetState.pressed) ? AppColor.gray200 : null, - ), - ), - onPressed: onTap, - child: Container( - width: 96, - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 10), - child: Text(label, style: TextStyle(fontSize: label == '제목1' ? 18 : 16, fontWeight: FontWeight.w600)), - ), - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/import_menu.dart b/lib/presentation/post_upload/widget/toolbar/parts/import_menu.dart deleted file mode 100644 index 633b253c..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/import_menu.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:gap/gap.dart'; -import 'package:grimity/presentation/post_upload/widget/post_add_link_bottom_sheet.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/menu_archor_square.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:grimity/app/config/app_typeface.dart'; -import 'package:grimity/app/config/app_router.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/presentation/common/enum/upload_image_type.dart'; -import 'package:grimity/presentation/post_upload/provider/post_upload_provider.dart'; - -class ImportMenu extends ConsumerWidget { - const ImportMenu({super.key, required this.controller}); - - final QuillController controller; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return MenuAnchorSquare( - icon: Assets.icons.editor.import.svg(), - menuChildren: [ - _menuItem( - icon: Assets.icons.icon.camera.svg( - width: 22, - height: 22, - ), - label: '그림 업로드', - onTap: () async { - await PhotoSelectRoute(type: UploadImageType.post).push(context); - ref.read(postUploadProvider.notifier).insertImage(controller: controller); - }, - ), - _menuItem( - icon: Assets.icons.editor.link.svg(), - label: '링크 추가', - onTap: () { - final sel = controller.selection; - final fullText = controller.document.toPlainText(); - final selectedText = sel.isCollapsed ? '' : fullText.substring(sel.start, sel.end); - final currentUrl = controller.getSelectionStyle().attributes[Attribute.link.key]?.value as String?; - PostAddLinkBottomSheet.show( - context, - initialText: selectedText, - initialUrl: currentUrl, - onSubmit: (text, url) => controller.applyLink(text, url), - ); - }, - ), - ], - ); - } - - Widget _menuItem({required Widget icon, required String label, required VoidCallback onTap}) { - return MenuItemButton( - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith( - (states) => states.contains(WidgetState.pressed) ? AppColor.gray200 : null, - ), - ), - onPressed: onTap, - child: Container( - width: 165, - padding: EdgeInsets.symmetric(vertical: 16, horizontal: 10), - child: Row(children: [icon, Gap(10), Text(label, style: AppTypeface.label3.copyWith(color: AppColor.gray700))]), - ), - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/menu_archor_square.dart b/lib/presentation/post_upload/widget/toolbar/parts/menu_archor_square.dart deleted file mode 100644 index 31702e42..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/menu_archor_square.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'toolbar_buttons.dart'; - -class MenuAnchorSquare extends StatelessWidget { - const MenuAnchorSquare({ - super.key, - required this.icon, - required this.menuChildren, - this.padding = const EdgeInsets.symmetric(vertical: 4), - this.menuController, - }); - - final Widget icon; - final List menuChildren; - final EdgeInsets padding; - final MenuController? menuController; - - @override - Widget build(BuildContext context) { - return MenuAnchor( - controller: menuController, - style: MenuStyle( - /// menuAnchor의 menuChild가 가상 키보드가 올라왔을때 가려지는 이슈가 있음. - /// Alignment의 y를 크게하면 위쪽으로 나와서 이렇게 일단 대응.. - /// issue : https://github.com/flutter/flutter/issues/142921 - alignment: Alignment(-1, 50), - padding: WidgetStatePropertyAll(padding), - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), - backgroundColor: WidgetStatePropertyAll(AppColor.gray00), - ), - alignmentOffset: Offset(0, 12), - menuChildren: menuChildren, - builder: - (context, anchor, child) => IconSquareButton( - activeColor: AppColor.gray200, - active: anchor.isOpen, - onTap: () { - if (anchor.isOpen) { - anchor.close(); - } else { - anchor.open(); - } - }, - child: icon, - ), - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart b/lib/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart deleted file mode 100644 index 7734b95d..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:grimity/app/util/color_util.dart'; - -extension QuillControllerX on QuillController { - SelectionState get currentSelectionState { - final attrs = getSelectionStyle().attributes; - final header = attrs[Attribute.header.key]; - final isH1 = header?.value == 1; - final isH2 = header?.value == 2; - - final textColorAttr = attrs[Attribute.color.key]?.value as String?; - final bgColorAttr = attrs[Attribute.background.key]?.value as String?; - - return SelectionState( - isH1: isH1, - isH2: isH2, - isNormal: !(isH1 || isH2), - isBold: attrs.containsKey(Attribute.bold.key), - isItalic: attrs.containsKey(Attribute.italic.key), - isUnderline: attrs.containsKey(Attribute.underline.key), - isStrike: attrs.containsKey(Attribute.strikeThrough.key), - textColor: ColorUtil.parseHex(textColorAttr), - backgroundColor: ColorUtil.parseHex(bgColorAttr), - ); - } - - void toggle(Attribute attribute) { - final attributes = getSelectionStyle().attributes; - final already = attributes.containsKey(attribute.key); - final unset = Attribute(attribute.key, attribute.scope, null); - formatSelection(already ? unset : attribute); - } - - void setHeader(Attribute? attribute) { - if (attribute == null) { - formatSelection(Attribute(Attribute.header.key, AttributeScope.block, null)); - } else { - formatSelection(attribute); - } - } - - void applyLink(String text, String url) { - final sel = selection; - final start = sel.start, end = sel.end; - if (sel.isCollapsed) { - final label = (text.trim().isEmpty ? url : text).trim(); - if (label.isEmpty) return; - replaceText(start, 0, label, TextSelection.collapsed(offset: start + label.length)); - updateSelection(TextSelection(baseOffset: start, extentOffset: start + label.length), ChangeSource.local); - formatSelection(LinkAttribute(url)); - return; - } - final fullText = document.toPlainText(); - final current = fullText.substring(start, end); - final newLabel = (text.trim().isEmpty ? current : text).trim(); - - if (newLabel != current) { - replaceText( - start, - end - start, - newLabel, - TextSelection(baseOffset: start, extentOffset: start + newLabel.length), - ); - } else { - updateSelection(TextSelection(baseOffset: start, extentOffset: start + newLabel.length), ChangeSource.local); - } - formatSelection(LinkAttribute(url)); - } -} - -class SelectionState { - final bool isH1, isH2, isNormal, isBold, isItalic, isUnderline, isStrike; - final Color? textColor, backgroundColor; - - const SelectionState({ - required this.isH1, - required this.isH2, - required this.isNormal, - required this.isBold, - required this.isItalic, - required this.isUnderline, - required this.isStrike, - required this.textColor, - required this.backgroundColor, - }); -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/text_style_group.dart b/lib/presentation/post_upload/widget/toolbar/parts/text_style_group.dart deleted file mode 100644 index ca69be7c..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/text_style_group.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart'; -import 'toolbar_buttons.dart'; - -class TextStyleGroup extends StatelessWidget { - const TextStyleGroup({super.key, required this.controller, required this.selectionState}); - - final QuillController controller; - final SelectionState selectionState; - - @override - Widget build(BuildContext context) { - return Row( - spacing: 10, - children: [ - IconSquareButton( - active: selectionState.isBold, - onTap: () => controller.toggle(Attribute.bold), - child: Assets.icons.editor.bold.svg( - colorFilter: selectionState.isBold ? const ColorFilter.mode(Colors.white, BlendMode.srcIn) : null, - ), - ), - IconSquareButton( - active: selectionState.isItalic, - onTap: () => controller.toggle(Attribute.italic), - child: Assets.icons.editor.italic.svg( - colorFilter: selectionState.isItalic ? const ColorFilter.mode(Colors.white, BlendMode.srcIn) : null, - ), - ), - IconSquareButton( - active: selectionState.isUnderline, - onTap: () => controller.toggle(Attribute.underline), - child: Assets.icons.editor.underline.svg( - colorFilter: selectionState.isUnderline ? const ColorFilter.mode(Colors.white, BlendMode.srcIn) : null, - ), - ), - IconSquareButton( - active: selectionState.isStrike, - onTap: () => controller.toggle(Attribute.strikeThrough), - child: Assets.icons.editor.strikethrough.svg( - colorFilter: selectionState.isStrike ? const ColorFilter.mode(Colors.white, BlendMode.srcIn) : null, - ), - ), - ], - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/toolbar_buttons.dart b/lib/presentation/post_upload/widget/toolbar/parts/toolbar_buttons.dart deleted file mode 100644 index 58598ff9..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/toolbar_buttons.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/presentation/common/widget/grimity_animation_button.dart'; - -class IconSquareButton extends StatelessWidget { - const IconSquareButton({ - super.key, - required this.child, - this.active = false, - this.activeColor = AppColor.gray700, - this.onTap, - }); - - final Widget child; - final bool active; - final Color? activeColor; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return Container( - width: 32, - height: 32, - padding: EdgeInsets.all(4), - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: active ? activeColor : AppColor.gray00), - child: onTap != null ? GrimityAnimationButton(onTap: onTap!, child: child) : child, - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/toolbar_container.dart b/lib/presentation/post_upload/widget/toolbar/parts/toolbar_container.dart deleted file mode 100644 index 0dfa53a4..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/toolbar_container.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:grimity/app/config/app_color.dart'; - -class ToolbarContainer extends StatelessWidget { - const ToolbarContainer({super.key, required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: AppColor.gray00, - border: Border(top: BorderSide(color: AppColor.gray300, width: 1)), - ), - height: 42, - child: SingleChildScrollView(scrollDirection: Axis.horizontal, child: child), - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/parts/undo_redo_group.dart b/lib/presentation/post_upload/widget/toolbar/parts/undo_redo_group.dart deleted file mode 100644 index c387576a..00000000 --- a/lib/presentation/post_upload/widget/toolbar/parts/undo_redo_group.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'toolbar_buttons.dart'; - -class UndoRedoGroup extends StatelessWidget { - const UndoRedoGroup({super.key, required this.controller}); - - final QuillController controller; - - @override - Widget build(BuildContext context) { - return Row( - spacing: 10, - children: [ - IconSquareButton( - onTap: controller.hasUndo ? controller.undo : null, - child: Assets.icons.editor.undo.svg( - colorFilter: ColorFilter.mode(controller.hasUndo ? AppColor.gray700 : AppColor.gray500, BlendMode.srcIn), - ), - ), - IconSquareButton( - onTap: controller.hasRedo ? controller.redo : null, - child: Assets.icons.editor.redo.svg( - colorFilter: ColorFilter.mode(controller.hasRedo ? AppColor.gray700 : AppColor.gray500, BlendMode.srcIn), - ), - ), - ], - ); - } -} diff --git a/lib/presentation/post_upload/widget/toolbar/quill_bottom_sheet_toolbar.dart b/lib/presentation/post_upload/widget/toolbar/quill_bottom_sheet_toolbar.dart deleted file mode 100644 index 55f38799..00000000 --- a/lib/presentation/post_upload/widget/toolbar/quill_bottom_sheet_toolbar.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/presentation/post_upload/provider/post_upload_page_argument_provider.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/color_menu.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/header_menu.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/import_menu.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/quill_toolbar_utils.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/text_style_group.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/toolbar_container.dart'; -import 'package:grimity/presentation/post_upload/widget/toolbar/parts/undo_redo_group.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class QuillBottomSheetToolbar extends ConsumerWidget { - const QuillBottomSheetToolbar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final quillController = ref.watch(postUploadQuillControllerArgumentProvider); - final selectionState = quillController.currentSelectionState; - - return ToolbarContainer( - child: Row( - spacing: 10, - children: [ - Gap(6), - - /// import(Image, Link), - ImportMenu(controller: quillController), - - /// undo, redo - UndoRedoGroup(controller: quillController), - - /// verticalDivider - _buildVerticalDivider(), - - /// headLine(H1, H2, 본문) - HeaderMenu(controller: quillController, selectionState: selectionState), - - /// bold, italic, underLine, strikeThrough - TextStyleGroup(controller: quillController, selectionState: selectionState), - - /// textColor, backgroundColor - ColorMenu.text(controller: quillController, selectionState: selectionState), - ColorMenu.background(controller: quillController, selectionState: selectionState), - - Gap(6), - ], - ), - ); - } - - Widget _buildVerticalDivider() => - Padding(padding: EdgeInsets.symmetric(vertical: 8), child: VerticalDivider(color: AppColor.gray300, width: 1)); -} From 28cc61d045a09e578be9ab512ca17e35768a78d9 Mon Sep 17 00:00:00 2001 From: Dev Ttangkong Date: Mon, 6 Jul 2026 09:21:44 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EA=B8=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EB=AA=A9=20=ED=95=84=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=88=84=EB=9D=BD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/config/app_typeface_editor.dart | 1 - .../post_upload/widget/post_upload_title_text_field.dart | 1 + .../post_upload/widget/post_upload_toolbar.dart | 7 +++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/app/config/app_typeface_editor.dart b/lib/app/config/app_typeface_editor.dart index b6fd2402..4779c5e9 100644 --- a/lib/app/config/app_typeface_editor.dart +++ b/lib/app/config/app_typeface_editor.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:gds/gds.dart'; -import 'package:grimity/app/config/app_color.dart'; class AppTypefaceEditor { const AppTypefaceEditor({required this.context}); diff --git a/lib/presentation/post_upload/widget/post_upload_title_text_field.dart b/lib/presentation/post_upload/widget/post_upload_title_text_field.dart index 7a28cdb3..b50f8498 100644 --- a/lib/presentation/post_upload/widget/post_upload_title_text_field.dart +++ b/lib/presentation/post_upload/widget/post_upload_title_text_field.dart @@ -22,6 +22,7 @@ class PostUploadTitleTextField extends HookConsumerWidget { return GdsTextField.title( size: GdsTextFieldSize.small, + controller: controller, placeholder: '제목을 입력해주세요', maxLength: 32, onChanged: ref.read(postUploadProvider.notifier).updateTitle, diff --git a/lib/presentation/post_upload/widget/post_upload_toolbar.dart b/lib/presentation/post_upload/widget/post_upload_toolbar.dart index f0bca74b..db0af5c1 100644 --- a/lib/presentation/post_upload/widget/post_upload_toolbar.dart +++ b/lib/presentation/post_upload/widget/post_upload_toolbar.dart @@ -49,8 +49,11 @@ class PostUploadToolbar extends HookConsumerWidget { }, onImageUpload: () async { await PhotoSelectRoute(type: UploadImageType.post).push(context); - ref.read(postUploadProvider.notifier).insertImage(controller: quillController); - editorType.value = GdsEditorType.none; + + if (context.mounted) { + ref.read(postUploadProvider.notifier).insertImage(controller: quillController); + editorType.value = GdsEditorType.none; + } }, onAddLink: () { _showLinkPopup(context, quillController);