shlink-manager/lib/views/tag_selector_view.dart

242 lines
8.0 KiB
Dart

import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:shlink_app/API/Classes/Tag/tag_with_stats.dart';
import 'package:shlink_app/util/build_api_error_snackbar.dart';
import 'package:shlink_app/util/string_to_color.dart';
import '../globals.dart' as globals;
class TagSelectorView extends StatefulWidget {
const TagSelectorView({super.key, this.alreadySelectedTags = const []});
final List<String> alreadySelectedTags;
@override
State<TagSelectorView> createState() => _TagSelectorViewState();
}
class _TagSelectorViewState extends State<TagSelectorView> {
final FocusNode searchTagFocusNode = FocusNode();
final searchTagController = TextEditingController();
List<TagWithStats> availableTags = [];
List<TagWithStats> selectedTags = [];
List<TagWithStats> filteredTags = [];
bool tagsLoaded = false;
@override
void initState() {
super.initState();
selectedTags = [];
searchTagController.text = "";
filteredTags = [];
searchTagFocusNode.requestFocus();
WidgetsBinding.instance.addPostFrameCallback((_) => loadTags());
}
@override
void dispose() {
searchTagFocusNode.dispose();
searchTagController.dispose();
super.dispose();
}
Future<void> loadTags() async {
final response =
await globals.serverManager.getTags();
response.fold((l) {
List<TagWithStats> mappedAlreadySelectedTags =
widget.alreadySelectedTags.map((e) {
return l.firstWhere((t) => t.tag == e);
}).toList();
setState(() {
availableTags = (l + [... mappedAlreadySelectedTags]).toSet().toList();
selectedTags = [...mappedAlreadySelectedTags];
filteredTags = availableTags;
tagsLoaded = true;
});
_sortLists();
return true;
}, (r) {
ScaffoldMessenger.of(context).showSnackBar(
buildApiErrorSnackbar(r, context)
);
return false;
});
}
void _sortLists() {
setState(() {
availableTags.sort();
filteredTags.sort();
});
}
void _searchTextChanged(String text) {
if (text == "") {
setState(() {
filteredTags = availableTags;
});
} else {
setState(() {
filteredTags = availableTags.where((t) => t.tag.toLowerCase()
.contains(text.toLowerCase())).toList();
});
}
_sortLists();
}
void _addNewTag(String tag) {
if (tag != "" && !availableTags.contains(tag)) {
TagWithStats _tagWithStats = availableTags.firstWhere((e) => e.tag == tag);
setState(() {
availableTags.add(_tagWithStats);
selectedTags.add(_tagWithStats);
_searchTextChanged(tag);
});
_sortLists();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: searchTagController,
focusNode: searchTagFocusNode,
onChanged: _searchTextChanged,
decoration: const InputDecoration(
hintText: "Start typing...",
border: InputBorder.none,
icon: Icon(Icons.label_outline),
),
),
actions: [
IconButton(
onPressed: () {
Navigator.pop(context, selectedTags.map((t) => t.tag).toList());
},
icon: const Icon(Icons.check),
)
],
),
body: CustomScrollView(
slivers: [
if (!tagsLoaded)
const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(strokeWidth: 3),
),
),
)
else if (tagsLoaded && availableTags.isEmpty)
SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.only(top: 50),
child: Column(
children: [
const Text(
"No Tags",
style: TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'Start typing to add new tags!',
style: TextStyle(
fontSize: 16, color: Theme.of(context).colorScheme.onSecondary),
),
)
],
))))
else
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
bool _isSelected = selectedTags.contains(filteredTags[index]);
TagWithStats _tag = filteredTags[index];
return GestureDetector(
onTap: () {
if (_isSelected) {
setState(() {
selectedTags.remove(_tag);
});
} else {
setState(() {
selectedTags.add(_tag);
});
}
},
child: Container(
padding: const EdgeInsets.only(left: 16, right: 16,
top: 16, bottom: 16),
decoration: BoxDecoration(
color: _isSelected ? Theme.of(context).colorScheme.primary : null,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Wrap(
spacing: 10,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: stringToColor(_tag.tag)
.harmonizeWith(Theme.of(context).colorScheme.primary),
borderRadius: BorderRadius.circular(15)
),
),
Text(_tag.tag)
],
),
Text("${_tag.shortUrlsCount} short URL"
"${_tag.shortUrlsCount == 1 ? "" : "s"}",
style: TextStyle(
color: Theme.of(context).colorScheme.onTertiary,
fontSize: 12
),)
],
)
)
);
}, childCount: filteredTags.length
),
),
if (searchTagController.text != "" &&
!availableTags.contains(searchTagController.text))
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8,
left: 16, right: 16),
child: Center(
child: TextButton(
onPressed: () {
_addNewTag(searchTagController.text);
},
child: Text('Add tag "${searchTagController.text}"'),
),
),
),
)
],
)
);
}
}