mirror of
https://github.com/friebetill/TubeCards.git
synced 2025-08-26 01:54:23 +08:00
Initial commit
Add Space version 2.0.1
This commit is contained in:
38
lib/modules/draw_image/component/discard_dialog.dart
Normal file
38
lib/modules/draw_image/component/discard_dialog.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../i18n/i18n.dart';
|
||||
import '../../../utils/custom_navigator.dart';
|
||||
|
||||
class DiscardDialog extends StatelessWidget {
|
||||
const DiscardDialog({required this.isEdit, Key? key}) : super(key: key);
|
||||
|
||||
final bool isEdit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isEdit ? S.of(context).discardEdits : S.of(context).discardImage,
|
||||
),
|
||||
content: Text(isEdit
|
||||
? S.of(context).discardEditsText
|
||||
: S.of(context).discardImageText),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
primary: Theme.of(context).textTheme.bodyText1!.color,
|
||||
),
|
||||
onPressed: () => CustomNavigator.getInstance().pop(false),
|
||||
child: Text(S.of(context).cancel.toUpperCase()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => CustomNavigator.getInstance().pop(true),
|
||||
style: TextButton.styleFrom(
|
||||
primary: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text(S.of(context).discard.toUpperCase()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
337
lib/modules/draw_image/component/draw_image/draw_image_bloc.dart
Normal file
337
lib/modules/draw_image/component/draw_image/draw_image_bloc.dart
Normal file
@ -0,0 +1,337 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_painter/flutter_painter.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../../../../data/preferences/user_history.dart';
|
||||
import '../../../../i18n/i18n.dart';
|
||||
import '../../../../utils/custom_navigator.dart';
|
||||
import '../../../../utils/snackbar.dart';
|
||||
import '../../../../widgets/component/component_build_context.dart';
|
||||
import '../../../../widgets/component/component_life_cycle_listener.dart';
|
||||
import '../../../../widgets/picker/color_picker.dart';
|
||||
import '../../../../widgets/picker/draw_size_picker.dart';
|
||||
import '../../../../widgets/picker/text_size_picker.dart';
|
||||
import '../discard_dialog.dart';
|
||||
import 'draw_image_view_model.dart';
|
||||
|
||||
@injectable
|
||||
class DrawImageBloc with ComponentBuildContext, ComponentLifecycleListener {
|
||||
DrawImageBloc(this._userHistory);
|
||||
|
||||
final UserHistory _userHistory;
|
||||
|
||||
final _logger = Logger((DrawImageBloc).toString());
|
||||
|
||||
Stream<DrawImageViewModel>? _viewModel;
|
||||
Stream<DrawImageViewModel>? get viewModel => _viewModel;
|
||||
|
||||
final _controller = PainterController();
|
||||
late FocusNode _textFocusNode;
|
||||
|
||||
final _backgroundImage = BehaviorSubject<ui.Image?>();
|
||||
final _mode = BehaviorSubject<PainterMode>.seeded(PainterMode.draw);
|
||||
final _isSaving = BehaviorSubject<bool>.seeded(false);
|
||||
final _shapesFilled = BehaviorSubject<bool>.seeded(false);
|
||||
|
||||
Stream<DrawImageViewModel> createViewModel(String? imageUrl) {
|
||||
if (_viewModel != null) {
|
||||
return _viewModel!;
|
||||
}
|
||||
|
||||
_textFocusNode = FocusNode();
|
||||
|
||||
if (imageUrl != null) {
|
||||
GetIt.I
|
||||
.get<BaseCacheManager>()
|
||||
.getFileFromCache(imageUrl)
|
||||
.then((f) => _handleImageLoad(f, imageUrl));
|
||||
} else {
|
||||
_controller.background = Colors.white.backgroundDrawable;
|
||||
_backgroundImage.add(null);
|
||||
}
|
||||
|
||||
return _viewModel = Rx.combineLatest9(
|
||||
_mode,
|
||||
_backgroundImage,
|
||||
_userHistory.brushSize,
|
||||
_userHistory.textSize,
|
||||
_userHistory.recentTextColors,
|
||||
_userHistory.recentShapeColors,
|
||||
_userHistory.shapeStrokeSize,
|
||||
_shapesFilled,
|
||||
_isSaving,
|
||||
_createViewModel,
|
||||
);
|
||||
}
|
||||
|
||||
DrawImageViewModel _createViewModel(
|
||||
PainterMode mode,
|
||||
ui.Image? backgroundImage,
|
||||
double brushSize,
|
||||
double textSize,
|
||||
List<Color> textColors,
|
||||
List<Color> shapeColors,
|
||||
double shapeStrokeSize,
|
||||
bool shapesFilled,
|
||||
bool isSaving,
|
||||
) {
|
||||
_controller.settings = _controller.settings.copyWith(
|
||||
freeStyle: FreeStyleSettings(
|
||||
enabled: mode == PainterMode.draw,
|
||||
color: _userHistory.recentBrushColors.getValue().first,
|
||||
strokeWidth: brushSize,
|
||||
),
|
||||
text: TextSettings(
|
||||
focusNode: _textFocusNode,
|
||||
textStyle: TextStyle(
|
||||
color: textColors.first,
|
||||
fontSize: textSize,
|
||||
),
|
||||
),
|
||||
shape: _controller.shapeSettings.copyWith(
|
||||
drawOnce: false,
|
||||
paint: Paint()
|
||||
..color = shapeColors.first
|
||||
..style = shapesFilled ? PaintingStyle.fill : PaintingStyle.stroke
|
||||
..strokeWidth = shapeStrokeSize,
|
||||
),
|
||||
);
|
||||
|
||||
return DrawImageViewModel(
|
||||
mode: mode,
|
||||
controller: _controller,
|
||||
isEdit: backgroundImage != null,
|
||||
isSaving: isSaving,
|
||||
shapesFilled: shapesFilled,
|
||||
backgroundImage: backgroundImage,
|
||||
onDrawTap: _handleDrawTap,
|
||||
onPickBrushColorTap: _handlePickBrushColorTap,
|
||||
onPickBrushSizeTap: () => _handlePickBrushSizeTap(brushSize),
|
||||
onPickTextSizeTap: () => _handlePickTextSizeTap(textSize),
|
||||
onPickTextColorTap: () => _handlePickTextColorTap(textColors),
|
||||
onPickShapeColorTap: () => _handlePickShapeColorTap(shapeColors),
|
||||
onPickShapeSizeTap: () => _handlePickShapeStrokeSizeTap(shapeStrokeSize),
|
||||
onTextTap: _handleTextTap,
|
||||
onLineTap: _handleLineTap,
|
||||
onArrowTap: _handleArrowTap,
|
||||
onRectangleTap: _handleRectangleTap,
|
||||
onCircleTap: _handleCircleTap,
|
||||
onFillShapeTap: () => _handleFillShapeTap(shapesFilled),
|
||||
onUndoTap: _handleUndoTap,
|
||||
onClearTap: _handleClearTap,
|
||||
onEscapePress: _handleEscapePress,
|
||||
saveImage: _saveImage,
|
||||
onClose: _handleClose,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_backgroundImage.close();
|
||||
_mode.close();
|
||||
_isSaving.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleImageLoad(FileInfo? fileInfo, String imageUrl) async {
|
||||
if (fileInfo == null) {
|
||||
_logger.severe(
|
||||
"Opening the image with url $imageUrl for drawing didn't work.",
|
||||
null,
|
||||
StackTrace.current,
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showErrorSnackBar(
|
||||
theme: Theme.of(context),
|
||||
text: S.of(context).errorUnknownText,
|
||||
);
|
||||
|
||||
return CustomNavigator.getInstance().pop();
|
||||
}
|
||||
|
||||
final imageBytes = fileInfo.file.readAsBytesSync();
|
||||
final codec = await ui.instantiateImageCodec(imageBytes);
|
||||
final frameInfo = await codec.getNextFrame();
|
||||
|
||||
_controller.background = ImageBackgroundDrawable(image: frameInfo.image);
|
||||
_backgroundImage.add(frameInfo.image);
|
||||
}
|
||||
|
||||
Future<void> _saveImage(Size canvasSize) async {
|
||||
if (_controller.drawables.isEmpty) {
|
||||
return CustomNavigator.getInstance().pop();
|
||||
}
|
||||
|
||||
_isSaving.add(true);
|
||||
|
||||
final size = _backgroundImage.value != null
|
||||
? Size(
|
||||
_backgroundImage.value!.width.toDouble(),
|
||||
_backgroundImage.value!.height.toDouble(),
|
||||
)
|
||||
: canvasSize;
|
||||
final renderedImage = await _controller.renderImage(size);
|
||||
final pngBytes = await renderedImage.pngBytes;
|
||||
|
||||
_isSaving.add(false);
|
||||
CustomNavigator.getInstance().pop(pngBytes);
|
||||
}
|
||||
|
||||
Future<void> _handleClose() async {
|
||||
if (_controller.drawables.isEmpty) {
|
||||
return CustomNavigator.getInstance().pop();
|
||||
}
|
||||
|
||||
final shouldDiscardImage = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => DiscardDialog(isEdit: _backgroundImage.value != null),
|
||||
);
|
||||
if (shouldDiscardImage != null && shouldDiscardImage) {
|
||||
CustomNavigator.getInstance().pop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePickBrushColorTap() async {
|
||||
final newColor = await showDialog<Color>(
|
||||
context: context,
|
||||
builder: (context) => ColorPicker(
|
||||
initialColor: _userHistory.recentBrushColors.getValue().first,
|
||||
suggestionColors: _userHistory.recentBrushColors.getValue(),
|
||||
),
|
||||
);
|
||||
|
||||
if (newColor == null) {
|
||||
return;
|
||||
}
|
||||
_userHistory.addRecentBrushColor(newColor);
|
||||
_controller.freeStyleSettings = _controller.freeStyleSettings.copyWith(
|
||||
color: newColor,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handlePickTextColorTap(List<Color> colors) async {
|
||||
final newColor = await showDialog<Color>(
|
||||
context: context,
|
||||
builder: (context) => ColorPicker(
|
||||
initialColor: colors.first,
|
||||
suggestionColors: colors,
|
||||
),
|
||||
);
|
||||
|
||||
if (newColor == null) {
|
||||
return;
|
||||
}
|
||||
_userHistory.addRecentTextColor(newColor);
|
||||
}
|
||||
|
||||
Future<void> _handlePickShapeColorTap(List<Color> shapeColors) async {
|
||||
final newColor = await showDialog<Color>(
|
||||
context: context,
|
||||
builder: (context) => ColorPicker(
|
||||
initialColor: _userHistory.recentShapeColors.getValue().first,
|
||||
suggestionColors: _userHistory.recentShapeColors.getValue(),
|
||||
),
|
||||
);
|
||||
|
||||
if (newColor == null) {
|
||||
return;
|
||||
}
|
||||
_userHistory.addRecentShapeColor(newColor);
|
||||
}
|
||||
|
||||
Future<void> _handlePickShapeStrokeSizeTap(double size) async {
|
||||
final newSize = await showDialog<double>(
|
||||
context: context,
|
||||
builder: (context) => DrawSizePicker(
|
||||
initialSize: size,
|
||||
));
|
||||
if (newSize == null) {
|
||||
return;
|
||||
}
|
||||
await _userHistory.shapeStrokeSize.setValue(newSize);
|
||||
}
|
||||
|
||||
Future<void> _handlePickBrushSizeTap(double size) async {
|
||||
final newSize = await showDialog<double>(
|
||||
context: context,
|
||||
builder: (context) => DrawSizePicker(initialSize: size),
|
||||
);
|
||||
if (newSize == null) {
|
||||
return;
|
||||
}
|
||||
await _userHistory.brushSize.setValue(newSize);
|
||||
}
|
||||
|
||||
Future<void> _handlePickTextSizeTap(double size) async {
|
||||
final newSize = await showDialog<double>(
|
||||
context: context,
|
||||
builder: (context) => TextSizePicker(initialSize: size),
|
||||
);
|
||||
if (newSize == null) {
|
||||
return;
|
||||
}
|
||||
await _userHistory.textSize.setValue(newSize);
|
||||
}
|
||||
|
||||
void _handleDrawTap() {
|
||||
_mode.add(PainterMode.draw);
|
||||
_controller.shapeSettings = const ShapeSettings();
|
||||
}
|
||||
|
||||
void _handleTextTap() {
|
||||
_controller.addText();
|
||||
_mode.add(PainterMode.text);
|
||||
_controller.shapeSettings = const ShapeSettings();
|
||||
}
|
||||
|
||||
void _handleLineTap() {
|
||||
_mode.add(PainterMode.line);
|
||||
_controller.shapeSettings = ShapeSettings(
|
||||
factory: LineFactory(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleArrowTap() {
|
||||
_mode.add(PainterMode.arrow);
|
||||
_controller.shapeSettings = ShapeSettings(
|
||||
factory: ArrowFactory(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleRectangleTap() {
|
||||
_mode.add(PainterMode.rectangle);
|
||||
_controller.shapeSettings = ShapeSettings(
|
||||
factory: RectangleFactory(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleCircleTap() {
|
||||
_mode.add(PainterMode.circle);
|
||||
_controller.shapeSettings = ShapeSettings(
|
||||
factory: OvalFactory(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleFillShapeTap(bool currentValue) {
|
||||
_shapesFilled.add(!currentValue);
|
||||
}
|
||||
|
||||
void _handleUndoTap() => _controller.removeLastDrawable();
|
||||
|
||||
void _handleClearTap() => _controller.clearDrawables();
|
||||
|
||||
void _handleEscapePress() {
|
||||
if (_textFocusNode.hasFocus) {
|
||||
_textFocusNode.unfocus();
|
||||
} else {
|
||||
_handleClose();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,422 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_painter/flutter_painter.dart';
|
||||
import 'package:flutter_phosphor_icons/flutter_phosphor_icons.dart';
|
||||
|
||||
import '../../../../i18n/i18n.dart';
|
||||
import '../../../../utils/icon_sized_loading_indicator.dart';
|
||||
import '../../../../utils/themes/custom_theme.dart';
|
||||
import '../../../../utils/tooltip_message.dart';
|
||||
import '../../../../widgets/component/component.dart';
|
||||
import '../../../../widgets/page_callback_shortcuts.dart';
|
||||
import '../../../../widgets/simple_skeleton.dart';
|
||||
import '../toolbar_icon_button.dart';
|
||||
import '../toolbar_toggle_button.dart';
|
||||
import 'draw_image_bloc.dart';
|
||||
import 'draw_image_view_model.dart';
|
||||
|
||||
class DrawImageComponent extends StatelessWidget {
|
||||
const DrawImageComponent(this._imageUrl, {Key? key}) : super(key: key);
|
||||
|
||||
final String? _imageUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Component<DrawImageBloc>(
|
||||
createViewModel: (bloc) => bloc.createViewModel(_imageUrl),
|
||||
builder: (context, bloc) {
|
||||
return StreamBuilder<DrawImageViewModel>(
|
||||
stream: bloc.viewModel,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SimpleSkeleton();
|
||||
}
|
||||
|
||||
return _DrawImageView(snapshot.data!);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DrawImageView extends StatefulWidget {
|
||||
const _DrawImageView(this.viewModel);
|
||||
|
||||
final DrawImageViewModel viewModel;
|
||||
|
||||
@override
|
||||
State<_DrawImageView> createState() => _DrawImageViewState();
|
||||
}
|
||||
|
||||
class _DrawImageViewState extends State<_DrawImageView> {
|
||||
late Size _canvasSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: _handleWillPop,
|
||||
child: PageCallbackShortcuts(
|
||||
bindings: {
|
||||
LogicalKeySet(LogicalKeyboardKey.escape):
|
||||
widget.viewModel.onEscapePress,
|
||||
LogicalKeySet(
|
||||
Platform.isMacOS
|
||||
? LogicalKeyboardKey.meta
|
||||
: LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.enter,
|
||||
): () => widget.viewModel.saveImage(_canvasSize),
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: _buildAppBar(context),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(child: _buildCanvas(context)),
|
||||
_buildToolbar(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _handleWillPop() async {
|
||||
widget.viewModel.onClose();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Text(
|
||||
widget.viewModel.isEdit
|
||||
? S.of(context).editDrawing
|
||||
: S.of(context).addDrawing,
|
||||
),
|
||||
elevation: 4,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: widget.viewModel.onClose,
|
||||
tooltip: closeTooltip(context),
|
||||
),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: widget.viewModel.isSaving
|
||||
? const IconSizedLoadingIndicator()
|
||||
: const Icon(Icons.check_outlined),
|
||||
tooltip: saveTooltip(context),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () => widget.viewModel.saveImage(_canvasSize),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCanvas(BuildContext context) {
|
||||
return InteractiveViewer(
|
||||
minScale: 0.005,
|
||||
maxScale: 4,
|
||||
panEnabled: false,
|
||||
child: Center(
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
// Always set the canvas size, as it changes on some devices after
|
||||
// the first build.
|
||||
_canvasSize = widget.viewModel.backgroundImage != null
|
||||
? Size(
|
||||
widget.viewModel.backgroundImage!.width.toDouble(),
|
||||
widget.viewModel.backgroundImage!.height.toDouble(),
|
||||
)
|
||||
: constraints.biggest;
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: _canvasSize.width / _canvasSize.height,
|
||||
child: FlutterPainter(controller: widget.viewModel.controller),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToolbar(BuildContext context) {
|
||||
return Material(
|
||||
elevation: 4,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: [
|
||||
const SizedBox(width: 8),
|
||||
_buildDrawButton(context),
|
||||
_buildTextButton(context),
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS)
|
||||
const VerticalDivider(indent: 8, endIndent: 8),
|
||||
..._buildShapeButtons(context),
|
||||
const VerticalDivider(indent: 8, endIndent: 8),
|
||||
if (widget.viewModel.mode == PainterMode.draw)
|
||||
_buildBrushColorButton(context)
|
||||
else if (widget.viewModel.mode == PainterMode.text)
|
||||
_buildTextColorButton(context)
|
||||
else
|
||||
_buildShapeColorButton(context),
|
||||
if (widget.viewModel.mode == PainterMode.draw)
|
||||
_buildPickBrushSizeButton(context)
|
||||
else if (widget.viewModel.mode == PainterMode.text)
|
||||
_buildPickTextSizeButton(context)
|
||||
else if (widget.viewModel.shapesFilled &&
|
||||
(widget.viewModel.mode == PainterMode.rectangle ||
|
||||
widget.viewModel.mode == PainterMode.circle))
|
||||
const SizedBox(width: 42)
|
||||
else
|
||||
_buildPickShapeSizeButton(context),
|
||||
if (widget.viewModel.mode == PainterMode.rectangle)
|
||||
_buildFillRectangleButton(context)
|
||||
else if (widget.viewModel.mode == PainterMode.circle)
|
||||
_buildFillCircleButton(context)
|
||||
else
|
||||
const SizedBox(width: 42),
|
||||
const VerticalDivider(indent: 8, endIndent: 8),
|
||||
_buildUndoButton(context),
|
||||
_buildClearButton(context),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDrawButton(BuildContext context) {
|
||||
return ToolbarToggleButton(
|
||||
icon: Icons.brush_outlined,
|
||||
isToggled: widget.viewModel.mode == PainterMode.draw,
|
||||
onTap: widget.viewModel.onDrawTap,
|
||||
tooltip: S.of(context).draw,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextButton(BuildContext context) {
|
||||
return ToolbarToggleButton(
|
||||
icon: Icons.title_outlined,
|
||||
isToggled: widget.viewModel.mode == PainterMode.text,
|
||||
onTap: widget.viewModel.onTextTap,
|
||||
tooltip: S.of(context).text,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildShapeButtons(BuildContext context) {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
return <Widget>[
|
||||
// We use
|
||||
// - SizedBox to limit the background size of the PopMenuButton
|
||||
// - Material is necessary for InkWell and colors the background color
|
||||
// - InkWell to limit the splash animation to the size of SizedBox
|
||||
SizedBox(
|
||||
width: 42,
|
||||
height: 42,
|
||||
child: Material(
|
||||
color: _getBackgroundColor(),
|
||||
child: InkWell(
|
||||
child: PopupMenuButton(
|
||||
icon: _getPopMenuIcon(),
|
||||
tooltip: S.of(context).selectShape,
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
onTap: widget.viewModel.onLineTap,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(PhosphorIcons.line_segment),
|
||||
const SizedBox(width: 10),
|
||||
Text(S.of(context).line)
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: widget.viewModel.onArrowTap,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(PhosphorIcons.arrow_up_right),
|
||||
const SizedBox(width: 10),
|
||||
Text(S.of(context).arrow)
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: widget.viewModel.onRectangleTap,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(PhosphorIcons.rectangle),
|
||||
const SizedBox(width: 10),
|
||||
Text(S.of(context).rectangle)
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: widget.viewModel.onCircleTap,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(PhosphorIcons.circle),
|
||||
const SizedBox(width: 10),
|
||||
Text(S.of(context).circle)
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return <Widget>[
|
||||
ToolbarToggleButton(
|
||||
icon: PhosphorIcons.line_segment,
|
||||
isToggled: widget.viewModel.mode == PainterMode.line,
|
||||
onTap: widget.viewModel.onLineTap,
|
||||
tooltip: S.of(context).line,
|
||||
),
|
||||
ToolbarToggleButton(
|
||||
icon: PhosphorIcons.arrow_up_right,
|
||||
isToggled: widget.viewModel.mode == PainterMode.arrow,
|
||||
onTap: widget.viewModel.onArrowTap,
|
||||
tooltip: S.of(context).arrow,
|
||||
),
|
||||
ToolbarToggleButton(
|
||||
icon: PhosphorIcons.rectangle,
|
||||
isToggled: widget.viewModel.mode == PainterMode.rectangle,
|
||||
onTap: widget.viewModel.onRectangleTap,
|
||||
tooltip: S.of(context).rectangle,
|
||||
),
|
||||
ToolbarToggleButton(
|
||||
icon: PhosphorIcons.circle,
|
||||
isToggled: widget.viewModel.mode == PainterMode.circle,
|
||||
onTap: widget.viewModel.onCircleTap,
|
||||
tooltip: S.of(context).circle,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Color _getBackgroundColor() {
|
||||
final _activeBackgroundColor =
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xFFF4F8FE)
|
||||
: const Color(0xFF343D52);
|
||||
final _inactiveBackgroundColor = Theme.of(context).custom.elevation4DPColor;
|
||||
|
||||
if (widget.viewModel.mode == PainterMode.line ||
|
||||
widget.viewModel.mode == PainterMode.arrow ||
|
||||
widget.viewModel.mode == PainterMode.rectangle ||
|
||||
widget.viewModel.mode == PainterMode.circle) {
|
||||
return _activeBackgroundColor;
|
||||
} else {
|
||||
return _inactiveBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
Icon _getPopMenuIcon() {
|
||||
final color = Theme.of(context).brightness == Brightness.light
|
||||
? Colors.blue.shade700
|
||||
: Colors.blue.shade100;
|
||||
switch (widget.viewModel.mode) {
|
||||
case PainterMode.line:
|
||||
return Icon(PhosphorIcons.line_segment, color: color);
|
||||
case PainterMode.arrow:
|
||||
return Icon(PhosphorIcons.arrow_up_right, color: color);
|
||||
case PainterMode.rectangle:
|
||||
return Icon(PhosphorIcons.rectangle, color: color);
|
||||
case PainterMode.circle:
|
||||
return Icon(PhosphorIcons.circle, color: color);
|
||||
default:
|
||||
return Icon(PhosphorIcons.line_segment, color: color);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBrushColorButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: Icons.palette_outlined,
|
||||
onTap: widget.viewModel.onPickBrushColorTap,
|
||||
tooltip: S.of(context).pickBrushColor,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickBrushSizeButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: Icons.line_weight,
|
||||
onTap: widget.viewModel.onPickBrushSizeTap,
|
||||
tooltip: S.of(context).pickBrushWidth,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextColorButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: Icons.palette_outlined,
|
||||
onTap: widget.viewModel.onPickTextColorTap,
|
||||
tooltip: S.of(context).pickTextColor,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickTextSizeButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: Icons.format_size_outlined,
|
||||
onTap: widget.viewModel.onPickTextSizeTap,
|
||||
tooltip: S.of(context).pickTextSize,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShapeColorButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: Icons.palette_outlined,
|
||||
onTap: widget.viewModel.onPickShapeColorTap,
|
||||
tooltip: S.of(context).pickShapeColor,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickShapeSizeButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: Icons.line_weight,
|
||||
onTap: widget.viewModel.onPickShapeSizeTap,
|
||||
tooltip: S.of(context).selectShapeWidth,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFillRectangleButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: widget.viewModel.shapesFilled
|
||||
? PhosphorIcons.rectangle_fill
|
||||
: PhosphorIcons.rectangle_thin,
|
||||
onTap: widget.viewModel.onFillShapeTap,
|
||||
tooltip: S.of(context).fillRectangle,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFillCircleButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: widget.viewModel.shapesFilled
|
||||
? PhosphorIcons.circle_fill
|
||||
: PhosphorIcons.circle_thin,
|
||||
onTap: widget.viewModel.onFillShapeTap,
|
||||
tooltip: S.of(context).fillCircle,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUndoButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: Icons.undo_outlined,
|
||||
onTap: widget.viewModel.onUndoTap,
|
||||
tooltip: S.of(context).undoLastAction,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClearButton(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
icon: Icons.clear_outlined,
|
||||
onTap: widget.viewModel.onClearTap,
|
||||
tooltip: S.of(context).clearCanvas,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_painter/flutter_painter.dart';
|
||||
|
||||
class DrawImageViewModel {
|
||||
const DrawImageViewModel({
|
||||
required this.mode,
|
||||
required this.controller,
|
||||
required this.isEdit,
|
||||
required this.backgroundImage,
|
||||
required this.shapesFilled,
|
||||
required this.isSaving,
|
||||
required this.onDrawTap,
|
||||
required this.onPickBrushColorTap,
|
||||
required this.onPickBrushSizeTap,
|
||||
required this.onPickTextSizeTap,
|
||||
required this.onPickTextColorTap,
|
||||
required this.onPickShapeColorTap,
|
||||
required this.onPickShapeSizeTap,
|
||||
required this.onTextTap,
|
||||
required this.onLineTap,
|
||||
required this.onArrowTap,
|
||||
required this.onRectangleTap,
|
||||
required this.onCircleTap,
|
||||
required this.onFillShapeTap,
|
||||
required this.onClearTap,
|
||||
required this.onEscapePress,
|
||||
required this.onUndoTap,
|
||||
required this.onClose,
|
||||
required this.saveImage,
|
||||
});
|
||||
|
||||
final PainterMode mode;
|
||||
final PainterController controller;
|
||||
final bool isEdit;
|
||||
final Image? backgroundImage;
|
||||
final bool shapesFilled;
|
||||
final bool isSaving;
|
||||
|
||||
final VoidCallback onDrawTap;
|
||||
final VoidCallback onPickBrushColorTap;
|
||||
final VoidCallback onPickBrushSizeTap;
|
||||
final VoidCallback onPickTextSizeTap;
|
||||
final VoidCallback onPickTextColorTap;
|
||||
final VoidCallback onPickShapeColorTap;
|
||||
final VoidCallback onPickShapeSizeTap;
|
||||
|
||||
final VoidCallback onTextTap;
|
||||
final VoidCallback onLineTap;
|
||||
final VoidCallback onArrowTap;
|
||||
final VoidCallback onRectangleTap;
|
||||
final VoidCallback onCircleTap;
|
||||
|
||||
final VoidCallback onFillShapeTap;
|
||||
|
||||
final VoidCallback onClearTap;
|
||||
final VoidCallback? onUndoTap;
|
||||
final VoidCallback onEscapePress;
|
||||
final VoidCallback onClose;
|
||||
final ValueSetter<Size> saveImage;
|
||||
}
|
||||
|
||||
enum PainterMode {
|
||||
draw,
|
||||
text,
|
||||
line,
|
||||
arrow,
|
||||
rectangle,
|
||||
circle,
|
||||
}
|
43
lib/modules/draw_image/component/toolbar_icon_button.dart
Normal file
43
lib/modules/draw_image/component/toolbar_icon_button.dart
Normal file
@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../utils/themes/custom_theme.dart';
|
||||
|
||||
class ToolbarIconButton extends StatelessWidget {
|
||||
const ToolbarIconButton({
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
required this.tooltip,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final VoidCallback? onTap;
|
||||
final String tooltip;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final backgroundColor = theme.custom.elevation4DPColor;
|
||||
final iconColor =
|
||||
onTap == null ? theme.disabledColor : theme.iconTheme.color;
|
||||
|
||||
return SizedBox(
|
||||
width: 42,
|
||||
height: 42,
|
||||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: RawMaterialButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
|
||||
fillColor: backgroundColor,
|
||||
hoverElevation: 0,
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
onPressed: onTap,
|
||||
child: Icon(icon, color: iconColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
58
lib/modules/draw_image/component/toolbar_toggle_button.dart
Normal file
58
lib/modules/draw_image/component/toolbar_toggle_button.dart
Normal file
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../utils/themes/custom_theme.dart';
|
||||
|
||||
class ToolbarToggleButton extends StatelessWidget {
|
||||
const ToolbarToggleButton({
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
required this.isToggled,
|
||||
required this.tooltip,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final VoidCallback? onTap;
|
||||
final bool isToggled;
|
||||
final String tooltip;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final activeIconColor = theme.colorScheme.primary;
|
||||
final inactiveIconColor = theme.iconTheme.color;
|
||||
final disabledIconColor = theme.disabledColor;
|
||||
final activeIconBackgroundColor = theme.brightness == Brightness.light
|
||||
? const Color(0xFFF4F8FE)
|
||||
: const Color(0xFF343D52);
|
||||
final inactiveIconBackgroundColor = theme.custom.elevation4DPColor;
|
||||
|
||||
final isEnabled = onTap != null;
|
||||
final iconColor = isEnabled
|
||||
? isToggled
|
||||
? activeIconColor
|
||||
: inactiveIconColor
|
||||
: disabledIconColor;
|
||||
final _backgroundColor =
|
||||
isToggled ? activeIconBackgroundColor : inactiveIconBackgroundColor;
|
||||
|
||||
return SizedBox(
|
||||
width: 42,
|
||||
height: 42,
|
||||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: RawMaterialButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
|
||||
fillColor: _backgroundColor,
|
||||
hoverElevation: 0,
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
onPressed: onTap,
|
||||
child: Icon(icon, color: iconColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user