Skip to content

Commit 7b5662e

Browse files
authored
Merge pull request #1 from LahevOdVika/settings
Settings
2 parents 0d5b060 + f247604 commit 7b5662e

13 files changed

Lines changed: 498 additions & 346 deletions

File tree

lib/Views/home.dart

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:url_launcher/url_launcher.dart';
3+
import 'package:stashcard/card/carddetail.dart';
4+
import 'package:stashcard/card/cardlist.dart';
5+
import 'package:stashcard/models/enums.dart';
6+
import 'package:stashcard/providers/db.dart';
7+
8+
9+
class Home extends StatefulWidget {
10+
const Home({super.key});
11+
12+
@override
13+
State<Home> createState() => _HomeState();
14+
}
15+
16+
class _HomeState extends State<Home> {
17+
SortOptions selectedSort = SortOptions.byName;
18+
bool _isSearching = false;
19+
TextEditingController _searchController = TextEditingController();
20+
String searchQuery = '';
21+
22+
23+
@override
24+
void dispose() {
25+
_searchController.dispose();
26+
super.dispose();
27+
}
28+
29+
@override
30+
Widget build(BuildContext context) {
31+
const String title = 'Stashcard';
32+
33+
return Scaffold(
34+
appBar: AppBar(
35+
title: _isSearching ?
36+
TextField(
37+
controller: _searchController,
38+
autofocus: true,
39+
decoration: const InputDecoration(
40+
hintText: 'Search...',
41+
border: InputBorder.none,
42+
),
43+
onChanged: (value) {
44+
setState(() {
45+
searchQuery = value;
46+
});
47+
},
48+
)
49+
: Text(title),
50+
actions: [
51+
IconButton(
52+
onPressed: () {
53+
setState(() {
54+
_isSearching = !_isSearching;
55+
if (!_isSearching) {
56+
_searchController.clear();
57+
searchQuery = '';
58+
}
59+
});
60+
},
61+
icon: Icon(_isSearching ? Icons.close : Icons.search)
62+
),
63+
IconButton(
64+
onPressed: () {
65+
showDialog(
66+
context: context,
67+
builder: (BuildContext context) => Theme(
68+
data: ThemeData.from(colorScheme: ColorScheme.of(context)),
69+
child: AlertDialog(
70+
title: const Text("Donate"),
71+
icon: const Icon(Icons.favorite),
72+
iconColor: Colors.red,
73+
content: Column(
74+
mainAxisSize: MainAxisSize.min,
75+
spacing: 10,
76+
children: [
77+
const Text(
78+
"I'm a student and I work on this app in my free time. If you like it, you can support development by donating. And if you don't want to donate, that's fine too.",
79+
softWrap: true,
80+
),
81+
const Text("Enjoy the app!", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),)
82+
],
83+
),
84+
actions: [
85+
FilledButton(
86+
onPressed: () async {
87+
try {
88+
await launchUrl(Uri.parse("https://ko-fi.com/lahev"));
89+
} catch (e) {
90+
if (context.mounted) {
91+
ScaffoldMessenger.of(context).showSnackBar(
92+
const SnackBar(content: Text('Could not open donation link')),
93+
);
94+
}
95+
}
96+
},
97+
child: const Text('Donate'),
98+
),
99+
OutlinedButton(
100+
onPressed: () =>
101+
Navigator.pop(context),
102+
child: const Text('Close'),
103+
)
104+
],
105+
)
106+
));
107+
},
108+
icon: const Icon(Icons.favorite_border),
109+
),
110+
PopupMenuButton<SortOptions>(
111+
initialValue: selectedSort,
112+
onSelected: (SortOptions sort) {
113+
setState(() {
114+
selectedSort = sort;
115+
});
116+
},
117+
itemBuilder: (BuildContext context) => <PopupMenuEntry<SortOptions>>[
118+
const PopupMenuItem(
119+
value: SortOptions.byName,
120+
child: Text('Sort by name')
121+
),
122+
const PopupMenuItem(
123+
value: SortOptions.byDateCreated,
124+
child: Text('Sort by date created')
125+
),
126+
const PopupMenuItem(
127+
value: SortOptions.byUsage,
128+
child: Text('Sort by usage')
129+
),
130+
],
131+
),
132+
],
133+
),
134+
floatingActionButton: Builder(
135+
builder: (BuildContext context) {
136+
return FloatingActionButton(
137+
onPressed: () {
138+
Navigator.push(
139+
context,
140+
MaterialPageRoute(builder: (context) => const CardList())
141+
);
142+
},
143+
child: const Icon(Icons.add),
144+
);
145+
},
146+
),
147+
body: CardGrid(selectedOption: selectedSort, searchQuery: searchQuery,),
148+
);
149+
}
150+
}
151+
152+
class CardGrid extends StatefulWidget {
153+
154+
final SortOptions selectedOption;
155+
final String searchQuery;
156+
157+
const CardGrid({super.key, required this.selectedOption, this.searchQuery = ''});
158+
159+
@override
160+
State<CardGrid> createState() => _CardGridState();
161+
}
162+
163+
class _CardGridState extends State<CardGrid> {
164+
late Future<List<UserCard>> _futureCards;
165+
final db = DatabaseHelper();
166+
167+
@override
168+
void initState() {
169+
super.initState();
170+
_futureCards = _loadCards();
171+
}
172+
173+
Future<List<UserCard>> _loadCards() async {
174+
return await db.getUserCardsSorted(widget.selectedOption);
175+
}
176+
177+
Future<void> _refreshCards() async {
178+
setState(() {
179+
_futureCards = _loadCards();
180+
});
181+
}
182+
183+
@override
184+
void didUpdateWidget(covariant CardGrid oldWidget) {
185+
super.didUpdateWidget(oldWidget);
186+
187+
if (widget.selectedOption != oldWidget.selectedOption) {
188+
_refreshCards();
189+
}
190+
191+
if (widget.searchQuery != oldWidget.searchQuery) {
192+
_refreshCards();
193+
}
194+
}
195+
196+
@override
197+
Widget build(BuildContext context) {
198+
return FutureBuilder<List<UserCard>>(
199+
future: _futureCards,
200+
builder: (context, snapshot) {
201+
if (snapshot.hasError) {
202+
return const Center(child: Text("Error loading cards"));
203+
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
204+
return const Center(child: Text("No cards found"));
205+
}
206+
207+
final userCards = snapshot.data!;
208+
final filteredCards = userCards.where((userCard) {
209+
return widget.searchQuery.isEmpty || userCard.name.toLowerCase().contains(widget.searchQuery.toLowerCase());
210+
}).toList();
211+
212+
return RefreshIndicator(
213+
onRefresh: () => _refreshCards(),
214+
child: GridView.builder(
215+
padding: const EdgeInsets.all(20),
216+
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
217+
crossAxisCount: 2,
218+
crossAxisSpacing: 10,
219+
mainAxisSpacing: 10,
220+
childAspectRatio: 1.5,
221+
),
222+
itemCount: filteredCards.length,
223+
itemBuilder: (context, index) {
224+
final userCard = filteredCards[index];
225+
return GestureDetector(
226+
onTap: () async {
227+
await Navigator.push(
228+
context,
229+
MaterialPageRoute(builder: (context) => CardDetail(cardId: userCard.id,))
230+
);
231+
if (userCard.id != null) {
232+
db.incrementUsage(userCard.id!);
233+
}
234+
_refreshCards();
235+
},
236+
child: Card(
237+
elevation: 2,
238+
child: Center(
239+
child: Text(userCard.name),
240+
),
241+
),
242+
);
243+
},
244+
),
245+
);
246+
},
247+
);
248+
}
249+
}

