369 lines
12 KiB
Dart
Executable File
369 lines
12 KiB
Dart
Executable File
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);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
)
|
||
);
|
||
},
|
||
);
|
||
}
|
||
} |