topics intermediate

This commit is contained in:
Fabian Freund
2024-08-21 13:36:58 +02:00
parent e75052aa99
commit 7fedbe260c
35 changed files with 2511 additions and 198 deletions
@@ -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',
},
),
),
],
),
],
);
}
}