Add Scribe mobile
Same features than Scribe desktop but as a full screen modal instead of a context menu. Reuse menu and submenu components. Reuse suggestion components. Reuse 99% of Scribe desktop style.
This commit is contained in:
@@ -46,3 +46,6 @@ export 'scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_suc
|
||||
export 'scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart';
|
||||
export 'scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart';
|
||||
export 'scribe/ai/presentation/widgets/search/ai_scribe_bar.dart';
|
||||
export 'scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_bottom_sheet.dart';
|
||||
export 'scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_item.dart';
|
||||
export 'scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_suggestion_bottom_sheet.dart';
|
||||
|
||||
@@ -181,4 +181,7 @@ abstract final class AIScribeSizes {
|
||||
|
||||
static const EdgeInsetsGeometry sendIconPadding =
|
||||
EdgeInsetsDirectional.all(8);
|
||||
|
||||
static const EdgeInsetsGeometry backIconPadding =
|
||||
EdgeInsetsDirectional.only(end: 8.0, top: 8.0, bottom: 8.0);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:scribe/scribe.dart';
|
||||
@@ -16,21 +17,30 @@ class AiScribeModalManager {
|
||||
ModalPlacement? preferredPlacement,
|
||||
ModalCrossAxisAlignment crossAxisAlignment = ModalCrossAxisAlignment.center,
|
||||
}) async {
|
||||
final ContextSubmenuController submenuController = ContextSubmenuController();
|
||||
final AIAction? aiAction;
|
||||
|
||||
final aiAction = await Get.dialog<AIAction>(
|
||||
AiScribeModalWidget(
|
||||
if (PlatformInfo.isMobile) {
|
||||
aiAction = await showMobileAIScribeMenuModal(
|
||||
imagePaths: imagePaths,
|
||||
content: content,
|
||||
availableCategories: availableCategories,
|
||||
buttonPosition: buttonPosition,
|
||||
buttonSize: buttonSize,
|
||||
preferredPlacement: preferredPlacement,
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
submenuController: submenuController,
|
||||
),
|
||||
barrierColor: AIScribeColors.dialogBarrier,
|
||||
).whenComplete(submenuController.dispose);
|
||||
);
|
||||
} else {
|
||||
final ContextSubmenuController submenuController = ContextSubmenuController();
|
||||
|
||||
aiAction = await Get.dialog<AIAction>(
|
||||
AiScribeModalWidget(
|
||||
imagePaths: imagePaths,
|
||||
content: content,
|
||||
availableCategories: availableCategories,
|
||||
buttonPosition: buttonPosition,
|
||||
buttonSize: buttonSize,
|
||||
preferredPlacement: preferredPlacement,
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
submenuController: submenuController,
|
||||
),
|
||||
barrierColor: AIScribeColors.dialogBarrier,
|
||||
).whenComplete(submenuController.dispose);
|
||||
}
|
||||
|
||||
if (aiAction != null) {
|
||||
await showAIScribeSuggestionModal(
|
||||
@@ -56,18 +66,70 @@ class AiScribeModalManager {
|
||||
ModalPlacement? preferredPlacement,
|
||||
ModalCrossAxisAlignment crossAxisAlignment = ModalCrossAxisAlignment.center,
|
||||
}) async {
|
||||
await Get.dialog<String?>(
|
||||
AiScribeSuggestionWidget(
|
||||
if (PlatformInfo.isMobile) {
|
||||
await showMobileAIScribeSuggestionModal(
|
||||
aiAction: aiAction,
|
||||
imagePaths: imagePaths,
|
||||
content: content,
|
||||
buttonPosition: buttonPosition,
|
||||
buttonSize: buttonSize,
|
||||
preferredPlacement: preferredPlacement,
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
|
||||
);
|
||||
} else {
|
||||
await Get.dialog<String?>(
|
||||
AiScribeSuggestionWidget(
|
||||
aiAction: aiAction,
|
||||
imagePaths: imagePaths,
|
||||
content: content,
|
||||
buttonPosition: buttonPosition,
|
||||
buttonSize: buttonSize,
|
||||
preferredPlacement: preferredPlacement,
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
|
||||
),
|
||||
barrierColor: AIScribeColors.dialogBarrier,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<AIAction?> showMobileAIScribeMenuModal({
|
||||
required ImagePaths imagePaths,
|
||||
required List<AIScribeMenuCategory> availableCategories,
|
||||
}) async {
|
||||
final context = Get.context;
|
||||
|
||||
if (context == null) return null;
|
||||
|
||||
return await showModalBottomSheet<AIAction>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (context) => AiScribeMobileActionsBottomSheet(
|
||||
imagePaths: imagePaths,
|
||||
availableCategories: availableCategories,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> showMobileAIScribeSuggestionModal({
|
||||
required AIAction aiAction,
|
||||
required ImagePaths imagePaths,
|
||||
required OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction,
|
||||
String? content,
|
||||
}) async {
|
||||
final context = Get.context;
|
||||
|
||||
if (context == null) return;
|
||||
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
isDismissible: true,
|
||||
builder: (context) => AiScribeMobileSuggestionBottomSheet(
|
||||
aiAction: aiAction,
|
||||
imagePaths: imagePaths,
|
||||
content: content,
|
||||
onSelectAction: onSelectAiScribeSuggestionAction,
|
||||
),
|
||||
barrierColor: AIScribeColors.dialogBarrier,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scribe/scribe.dart';
|
||||
|
||||
class AiScribeMobileActionsBottomSheet extends StatefulWidget {
|
||||
final ImagePaths imagePaths;
|
||||
final List<AIScribeMenuCategory> availableCategories;
|
||||
|
||||
const AiScribeMobileActionsBottomSheet({
|
||||
super.key,
|
||||
required this.imagePaths,
|
||||
required this.availableCategories,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AiScribeMobileActionsBottomSheet> createState() =>
|
||||
_AiScribeMobileActionsBottomSheetState();
|
||||
}
|
||||
|
||||
class _AiScribeMobileActionsBottomSheetState
|
||||
extends State<AiScribeMobileActionsBottomSheet> {
|
||||
final ValueNotifier<AiScribeCategoryContextMenuAction?> _selectedCategory =
|
||||
ValueNotifier(null);
|
||||
|
||||
void _onActionSelected(AiScribeContextMenuAction menuAction) {
|
||||
if (menuAction is AiScribeActionContextMenuAction) {
|
||||
Navigator.of(context).pop(PredefinedAction(menuAction.action));
|
||||
}
|
||||
}
|
||||
|
||||
void _onCustomPromptSubmit(String prompt) {
|
||||
Navigator.of(context).pop(CustomPromptAction(prompt));
|
||||
}
|
||||
|
||||
void _onCategorySelected(AiScribeCategoryContextMenuAction category) {
|
||||
_selectedCategory.value = category;
|
||||
}
|
||||
|
||||
void _goBackToCategories() {
|
||||
_selectedCategory.value = null;
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context, ScribeLocalizations localizations) {
|
||||
return Container(
|
||||
padding: AIScribeSizes.suggestionHeaderPadding,
|
||||
child: Row(
|
||||
children: [
|
||||
ValueListenableBuilder<AiScribeCategoryContextMenuAction?>(
|
||||
valueListenable: _selectedCategory,
|
||||
builder: (context, selectedCategory, _) {
|
||||
return selectedCategory != null
|
||||
? GestureDetector(
|
||||
onTap: _goBackToCategories,
|
||||
child: Padding(
|
||||
padding: AIScribeSizes.backIconPadding,
|
||||
child: Icon(
|
||||
Icons.chevron_left,
|
||||
size: AIScribeSizes.aiAssistantIcon,
|
||||
color: AppColor.gray424244.withValues(alpha: 0.72),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<AiScribeCategoryContextMenuAction?>(
|
||||
valueListenable: _selectedCategory,
|
||||
builder: (context, selectedCategory, _) {
|
||||
return Text(
|
||||
selectedCategory?.actionName ?? localizations.aiAssistant,
|
||||
style: AIScribeTextStyles.suggestionTitle,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: AIScribeSizes.aiAssistantIcon,
|
||||
),
|
||||
color: AppColor.gray424244.withValues(alpha: 0.72),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuListView(List<AiScribeCategoryContextMenuAction> menuActions) {
|
||||
return ListView.builder(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: menuActions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final menuAction = menuActions[index];
|
||||
return AiScribeMobileActionsItem(
|
||||
menuAction: menuAction,
|
||||
imagePaths: widget.imagePaths,
|
||||
onCategorySelected: _onCategorySelected,
|
||||
onActionSelected: _onActionSelected,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubmenuListView() {
|
||||
return ValueListenableBuilder<AiScribeCategoryContextMenuAction?>(
|
||||
valueListenable: _selectedCategory,
|
||||
builder: (context, selectedCategory, _) {
|
||||
return ListView.builder(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: selectedCategory?.submenuActions?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final submenuAction = selectedCategory!.submenuActions![index];
|
||||
return AiScribeSubmenuItem(
|
||||
menuAction: submenuAction,
|
||||
onSelectAction: _onActionSelected,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: AppColor.colorDividerComposer,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 8.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
top: 8.0,
|
||||
),
|
||||
child: AIScribeBar(
|
||||
onCustomPrompt: _onCustomPromptSubmit,
|
||||
imagePaths: widget.imagePaths,
|
||||
boxShadow: const [],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectedCategory.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = ScribeLocalizations.of(context);
|
||||
final menuActions = widget.availableCategories
|
||||
.map((category) => AiScribeCategoryContextMenuAction(
|
||||
category,
|
||||
localizations,
|
||||
widget.imagePaths,
|
||||
))
|
||||
.toList();
|
||||
|
||||
return Container(
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: AIScribeColors.background,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeader(context, localizations),
|
||||
Flexible(
|
||||
child: ValueListenableBuilder<AiScribeCategoryContextMenuAction?>(
|
||||
valueListenable: _selectedCategory,
|
||||
builder: (context, selectedCategory, _) {
|
||||
return selectedCategory == null
|
||||
? _buildMenuListView(menuActions)
|
||||
: _buildSubmenuListView();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildBottomBar(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scribe/scribe.dart';
|
||||
|
||||
class AiScribeMobileActionsItem extends StatelessWidget {
|
||||
final AiScribeContextMenuAction menuAction;
|
||||
final ImagePaths imagePaths;
|
||||
final ValueChanged<AiScribeCategoryContextMenuAction>? onCategorySelected;
|
||||
final ValueChanged<AiScribeContextMenuAction> onActionSelected;
|
||||
|
||||
const AiScribeMobileActionsItem({
|
||||
super.key,
|
||||
required this.menuAction,
|
||||
required this.imagePaths,
|
||||
this.onCategorySelected,
|
||||
required this.onActionSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// When category
|
||||
if (menuAction.hasSubmenu) {
|
||||
return AiScribeMenuItem(
|
||||
menuAction: menuAction,
|
||||
imagePaths: imagePaths,
|
||||
onSelectAction: (menuAction) {
|
||||
if (menuAction is AiScribeCategoryContextMenuAction) {
|
||||
onCategorySelected?.call(menuAction);
|
||||
} else {
|
||||
onActionSelected.call(menuAction);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// When action alongside category
|
||||
final submenuActions = menuAction.submenuActions;
|
||||
if (submenuActions?.length == 1) {
|
||||
return AiScribeSubmenuItem(
|
||||
menuAction: submenuActions!.first,
|
||||
onSelectAction: onActionSelected,
|
||||
);
|
||||
}
|
||||
|
||||
// When action inside category
|
||||
return AiScribeMenuItem(
|
||||
menuAction: menuAction,
|
||||
imagePaths: imagePaths,
|
||||
onSelectAction: onActionSelected,
|
||||
);
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scribe/scribe.dart';
|
||||
|
||||
class AiScribeMobileSuggestionBottomSheet extends StatefulWidget {
|
||||
final AIAction aiAction;
|
||||
final ImagePaths imagePaths;
|
||||
final String? content;
|
||||
final OnSelectAiScribeSuggestionAction onSelectAction;
|
||||
|
||||
const AiScribeMobileSuggestionBottomSheet({
|
||||
super.key,
|
||||
required this.aiAction,
|
||||
required this.imagePaths,
|
||||
required this.onSelectAction,
|
||||
this.content,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AiScribeMobileSuggestionBottomSheet> createState() =>
|
||||
_AiScribeMobileSuggestionBottomSheetState();
|
||||
}
|
||||
|
||||
class _AiScribeMobileSuggestionBottomSheetState
|
||||
extends State<AiScribeMobileSuggestionBottomSheet>
|
||||
with AiScribeSuggestionStateMixin {
|
||||
@override
|
||||
AIAction get aiAction => widget.aiAction;
|
||||
|
||||
@override
|
||||
String? get content => widget.content;
|
||||
|
||||
@override
|
||||
ImagePaths get imagePaths => widget.imagePaths;
|
||||
|
||||
@override
|
||||
OnSelectAiScribeSuggestionAction get onSelectAction =>
|
||||
widget.onSelectAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = ScribeLocalizations.of(context);
|
||||
|
||||
return Container(
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: AIScribeColors.background,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: AIScribeSizes.suggestionHeaderPadding,
|
||||
child: AiScribeSuggestionHeader(
|
||||
title: aiAction.getLabel(localizations),
|
||||
imagePaths: imagePaths,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: buildStateContent(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildLoadingState() {
|
||||
return Padding(
|
||||
padding: AIScribeSizes.suggestionContentPadding,
|
||||
child: super.buildLoadingState(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildErrorState() {
|
||||
return Padding(
|
||||
padding: AIScribeSizes.suggestionContentPadding,
|
||||
child: super.buildErrorState(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuccessState(
|
||||
String suggestionText,
|
||||
bool hasContent,
|
||||
) {
|
||||
return Padding(
|
||||
padding: AIScribeSizes.suggestionContentPadding,
|
||||
child: super.buildSuccessState(suggestionText, hasContent),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user