topics intermediate
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/topics/domain/providers.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/features/topics/presentation/widgets/topic_dialog.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class _TopicTile extends HookWidget {
|
||||
final TopicData topic;
|
||||
final bool isSelected;
|
||||
|
||||
final void Function(TopicResult edited) onEdit;
|
||||
final void Function() onDelete;
|
||||
final void Function() onTap;
|
||||
|
||||
const _TopicTile(
|
||||
this.topic, {
|
||||
required this.isSelected,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final menuController = useMemoized(() => MenuController());
|
||||
|
||||
return ListTile(
|
||||
selected: isSelected,
|
||||
leading: CircleAvatar(backgroundColor: topic.color),
|
||||
title: Text(topic.name ?? 'New Topic'),
|
||||
trailing: MenuAnchor(
|
||||
controller: menuController,
|
||||
builder: (context, controller, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 8.0),
|
||||
child: Icon(Icons.more_vert),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final result = await showDialog<TopicResult?>(
|
||||
context: context,
|
||||
builder: (context) => TopicDialog.edit(
|
||||
name: topic.name,
|
||||
initialColor: topic.color,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
onEdit(result);
|
||||
}
|
||||
},
|
||||
leadingIcon: const Icon(Icons.edit),
|
||||
child: const Text('Edit'),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final result = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Delete Topic'),
|
||||
content: const Text(
|
||||
'Are you sure you want to delete this topic and all attached tabs?',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
onDelete();
|
||||
}
|
||||
},
|
||||
leadingIcon: const Icon(Icons.delete),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TopicListScreen extends HookConsumerWidget {
|
||||
const TopicListScreen();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Topics'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final initialColor =
|
||||
await ref.read(unusedRandomTopicColorProvider.future);
|
||||
|
||||
if (context.mounted) {
|
||||
final result = await showDialog<TopicResult?>(
|
||||
context: context,
|
||||
builder: (context) => TopicDialog.create(
|
||||
initialColor: initialColor,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.addTopic(name: result.name, color: result.color);
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final topicsAsync = ref.watch(topicListProvider);
|
||||
final selectedTopic = ref.watch(selectedTopicProvider);
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: topicsAsync.isLoading,
|
||||
child: topicsAsync.when(
|
||||
data: (topics) => ListView.builder(
|
||||
itemCount: topics.length,
|
||||
itemBuilder: (context, index) {
|
||||
final topic = topics[index];
|
||||
return _TopicTile(
|
||||
topic,
|
||||
key: ValueKey(topic.id),
|
||||
isSelected: topic.id == selectedTopic,
|
||||
onEdit: (edited) async {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.replaceTopic(
|
||||
id: topic.id,
|
||||
name: edited.name,
|
||||
color: edited.color,
|
||||
);
|
||||
},
|
||||
onDelete: () async {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.deleteTopic(topic.id);
|
||||
},
|
||||
onTap: () {
|
||||
ref
|
||||
.read(selectedTopicProvider.notifier)
|
||||
.toggleTopic(topic.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
error: (error, stackTrace) => SizedBox.shrink(),
|
||||
loading: () => ListView.builder(
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) => _TopicTile(
|
||||
const TopicData(id: 'null', color: Colors.transparent),
|
||||
isSelected: false,
|
||||
onEdit: (_) {},
|
||||
onDelete: () {},
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// The Color Picker which contains Material Design Color Palette.
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lensai/features/topics/utils/color_palette.dart';
|
||||
|
||||
class MaterialPicker extends StatefulWidget {
|
||||
const MaterialPicker({
|
||||
super.key,
|
||||
required this.pickerColor,
|
||||
required this.onColorChanged,
|
||||
this.onPrimaryChanged,
|
||||
this.enableLabel = false,
|
||||
this.portraitOnly = false,
|
||||
});
|
||||
|
||||
final Color pickerColor;
|
||||
final ValueChanged<Color> onColorChanged;
|
||||
final ValueChanged<Color>? onPrimaryChanged;
|
||||
final bool enableLabel;
|
||||
final bool portraitOnly;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MaterialPickerState();
|
||||
}
|
||||
|
||||
class _MaterialPickerState extends State<MaterialPicker> {
|
||||
List<Color> _currentColorType = [Colors.red, Colors.redAccent];
|
||||
Color _currentShading = Colors.transparent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
for (final colors in colorTypes) {
|
||||
shadingTypes(colors).forEach((Map<Color, String> color) {
|
||||
if (widget.pickerColor.value == color.keys.first.value) {
|
||||
return setState(() {
|
||||
_currentColorType = colors;
|
||||
_currentShading = color.keys.first;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPortrait =
|
||||
MediaQuery.of(context).orientation == Orientation.portrait ||
|
||||
widget.portraitOnly;
|
||||
|
||||
Widget colorList() {
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Container(
|
||||
margin: isPortrait
|
||||
? const EdgeInsets.only(right: 10)
|
||||
: const EdgeInsets.only(bottom: 10),
|
||||
width: isPortrait ? 60 : null,
|
||||
height: isPortrait ? null : 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
border: isPortrait
|
||||
? Border(
|
||||
right: BorderSide(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
),
|
||||
)
|
||||
: Border(
|
||||
top: BorderSide(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(dragDevices: PointerDeviceKind.values.toSet()),
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
scrollDirection: isPortrait ? Axis.vertical : Axis.horizontal,
|
||||
children: [
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 7))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 7)),
|
||||
...colorTypes.map((List<Color> colors) {
|
||||
final Color colorType = colors[0];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.onPrimaryChanged != null) {
|
||||
widget.onPrimaryChanged!.call(colorType);
|
||||
}
|
||||
setState(() => _currentColorType = colors);
|
||||
},
|
||||
child: Container(
|
||||
color: const Color(0x00000000),
|
||||
padding: isPortrait
|
||||
? const EdgeInsets.fromLTRB(0, 7, 0, 7)
|
||||
: const EdgeInsets.fromLTRB(7, 0, 7, 0),
|
||||
child: Align(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: 25,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: colorType,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: _currentColorType == colors
|
||||
? [
|
||||
if (colorType ==
|
||||
Theme.of(context).cardColor)
|
||||
BoxShadow(
|
||||
color:
|
||||
(Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
)
|
||||
else
|
||||
BoxShadow(
|
||||
color: colorType,
|
||||
blurRadius: 10,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
border: colorType == Theme.of(context).cardColor
|
||||
? Border.all(
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 5))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 5)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget shadingList() {
|
||||
return ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(dragDevices: PointerDeviceKind.values.toSet()),
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
scrollDirection: isPortrait ? Axis.vertical : Axis.horizontal,
|
||||
children: [
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 15))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 15)),
|
||||
...shadingTypes(_currentColorType)
|
||||
.map((Map<Color, String> colors) {
|
||||
final Color color = colors.keys.first;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => _currentShading = color);
|
||||
widget.onColorChanged(color);
|
||||
},
|
||||
child: Container(
|
||||
color: const Color(0x00000000),
|
||||
margin: isPortrait
|
||||
? const EdgeInsets.only(right: 10)
|
||||
: const EdgeInsets.only(bottom: 10),
|
||||
padding: isPortrait
|
||||
? const EdgeInsets.fromLTRB(0, 7, 0, 7)
|
||||
: const EdgeInsets.fromLTRB(7, 0, 7, 0),
|
||||
child: Align(
|
||||
child: AnimatedContainer(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
width: isPortrait
|
||||
? (_currentShading == color ? 250 : 230)
|
||||
: (_currentShading == color ? 50 : 30),
|
||||
height: isPortrait ? 50 : 220,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
boxShadow: _currentShading == color
|
||||
? [
|
||||
if ((color == Colors.white) ||
|
||||
(color == Colors.black))
|
||||
BoxShadow(
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
)
|
||||
else
|
||||
BoxShadow(
|
||||
color: color,
|
||||
blurRadius: 10,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
border: (color == Colors.white) ||
|
||||
(color == Colors.black)
|
||||
? Border.all(
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: widget.enableLabel
|
||||
? isPortrait
|
||||
? Row(
|
||||
children: [
|
||||
Text(
|
||||
' ${colors.values.first}',
|
||||
style: TextStyle(
|
||||
color: useWhiteForeground(color)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'#${color.toString().replaceFirst('Color(0xff', '').replaceFirst(')', '').toUpperCase()} ',
|
||||
style: TextStyle(
|
||||
color: useWhiteForeground(color)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: AnimatedOpacity(
|
||||
duration:
|
||||
const Duration(milliseconds: 300),
|
||||
opacity: _currentShading == color ? 1 : 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
alignment: Alignment.topCenter,
|
||||
child: Text(
|
||||
colors.values.first,
|
||||
style: TextStyle(
|
||||
color: useWhiteForeground(color)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 15))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 15)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isPortrait) {
|
||||
return SizedBox(
|
||||
width: 350,
|
||||
height: 500,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
colorList(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: shadingList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: 500,
|
||||
height: 300,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
colorList(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: shadingList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
import 'package:lensai/features/topics/domain/providers.dart';
|
||||
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||
|
||||
class TopicChips extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final topicsAsync = ref.watch(topicListProvider);
|
||||
final selectedTopic = ref
|
||||
.watch(selectedTopicDataProvider.select((value) => value.valueOrNull));
|
||||
|
||||
return topicsAsync.when(
|
||||
data: (availableTopics) => SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
if (selectedTopic != null || availableTopics.isNotEmpty)
|
||||
Expanded(
|
||||
child: SelectableChips(
|
||||
deleteIcon: false,
|
||||
itemId: (topic) => topic.id,
|
||||
itemAvatar: (topic) => Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
decoration: BoxDecoration(
|
||||
color: topic.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
itemLabel: (topic) => Text(topic.name ?? 'New Topic'),
|
||||
availableItems: availableTopics,
|
||||
selectedItem: selectedTopic,
|
||||
onSelected: (topic) {
|
||||
ref.read(selectedTopicProvider.notifier).setTopic(topic.id);
|
||||
},
|
||||
onDeleted: (topic) async {
|
||||
ref.read(selectedTopicProvider.notifier).clearTopic();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Press '>' to manage Topics.",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context.push(TopicListRoute().location);
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
error: (error, stackTrace) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox(
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lensai/features/topics/presentation/widgets/material_color_picker.dart';
|
||||
|
||||
typedef TopicResult = ({String? name, Color color});
|
||||
|
||||
enum _DialogMode { create, edit }
|
||||
|
||||
class TopicDialog extends HookWidget {
|
||||
final _DialogMode _mode;
|
||||
|
||||
final String? initialName;
|
||||
final Color initialColor;
|
||||
|
||||
const TopicDialog._({
|
||||
required _DialogMode mode,
|
||||
required this.initialColor,
|
||||
this.initialName,
|
||||
}) : _mode = mode;
|
||||
|
||||
factory TopicDialog.create({required Color initialColor}) {
|
||||
return TopicDialog._(
|
||||
mode: _DialogMode.create,
|
||||
initialColor: initialColor,
|
||||
);
|
||||
}
|
||||
|
||||
factory TopicDialog.edit({
|
||||
required String? name,
|
||||
required Color initialColor,
|
||||
}) {
|
||||
return TopicDialog._(
|
||||
mode: _DialogMode.edit,
|
||||
initialColor: initialColor,
|
||||
initialName: name,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedColor = useState<Color>(initialColor);
|
||||
final textController = useTextEditingController(text: initialName);
|
||||
|
||||
return SimpleDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0),
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 20.0,
|
||||
right: 20.0,
|
||||
bottom: 24.0,
|
||||
),
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20.0,
|
||||
vertical: 24.0,
|
||||
),
|
||||
title: Text(
|
||||
switch (_mode) {
|
||||
_DialogMode.create => 'New Topic',
|
||||
_DialogMode.edit => 'Edit Topic',
|
||||
},
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: 24,
|
||||
width: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: selectedColor.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
label: const Text('Name'),
|
||||
),
|
||||
controller: textController,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
MaterialPicker(
|
||||
pickerColor: selectedColor.value,
|
||||
onColorChanged: (value) {
|
||||
selectedColor.value = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
OverflowBar(
|
||||
alignment: MainAxisAlignment.end,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop<TopicResult?>(context);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final name = textController.text.trim();
|
||||
Navigator.pop<TopicResult?>(
|
||||
context,
|
||||
(
|
||||
name: name.isNotEmpty ? name : null,
|
||||
color: selectedColor.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
switch (_mode) {
|
||||
_DialogMode.create => 'Add',
|
||||
_DialogMode.edit => 'Edit',
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user