lib/Views/settings.dart

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
3+
import 'package:stashcard/providers/theme_provider.dart';
4+
import 'package:url_launcher/url_launcher.dart';
5+
import 'package:provider/provider.dart';
6+
7+
enum AppThemeMode {
8+
system("System"),
9+
light("Light"),
10+
dark("Dark");
11+
12+
final String displayName;
13+
const AppThemeMode(this.displayName);
14+
15+
ThemeMode toFlutterThemeMode() {
16+
switch (this) {
17+
case AppThemeMode.system:
18+
return ThemeMode.system;
19+
case AppThemeMode.light:
20+
return ThemeMode.light;
21+
case AppThemeMode.dark:
22+
return ThemeMode.dark;
23+
}
24+
}
25+
26+
static AppThemeMode fromFlutterThemeMode(ThemeMode flutterMode) {
27+
switch (flutterMode) {
28+
case ThemeMode.system:
29+
return AppThemeMode.system;
30+
case ThemeMode.light:
31+
return AppThemeMode.light;
32+
case ThemeMode.dark:
33+
return AppThemeMode.dark;
34+
}
35+
}
36+
}
37+
38+
class SettingsPage extends StatefulWidget {
39+
const SettingsPage({super.key});
40+
41+
@override
42+
State<SettingsPage> createState() => _SettingsPageState();
43+
}
44+
45+
class _SettingsPageState extends State<SettingsPage> {
46+
final githubUrl = "https://github.com/LahevOdVika/Stashcard";
47+
final TextEditingController _themeModeController = TextEditingController();
48+
49+
Future<void> _launchUrl() async {
50+
final Uri url = Uri.parse(githubUrl);
51+
if (!await launchUrl(url)) {
52+
if (mounted) {
53+
ScaffoldMessenger.of(context).showSnackBar(
54+
SnackBar(content: Text('Could not open $githubUrl'),)
55+
);
56+
}
57+
}
58+
}
59+
60+
@override
61+
void dispose() {
62+
_themeModeController.dispose();
63+
super.dispose();
64+
}
65+
66+
@override
67+
Widget build(BuildContext context) {
68+
final themeProvider = Provider.of<ThemeProvider>(context, listen: false);
69+
70+
return Scaffold(
71+
appBar: AppBar(
72+
title: const Text("Settings"),
73+
),
74+
body: ListView(
75+
children: [
76+
ListTile(
77+
leading: const Icon(Icons.color_lens),
78+
title: const Text("App color scheme"),
79+
onTap: () {
80+
showDialog(
81+
context: context,
82+
builder: (BuildContext context) {
83+
return AlertDialog(
84+
title: const Text("Pick a color"),
85+
content: BlockPicker(
86+
pickerColor: themeProvider.seedColor,
87+
onColorChanged: (Color color) {
88+
themeProvider.setSeedColor(color);
89+
Navigator.of(context).pop();
90+
},
91+
),
92+
);
93+
},
94+
);
95+
},
96+
),
97+
const Divider(),
98+
ListTile(
99+
leading: const Icon(Icons.brightness_4),
100+
title: const Text("App theme"),
101+
trailing: DropdownMenu(
102+
initialSelection: AppThemeMode.fromFlutterThemeMode(themeProvider.themeMode),
103+
controller: _themeModeController,
104+
dropdownMenuEntries: AppThemeMode.values.map<DropdownMenuEntry<AppThemeMode>>(
105+
(AppThemeMode mode) {
106+
return DropdownMenuEntry(value: mode, label: mode.displayName);
107+
},
108+
).toList(),
109+
onSelected: (AppThemeMode? selectedAppMode) {
110+
if (selectedAppMode != null) {
111+
themeProvider.setThemeMode(selectedAppMode.toFlutterThemeMode());
112+
_themeModeController.text = selectedAppMode.displayName;
113+
}
114+
},
115+
),
116+
),
117+
const Divider(),
118+
ListTile(
119+
trailing: TextButton.icon(
120+
onPressed: () {
121+
_launchUrl();
122+
},
123+
icon: const Icon(Icons.code),
124+
label: const Text("Source code"),
125+
),
126+
),
127+
const Divider(),
128+
ListTile(
129+
trailing: const Text("Copyright © 2025 LahevOdVika"),
130+
),
131+
],
132+
),
133+
);
134+
}
135+
}

0 commit comments

Comments
 (0)