- feat(sync): SyncEngine 接入 EditorPage, 保存时 enqueue + 网络恢复自动 trySync - fix(editor): authorId 从 AuthBloc 获取, 替代硬编码 'local' - fix(bloc): class_bloc/calendar/profile/parent catch(_).全部改为 debugPrint - feat(editor): 编辑器工具栏拆分 (brush_panel/tag_panel/text_format_bar/dot_grid_painter) - feat(editor): EditorBloc 扩展 + EditorPage 增强 - feat(search): SearchBloc 扩展搜索功能 - feat(home): HomeBloc/HomePage 增强 - feat(auth): LoginPage 增强 - feat(templates): TemplateGalleryPage 重构 - fix(web): 管理端班级/日记页面修复 - fix(server): comment_service + theme_handler 修复 - docs: 添加全链路审计报告和验证截图
216 lines
6.5 KiB
Dart
216 lines
6.5 KiB
Dart
// 画笔面板 -- 底部抽屉
|
|
// 提供画笔类型/粗细/颜色/透明度设置
|
|
// 遵循 StickerPickerSheet 底部面板模式
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../bloc/editor_bloc.dart';
|
|
import 'stroke_model.dart';
|
|
|
|
/// 画笔面板 -- 底部抽屉
|
|
class BrushPanel extends StatelessWidget {
|
|
final BrushType activeBrushType;
|
|
final String activeColor;
|
|
final double activeWidth;
|
|
final double activeOpacity;
|
|
final void Function(BrushType type) onBrushTypeChanged;
|
|
final void Function(String color) onColorChanged;
|
|
final void Function(double width) onWidthChanged;
|
|
final void Function(double opacity) onOpacityChanged;
|
|
|
|
const BrushPanel({
|
|
super.key,
|
|
required this.activeBrushType,
|
|
required this.activeColor,
|
|
required this.activeWidth,
|
|
required this.activeOpacity,
|
|
required this.onBrushTypeChanged,
|
|
required this.onColorChanged,
|
|
required this.onWidthChanged,
|
|
required this.onOpacityChanged,
|
|
});
|
|
|
|
static const _brushTypes = [
|
|
(BrushType.pen, '钢笔', Icons.gesture_rounded),
|
|
(BrushType.pencil, '铅笔', Icons.edit_rounded),
|
|
(BrushType.marker, '马克笔', Icons.brush_rounded),
|
|
(BrushType.eraser, '橡皮', Icons.auto_fix_high_rounded),
|
|
];
|
|
|
|
static const _colors = [
|
|
'#2D2420', '#E07A5F', '#81B29A', '#F2CC8F',
|
|
'#D4A5A5', '#42A5F5', '#9C27B0', '#FFFFFF',
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: 280,
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(22)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// 拖拽指示条
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
|
child: Container(
|
|
width: 36,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
// 画笔类型行
|
|
_buildBrushTypeRow(context),
|
|
// 粗细滑块
|
|
_buildSizeSlider(context),
|
|
// 颜色行
|
|
_buildColorRow(context),
|
|
// 透明度滑块(仅马克笔)
|
|
if (activeBrushType == BrushType.marker)
|
|
_buildOpacitySlider(context),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBrushTypeRow(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: _brushTypes.map((bt) {
|
|
final isActive = activeBrushType == bt.$1;
|
|
return GestureDetector(
|
|
onTap: () => onBrushTypeChanged(bt.$1),
|
|
child: Container(
|
|
width: 64,
|
|
height: 52,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: isActive
|
|
? Border.all(color: AppColors.accent, width: 2)
|
|
: Border.all(color: Colors.transparent),
|
|
color: isActive
|
|
? AppColors.accent.withValues(alpha: 0.1)
|
|
: null,
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
bt.$3,
|
|
size: 20,
|
|
color: isActive ? AppColors.accent : Colors.grey[600],
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
bt.$2,
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: isActive ? AppColors.accent : Colors.grey[600],
|
|
fontWeight:
|
|
isActive ? FontWeight.w600 : FontWeight.normal,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSizeSlider(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
Text('粗细',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
|
Expanded(
|
|
child: Slider(
|
|
value: activeWidth,
|
|
min: 1,
|
|
max: 20,
|
|
divisions: 19,
|
|
activeColor: AppColors.accent,
|
|
label: activeWidth.round().toString(),
|
|
onChanged: onWidthChanged,
|
|
),
|
|
),
|
|
Text(
|
|
activeWidth.round().toString(),
|
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildColorRow(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: _colors.map((c) {
|
|
final isActive = activeColor == c;
|
|
final color = _parseHexColor(c);
|
|
return GestureDetector(
|
|
onTap: () => onColorChanged(c),
|
|
child: Container(
|
|
width: 24,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: color,
|
|
border: isActive
|
|
? Border.all(color: AppColors.accent, width: 2)
|
|
: (c == '#FFFFFF'
|
|
? Border.all(color: Colors.grey[300]!)
|
|
: null),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildOpacitySlider(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
Text('透明度',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
|
Expanded(
|
|
child: Slider(
|
|
value: activeOpacity,
|
|
min: 0.1,
|
|
max: 1.0,
|
|
activeColor: AppColors.accent,
|
|
onChanged: onOpacityChanged,
|
|
),
|
|
),
|
|
Text(
|
|
'${(activeOpacity * 100).round()}%',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _parseHexColor(String hex) {
|
|
final code = hex.replaceFirst('#', '');
|
|
return Color(int.parse('FF$code', radix: 16));
|
|
}
|
|
}
|