Initial commit

Add Space version 2.0.1
This commit is contained in:
friebetill
2022-03-28 14:56:00 +02:00
commit 80f218097d
1258 changed files with 59763 additions and 0 deletions

View 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()),
),
],
);
}
}

View 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();
}
}
}

View File

@ -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,
);
}
}

View File

@ -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,
}

View 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),
),
),
);
}
}

View 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),
),
),
);
}
}

View File

@ -0,0 +1,24 @@
import 'dart:ui';
import 'package:flutter/material.dart' hide Image;
import 'component/draw_image/draw_image_component.dart';
/// The screen with which the user can paint images.
///
/// Returns the painted [Image] when this page is closed.
/// If the user aborted, null is returned.
class DrawImagePage extends StatelessWidget {
const DrawImagePage({Key? key, this.imageUrl}) : super(key: key);
/// The name of the route to the [DrawImagePage].
static const String routeName = '/card/draw-image';
/// The optional url of an image.
///
/// Opens an empty canvas, if no URL is given.
final String? imageUrl;
@override
Widget build(BuildContext context) => DrawImageComponent(imageUrl);
}