Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions lib/Views/home.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:stashcard/card/carddetail.dart';
import 'package:stashcard/card/cardlist.dart';
import 'package:stashcard/models/enums.dart';
import 'package:stashcard/providers/db.dart';


class Home extends StatefulWidget {
const Home({super.key});

@override
State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
SortOptions selectedSort = SortOptions.byName;
bool _isSearching = false;
TextEditingController _searchController = TextEditingController();
String searchQuery = '';


@override
void dispose() {
_searchController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
const String title = 'Stashcard';

return Scaffold(
appBar: AppBar(
title: _isSearching ?
TextField(
controller: _searchController,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Search...',
border: InputBorder.none,
),
onChanged: (value) {
setState(() {
searchQuery = value;
});
},
)
: Text(title),
actions: [
IconButton(
onPressed: () {
setState(() {
_isSearching = !_isSearching;
if (!_isSearching) {
_searchController.clear();
searchQuery = '';
}
});
},
icon: Icon(_isSearching ? Icons.close : Icons.search)
),
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) => Theme(
data: ThemeData.from(colorScheme: ColorScheme.of(context)),
child: AlertDialog(
title: const Text("Donate"),
icon: const Icon(Icons.favorite),
iconColor: Colors.red,
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
const Text(
"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.",
softWrap: true,
),
const Text("Enjoy the app!", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),)
],
),
actions: [
FilledButton(
onPressed: () async {
try {
await launchUrl(Uri.parse("https://ko-fi.com/lahev"));
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open donation link')),
);
}
}
},
child: const Text('Donate'),
),
OutlinedButton(
onPressed: () =>
Navigator.pop(context),
child: const Text('Close'),
)
],
)
));
},
icon: const Icon(Icons.favorite_border),
),
PopupMenuButton<SortOptions>(
initialValue: selectedSort,
onSelected: (SortOptions sort) {
setState(() {
selectedSort = sort;
});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<SortOptions>>[
const PopupMenuItem(
value: SortOptions.byName,
child: Text('Sort by name')
),
const PopupMenuItem(
value: SortOptions.byDateCreated,
child: Text('Sort by date created')
),
const PopupMenuItem(
value: SortOptions.byUsage,
child: Text('Sort by usage')
),
],
),
],
),
floatingActionButton: Builder(
builder: (BuildContext context) {
return FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const CardList())
);
},
child: const Icon(Icons.add),
);
},
),
body: CardGrid(selectedOption: selectedSort, searchQuery: searchQuery,),
);
}
}

class CardGrid extends StatefulWidget {

final SortOptions selectedOption;
final String searchQuery;

const CardGrid({super.key, required this.selectedOption, this.searchQuery = ''});

@override
State<CardGrid> createState() => _CardGridState();
}

class _CardGridState extends State<CardGrid> {
late Future<List<UserCard>> _futureCards;
final db = DatabaseHelper();

@override
void initState() {
super.initState();
_futureCards = _loadCards();
}

Future<List<UserCard>> _loadCards() async {
return await db.getUserCardsSorted(widget.selectedOption);
}

Future<void> _refreshCards() async {
setState(() {
_futureCards = _loadCards();
});
}

@override
void didUpdateWidget(covariant CardGrid oldWidget) {
super.didUpdateWidget(oldWidget);

if (widget.selectedOption != oldWidget.selectedOption) {
_refreshCards();
}

if (widget.searchQuery != oldWidget.searchQuery) {
_refreshCards();
}
}

@override
Widget build(BuildContext context) {
return FutureBuilder<List<UserCard>>(
future: _futureCards,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error loading cards"));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text("No cards found"));
}

final userCards = snapshot.data!;
final filteredCards = userCards.where((userCard) {
return widget.searchQuery.isEmpty || userCard.name.toLowerCase().contains(widget.searchQuery.toLowerCase());
}).toList();

return RefreshIndicator(
onRefresh: () => _refreshCards(),
child: GridView.builder(
padding: const EdgeInsets.all(20),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1.5,
),
itemCount: filteredCards.length,
itemBuilder: (context, index) {
final userCard = filteredCards[index];
return GestureDetector(
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => CardDetail(cardId: userCard.id,))
);
if (userCard.id != null) {
db.incrementUsage(userCard.id!);
}
_refreshCards();
},
child: Card(
elevation: 2,
child: Center(
child: Text(userCard.name),
),
),
);
},
),
);
},
);
}
}
135 changes: 135 additions & 0 deletions lib/Views/settings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:stashcard/providers/theme_provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart';

enum AppThemeMode {
system("System"),
light("Light"),
dark("Dark");

final String displayName;
const AppThemeMode(this.displayName);

ThemeMode toFlutterThemeMode() {
switch (this) {
case AppThemeMode.system:
return ThemeMode.system;
case AppThemeMode.light:
return ThemeMode.light;
case AppThemeMode.dark:
return ThemeMode.dark;
}
}

static AppThemeMode fromFlutterThemeMode(ThemeMode flutterMode) {
switch (flutterMode) {
case ThemeMode.system:
return AppThemeMode.system;
case ThemeMode.light:
return AppThemeMode.light;
case ThemeMode.dark:
return AppThemeMode.dark;
}
}
}

class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});

@override
State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
final githubUrl = "https://github.com/LahevOdVika/Stashcard";
final TextEditingController _themeModeController = TextEditingController();

Future<void> _launchUrl() async {
final Uri url = Uri.parse(githubUrl);
if (!await launchUrl(url)) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not open $githubUrl'),)
);
}
}
}

@override
void dispose() {
_themeModeController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final themeProvider = Provider.of<ThemeProvider>(context, listen: false);

return Scaffold(
appBar: AppBar(
title: const Text("Settings"),
),
body: ListView(
children: [
ListTile(
leading: const Icon(Icons.color_lens),
title: const Text("App color scheme"),
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Pick a color"),
content: BlockPicker(
pickerColor: themeProvider.seedColor,
onColorChanged: (Color color) {
themeProvider.setSeedColor(color);
Navigator.of(context).pop();
},
),
);
},
);
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.brightness_4),
title: const Text("App theme"),
trailing: DropdownMenu(
initialSelection: AppThemeMode.fromFlutterThemeMode(themeProvider.themeMode),
controller: _themeModeController,
dropdownMenuEntries: AppThemeMode.values.map<DropdownMenuEntry<AppThemeMode>>(
(AppThemeMode mode) {
return DropdownMenuEntry(value: mode, label: mode.displayName);
},
).toList(),
onSelected: (AppThemeMode? selectedAppMode) {
if (selectedAppMode != null) {
themeProvider.setThemeMode(selectedAppMode.toFlutterThemeMode());
_themeModeController.text = selectedAppMode.displayName;
}
},
),
),
const Divider(),
ListTile(
trailing: TextButton.icon(
onPressed: () {
_launchUrl();
},
icon: const Icon(Icons.code),
label: const Text("Source code"),
),
),
const Divider(),
ListTile(
trailing: const Text("Copyright © 2025 LahevOdVika"),
),
],
),
);
}
}
Loading