Files
hlaskovnik-reimagined/frontend/lib/pages/list.dart
T
2026-04-22 19:45:24 +02:00

369 lines
12 KiB
Dart
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:frontend/provider/quote_provider.dart';
import 'package:frontend/model/quote.dart';
import 'package:frontend/provider/theme_provider.dart';
import 'package:frontend/values/strings.dart';
import 'package:provider/provider.dart';
import 'package:frontend/values/constants.dart';
class ListPage extends StatefulWidget {
const ListPage({super.key});
@override
State<ListPage> createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
@override
Widget build(BuildContext context) {
final provider = context.watch<QuoteProvider>();
final themeProvider = context.watch<ThemeProvider>();
return RefreshIndicator(
onRefresh: () async {
await provider.readQuotes();
},
child: _bodyBuilder(context, provider, themeProvider),
);
}
Widget _bodyBuilder(BuildContext context, QuoteProvider provider, ThemeProvider themeProvider) {
if (provider.loading) {
return Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Center(child: Text(provider.error!));
}
final quoteCount = provider.filteredQuotes.length;
return
Column(
children: [
Center(
child: Padding(
padding: const EdgeInsets.only(
top: Constants.edgeInset*2
),
child: Text(
'${Strings.allQuotesCount} $quoteCount',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
filterBar(context, provider, themeProvider),
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(Constants.edgeInset/2),
itemCount: provider.filteredQuotes.length,
itemBuilder: (context, index) {
final quote = provider.filteredQuotes[index];
return
ListTile(
contentPadding: EdgeInsets.only(
bottom: Constants.edgeInset,
left: Constants.edgeInset,
right: Constants.edgeInset,
),
title: Text(quote.body),
subtitle: Text(" ${quote.author}"),
trailing: PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'update') {
// open a dialog window that allows for mods
_showUpdateDialog(context, provider, quote);
} else if (value == 'delete') {
// open a confirmation dialog window for deletion
_showDeleteDialog(context, provider, quote);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'update',
child: Row(
children: [
Icon(Icons.edit),
SizedBox(width: 10),
Text(Strings.updateQuote),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete),
SizedBox(width: 10),
Text(Strings.deleteQuote),
],
),
),
],
)
);
},
),
),
],
);
}
void _showUpdateDialog(BuildContext context, QuoteProvider provider, Quote quote) {
final bodyController = TextEditingController(text: quote.body);
final authorController = TextEditingController(text: quote.author);
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(Strings.updateQuote),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
autofocus: true,
decoration: InputDecoration(
label: Text(Strings.quote)
),
controller: bodyController,
),
TextField(
controller: authorController,
decoration: InputDecoration(
label: Text(Strings.author),
suffixIcon: IconButton(
icon: Icon(Icons.arrow_drop_down),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) {
var authors = provider.authors.toList()..sort();
return SizedBox(
height: 300,
child: ListView.builder(
padding: const EdgeInsets.all(Constants.edgeInset),
itemCount: authors.length,
itemBuilder: (context, index) {
final author = authors[index];
return ListTile(
title: Text(author),
onTap: () {
authorController.text = author;
Navigator.pop(context);
},
);
},
),
);
},
);
},
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(Strings.cancel),
),
ElevatedButton(
onPressed: () {
final updatedQuote = Quote(
id: quote.id,
body: bodyController.text,
author: authorController.text,
);
Navigator.pop(context);
provider.updateQuote(updatedQuote);
},
child: const Text(Strings.save),
),
],
);
},
);
}
void _showDeleteDialog(BuildContext context, QuoteProvider provider, Quote quote) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('${Strings.deleteQuote}?'),
content: const Text(Strings.noUndo),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(Strings.cancel),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () {
Navigator.pop(context);
provider.deleteQuote(quote);
},
child: const Text(Strings.delete),
),
],
);
},
);
}
Widget filterBar(BuildContext context, QuoteProvider provider, ThemeProvider themeProvider) {
return Padding(
padding: const EdgeInsets.all(Constants.edgeInset),
child: Row(
children: [
Expanded(
child: TextField(
controller: provider.filterController,
onChanged: (_) => provider.filterQuotes(),
decoration: InputDecoration(
label: Text(
'${Strings.filterQuery} ${Strings.byAttribute} ${(provider.filterBy == 0) ? Strings.author : Strings.quote}'
),
suffixIcon: IconButton(
icon: Icon(Icons.cancel),
onPressed: () {
provider.clearFilterController();
provider.filterQuotes();
},
),
),
),
),
IconButton(
icon: Icon(Icons.more_horiz),
onPressed: () {
_showOptionsBottomSheet(context, provider, themeProvider);
},
),
],
),
);
}
void _showOptionsBottomSheet(BuildContext context, QuoteProvider provider, ThemeProvider themeProvider) {
showModalBottomSheet(
context: context,
builder: (context) {
return Padding(
padding: const EdgeInsets.all(Constants.edgeInset),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(Strings.filterBy),
trailing: Icon(Icons.filter_alt),
onTap: () {
_showFilterOptionsBottomSheet(context, provider);
},
),
ListTile(
title: Text(Strings.exportSelectedQuotes),
trailing: Icon(Icons.import_export),
onTap: () async {
Navigator.pop(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final success = await provider.exportWithSAF();
// Make sure the context is still valid
if (!mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(success ? Strings.fileExportedSuccessfully + provider.exportDir : "export [x]"),
duration: const Duration(seconds: 3),
),
);
}
),
ListTile(
title: Text(Strings.userPrefersDarkMode),
trailing: Consumer<ThemeProvider>(
builder: (context, themeProvider, _) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(right: Constants.edgeInset),
child: Switch(
onChanged: (value) {
themeProvider.setDarkMode(value);
},
value: themeProvider.isDark,
),
),
Icon(Icons.dark_mode),
],
),
),
),
],
),
);
},
);
}
void _showFilterOptionsBottomSheet(
BuildContext context,
QuoteProvider provider,
) {
showModalBottomSheet(
context: context,
builder: (context) {
return Padding(
padding: const EdgeInsets.all(Constants.edgeInset),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioGroup<int>(
groupValue: provider.filterBy,
onChanged: (value) {
if (value != null) {
provider.setFilterBy(value);
Navigator.pop(context);
}
},
child: Column(
children: [
ListTile(
title: const Text(Strings.author),
leading: Radio<int>(value: 0),
onTap: () {
provider.setFilterBy(0);
Navigator.pop(context);
},
),
ListTile(
title: const Text(Strings.quote),
leading: Radio<int>(value: 1),
onTap: () {
provider.setFilterBy(1);
Navigator.pop(context);
},
),
],
),
),
],
)
);
},
);
}
}