diff --git a/ltx_flutter/lib/appwrite/appwrite.dart b/ltx_flutter/lib/appwrite/auth_api.dart similarity index 100% rename from ltx_flutter/lib/appwrite/appwrite.dart rename to ltx_flutter/lib/appwrite/auth_api.dart diff --git a/ltx_flutter/lib/appwrite/categories_api.dart b/ltx_flutter/lib/appwrite/categories_api.dart index 4ac80c1..902a53c 100644 --- a/ltx_flutter/lib/appwrite/categories_api.dart +++ b/ltx_flutter/lib/appwrite/categories_api.dart @@ -1,30 +1,63 @@ import 'package:appwrite/appwrite.dart'; import 'package:appwrite/models.dart'; import 'package:ltx_flutter/constants/constants.dart'; -import 'package:flutter/widgets.dart'; -import 'package:ltx_flutter/appwrite/appwrite.dart'; +import 'package:ltx_flutter/constants/colors.dart'; +import 'package:flutter/material.dart'; + +class Category { + late final Color backgroundColor; + late final Color foregroundColor; + late final num number; + late final num? parent; + late final String name; + late final String? description; + + Category(Document doc) { + backgroundColor = _getBackgroundColor(doc.data['color']); + foregroundColor = _getForegroundColor(doc.data['color']); + number = doc.data['number']; + parent = doc.data['parent']; + name = doc.data['name']; + description = doc.data['description']; + } + + Color _getBackgroundColor(String colorStr) { + return Color(int.parse("0xff$colorStr")); + } + + Color _getForegroundColor(String colorStr) { + return Color(int.parse("0xff$colorStr")).computeLuminance() > 0.5 + ? Colors.black + : Colors.white; + } + + bool hasParent() { + return parent == number ? false : true; + } + + double leftPadding() { + return hasParent() ? 50.0 : 20.0; + } +} class CategoriesAPI extends ChangeNotifier { Client client = Client(); late final Account account; late final Databases databases; - late final String userId; - final AuthAPI auth = AuthAPI(); + late List _categories = []; // Constructor CategoriesAPI() { init(); + getCategories(); } - // loadUser() async { - // try { - // user = await account.get(); - // notifyListeners(); - // } catch (e) { - // print(e); - // notifyListeners(); - // } - // } + // Getters + get total => _categories.length; + get all => _categories; + get isEmpty => _categories.isEmpty; + get(n) => Category(_categories[n]); + get colors => CategoryColor.values; init() { client.setEndpoint(APPWRITE_URL).setProject(APPWRITE_PROJECT_ID); @@ -32,34 +65,45 @@ class CategoriesAPI extends ChangeNotifier { databases = Databases(client); } - Future getCategories() { - print("Getting categories"); - return databases.listDocuments( + getCategories() async { + print("Updating categories."); + _categories = []; + var response = await databases.listDocuments( databaseId: CATEGORIES_DB, collectionId: COLLECTION, queries: [ + Query.orderAsc("parent"), Query.orderAsc("number"), ]); + _categories = response.documents; + notifyListeners(); } - Future addCategory({ + Future? addCategory({ required String name, required double number, required String color, String? description, int? parentId, - }) { - return databases.createDocument( - databaseId: CATEGORIES_DB, - collectionId: COLLECTION, - documentId: "category-${number.toString()}", - data: { - 'name': name, - 'number': number, - 'color': color, - 'parent': parentId, - 'description': description, - }); + }) async { + try { + return await databases.createDocument( + databaseId: CATEGORIES_DB, + collectionId: COLLECTION, + documentId: "category-${number.toString()}", + data: { + 'name': name, + 'number': number, + 'color': color, + 'parent': parentId, + 'description': description, + }); + } catch (e) { + print(e); + throw "Didn't work"; + } finally { + notifyListeners(); + } } Future deleteCategory({required double number}) { diff --git a/ltx_flutter/lib/appwrite/database_api.dart b/ltx_flutter/lib/appwrite/database_api.dart index ef5a6e6..8fc6491 100644 --- a/ltx_flutter/lib/appwrite/database_api.dart +++ b/ltx_flutter/lib/appwrite/database_api.dart @@ -1,19 +1,28 @@ import 'package:appwrite/appwrite.dart'; import 'package:appwrite/models.dart'; -import 'package:ltx_flutter/appwrite/appwrite.dart'; +import 'package:flutter/material.dart'; +import 'package:ltx_flutter/appwrite/auth_api.dart'; import 'package:ltx_flutter/constants/constants.dart'; import 'package:intl/intl.dart'; -class DatabaseAPI { +class DatabaseAPI extends ChangeNotifier { Client client = Client(); late final Account account; late final Databases databases; final AuthAPI auth = AuthAPI(); + late List _entries = []; + + // Getter methods + List get entries => _entries; + int? get total => _entries.length; + final DateFormat formatter = DateFormat('yyyy-MM-dd'); + // Constructor DatabaseAPI() { init(); + getEntries(); } init() { @@ -25,7 +34,7 @@ class DatabaseAPI { databases = Databases(client); } - Future getEntries({int limit = 100, String dateISO = ""}) { + getEntries({int limit = 100, String dateISO = ""}) async { if (dateISO == "") { dateISO = DateTime.now().toIso8601String(); } @@ -37,7 +46,7 @@ class DatabaseAPI { print("Getting $limit entries starting from $offset"); - return databases.listDocuments( + var response = await databases.listDocuments( databaseId: APPWRITE_DATABASE_ID, collectionId: COLLECTION, queries: [ @@ -45,6 +54,8 @@ class DatabaseAPI { Query.offset(offset), Query.limit(limit), ]); + _entries = response.documents; + notifyListeners(); } Future addEntry( diff --git a/ltx_flutter/lib/constants/colors.dart b/ltx_flutter/lib/constants/colors.dart new file mode 100644 index 0000000..82b6a27 --- /dev/null +++ b/ltx_flutter/lib/constants/colors.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +enum CategoryColor { + black("273036"), + blue("00a9b3"), + red("c71634"), + darkred("ff2816"), + lime("bfff55"), + green("189749"), + pink("ff65ae"), + purple("5b3ab1"), + cyan("005744"), + orange("ff6d01"), + yellow("fff336"); + + const CategoryColor(this.hex); + + final String hex; + + name() { + return this.toString().split('.').last; + } + + backgroundColor() { + return Color(int.parse("0xff$hex")); + } + + foregroundColor() { + return Color(int.parse("0xff$hex")).computeLuminance() > 0.3 + ? Colors.black + : Colors.white; + } +} diff --git a/ltx_flutter/lib/main.dart b/ltx_flutter/lib/main.dart index 5410b84..c7e4a10 100644 --- a/ltx_flutter/lib/main.dart +++ b/ltx_flutter/lib/main.dart @@ -1,12 +1,19 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:ltx_flutter/pages/tabs_page.dart'; -import 'package:ltx_flutter/appwrite/appwrite.dart'; +import 'package:ltx_flutter/appwrite/auth_api.dart'; +import 'package:ltx_flutter/appwrite/database_api.dart'; +import 'package:ltx_flutter/appwrite/categories_api.dart'; import 'package:ltx_flutter/pages/login_page.dart'; import 'package:provider/provider.dart'; void main() { - runApp(ChangeNotifierProvider( - create: (context) => AuthAPI(), + runApp(MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => AuthAPI()), + ChangeNotifierProvider(create: (context) => DatabaseAPI()), + ChangeNotifierProvider(create: (context) => CategoriesAPI()), + ], child: LifetrackerApp(), )); } @@ -16,19 +23,22 @@ class LifetrackerApp extends StatelessWidget { @override Widget build(BuildContext context) { - final value = context.watch().status; - print('TOP CHANGE Value changed to: $value!'); - + final loginStatus = context.watch().status; return MaterialApp( + scrollBehavior: const MaterialScrollBehavior().copyWith(dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.trackpad, + }), title: 'Lifetracker', theme: ThemeData.from( colorScheme: ColorScheme.dark(), ), - home: value == AuthStatus.uninitialized + home: loginStatus == AuthStatus.uninitialized ? const Scaffold( body: Center(child: CircularProgressIndicator()), ) - : value == AuthStatus.authenticated + : loginStatus == AuthStatus.authenticated ? const TabsPage() : LoginPage()); // DefaultTabController( @@ -55,88 +65,3 @@ class LifetrackerApp extends StatelessWidget { // } } } - -class AccountPage extends StatefulWidget { - const AccountPage({ - super.key, - }); - - @override - AccountPageState createState() { - return AccountPageState(); - } -} - -class AccountPageState extends State { - final _loginFormKey = GlobalKey(); - String email = ""; - String password = ""; - - void _logIn(context) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Logging in...')), - ); - // client.login(email, password); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(30.0), - child: Form( - key: _loginFormKey, - child: Column( - children: [ - SizedBox( - width: 250, - child: TextFormField( - obscureText: false, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Email', - ), - keyboardType: TextInputType.emailAddress, - onChanged: (value) => setState(() { - email = value; - }), - )), - SizedBox( - height: 20, - ), - SizedBox( - width: 250, - child: TextFormField( - obscureText: true, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: 'Password', - ), - keyboardType: TextInputType.visiblePassword, - onChanged: (value) => setState(() { - password = value; - }), - )), - SizedBox( - height: 20, - ), - ElevatedButton.icon( - onPressed: () { - _logIn(context); - }, - icon: Icon(Icons.login), - label: Text("Log in")), - Column( - children: [ - email.isEmpty ? Text("No data") : Text(email), - SizedBox( - height: 10, - ), - password.isEmpty ? Text("No Data") : Text(password), - ], - ) - ], - ), - ), - ); - } -} diff --git a/ltx_flutter/lib/pages/account_page.dart b/ltx_flutter/lib/pages/account_page.dart index 8229b4b..ab3c92c 100644 --- a/ltx_flutter/lib/pages/account_page.dart +++ b/ltx_flutter/lib/pages/account_page.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:ltx_flutter/appwrite/auth_api.dart'; +import 'package:ltx_flutter/appwrite/database_api.dart'; +import 'package:ltx_flutter/appwrite/categories_api.dart'; +import 'package:provider/provider.dart'; class AccountPage extends StatefulWidget { const AccountPage({Key? key}) : super(key: key); @@ -10,8 +14,47 @@ class AccountPage extends StatefulWidget { class _AccountPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Center(child: Text("Account")), + return Column( + children: [ + Expanded( + child: ListView( + children: [ + Card( + child: Consumer(builder: (context, account, child) { + return ListTile( + leading: Icon(Icons.person), + title: Text("Account"), + trailing: Text("${account.username}"), + ); + }), + ), + Card( + child: + Consumer(builder: (context, entries, child) { + return ListTile( + leading: Icon(Icons.edit_note), + title: Text("Entries"), + trailing: Text("${entries.total}"), + ); + }), + ), + Card( + child: Consumer( + builder: (context, categories, child) { + return ListTile( + leading: Icon(Icons.category), + title: Text("Categories"), + trailing: Text("${categories.total}"), + ); + }), + ), + ], + ), + ), + Expanded( + child: Placeholder(), + ), + ], ); } } diff --git a/ltx_flutter/lib/pages/categories_page.dart b/ltx_flutter/lib/pages/categories_page.dart index 708a0ff..4e6e2ea 100644 --- a/ltx_flutter/lib/pages/categories_page.dart +++ b/ltx_flutter/lib/pages/categories_page.dart @@ -1,6 +1,8 @@ import 'package:ltx_flutter/appwrite/categories_api.dart'; import 'package:flutter/material.dart'; import 'package:appwrite/models.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; class CategoriesPage extends StatefulWidget { const CategoriesPage({Key? key}) : super(key: key); @@ -10,53 +12,218 @@ class CategoriesPage extends StatefulWidget { } class _CategoriesPageState extends State { - final api = CategoriesAPI(); - late List? categories = []; - @override void initState() { super.initState(); - loadCategories(); - } - - Future loadCategories() async { - try { - final value = await api.getCategories(); - setState(() { - categories = value.documents; - }); - } catch (e) { - print(e); - } } @override Widget build(BuildContext context) { - return RefreshIndicator( - onRefresh: loadCategories, - child: Center( - child: ListView.builder( - itemCount: categories!.length + 1, - itemBuilder: (context, index) { - if (index >= categories!.length || categories!.isEmpty) { - return ListTile( - leading: Text("New Category"), - ); - } else { - Document? category = categories?[index]; - Color backgroundColor = - Color(int.parse("0x${category!.data['color']}")); - Color textColor = backgroundColor.computeLuminance() > 0.2 - ? Colors.black - : Colors.white; - return ListTile( - tileColor: backgroundColor, - textColor: textColor, - leading: Text(category.data['number'].toString()), - title: Text(category.data['name']), - ); + return Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const NewCategoryPage()), + ).then((value) { + setState(() {}); + }), + tooltip: "New Category", + child: Icon(Icons.add), + ), + body: Consumer(builder: (context, categories, child) { + return ListView.builder( + itemCount: categories.total + 1, + itemBuilder: (context, i) { + if (i < categories.total) { + Category category = categories.get(i); + if (!category.hasParent() && category.number != 0) { + return Column( + children: [ + Divider( + thickness: 1, + ), + CategoryRow(category: category), + ], + ); + } + return CategoryRow(category: category); } }, + ); + }), + ); + } +} + +class CategoryRow extends StatelessWidget { + const CategoryRow({ + super.key, + required this.category, + }); + + final Category category; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + color: category.backgroundColor, + child: SizedBox.square( + dimension: 70, + child: Center( + child: Text( + style: TextStyle( + fontWeight: FontWeight.bold, + color: category.foregroundColor, + fontSize: 18, + ), + category.number.toString())), + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.only(left: category.leftPadding()), + child: Text( + style: TextStyle( + fontSize: 17, + ), + category.name, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Text( + style: TextStyle( + fontSize: 13, + color: Colors.white38, + ), + "${category.description}", + ), + ), + ), + ], + ); + } +} + +class NewCategoryPage extends StatefulWidget { + const NewCategoryPage({super.key}); + + @override + State createState() => _NewCategoryPageState(); +} + +class _NewCategoryPageState extends State { + String _name = ""; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _name = "New Category"; + } + + @override + Widget build(BuildContext context) { + final CategoriesAPI api = context.watch(); + saveCategory(form, context) { + var name = form?.fields['categoryName'].value; + var number = form?.fields['categoryNumber'].value; + var color = form?.fields['categoryColor'].value; + var description = form?.fields['categoryDescription'].value; + var parent = form?.fields['categoryParent'].value; + api.addCategory( + name: name, + number: double.parse(number), + color: color, + description: description, + parentId: int.parse(parent), + ); + Navigator.pop(context); + api.getCategories(); + } + + return Scaffold( + appBar: AppBar( + title: const Text("New Category"), + ), + body: Padding( + padding: const EdgeInsets.only(left: 50, right: 50), + child: Column( + children: [ + FormBuilder( + key: _formKey, + child: Column( + children: [ + SizedBox(height: 25), + FormBuilderTextField( + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: "Number", + ), + name: 'categoryNumber', + ), + SizedBox(height: 25), + FormBuilderTextField( + decoration: InputDecoration( + labelText: "Name", + ), + name: 'categoryName', + ), + SizedBox(height: 25), + Consumer( + builder: (context, categories, child) { + print(categories.colors); + return FormBuilderDropdown( + decoration: InputDecoration( + labelText: "Color", + ), + items: [ + for (var c in categories.colors) + DropdownMenuItem( + value: c.hex, + child: Container( + decoration: + BoxDecoration(color: c.backgroundColor()), + padding: EdgeInsets.all(20), + child: Text( + c.name(), + style: TextStyle(color: c.foregroundColor()), + ), + ), + ) + ], + name: 'categoryColor', + ); + }, + ), + SizedBox(height: 25), + FormBuilderTextField( + name: 'categoryDescription', + decoration: InputDecoration( + labelText: "Description", + ), + ), + SizedBox(height: 25), + FormBuilderTextField( + keyboardType: TextInputType.number, + name: 'categoryParent', + decoration: InputDecoration( + labelText: "Parent", + ), + ), + ], + ), + ), + SizedBox(height: 40), + OutlinedButton( + onPressed: () => saveCategory(_formKey.currentState, context), + child: Text("Save"), + ), + ], ), ), ); diff --git a/ltx_flutter/lib/pages/login_page.dart b/ltx_flutter/lib/pages/login_page.dart index b2be6bc..b4467e3 100644 --- a/ltx_flutter/lib/pages/login_page.dart +++ b/ltx_flutter/lib/pages/login_page.dart @@ -1,5 +1,5 @@ import 'package:appwrite/appwrite.dart'; -import 'package:ltx_flutter/appwrite/appwrite.dart'; +import 'package:ltx_flutter/appwrite/auth_api.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/ltx_flutter/lib/pages/table_page.dart b/ltx_flutter/lib/pages/table_page.dart index e7d3461..e4e5131 100644 --- a/ltx_flutter/lib/pages/table_page.dart +++ b/ltx_flutter/lib/pages/table_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:spreadsheet_table/spreadsheet_table.dart'; import 'package:pluto_grid/pluto_grid.dart'; -import 'package:ltx_flutter/appwrite/appwrite.dart'; +import 'package:ltx_flutter/appwrite/auth_api.dart'; import 'package:ltx_flutter/appwrite/database_api.dart'; import 'package:provider/provider.dart'; import 'package:appwrite/models.dart'; diff --git a/ltx_flutter/lib/pages/tabs_page.dart b/ltx_flutter/lib/pages/tabs_page.dart index 7b30d5f..b6507b3 100644 --- a/ltx_flutter/lib/pages/tabs_page.dart +++ b/ltx_flutter/lib/pages/tabs_page.dart @@ -11,7 +11,7 @@ class TabsPage extends StatefulWidget { } class _TabsPageState extends State { - int _selectedIndex = 0; + int _selectedIndex = 2; static const _widgets = [TodayPage(), CategoriesPage(), AccountPage()]; @@ -25,7 +25,7 @@ class _TabsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("LTX Android"), + title: Text("Flutter"), ), body: _widgets.elementAt(_selectedIndex), bottomNavigationBar: BottomNavigationBar( diff --git a/ltx_flutter/lib/pages/today_page.dart b/ltx_flutter/lib/pages/today_page.dart index 7e293b1..33b5185 100644 --- a/ltx_flutter/lib/pages/today_page.dart +++ b/ltx_flutter/lib/pages/today_page.dart @@ -1,4 +1,5 @@ -import 'package:ltx_flutter/appwrite/appwrite.dart'; +import 'dart:math' as math; +import 'package:ltx_flutter/appwrite/auth_api.dart'; import 'package:ltx_flutter/appwrite/database_api.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -92,6 +93,38 @@ class _TodayPageState extends State { }, ).toList()), ), + Container( + padding: EdgeInsets.only(top: 10), + color: Colors.black, + child: Row( + children: List.generate(10, (i) => i).map( + (e) { + var generatedColor = + math.Random().nextInt(Colors.primaries.length); + return Expanded( + child: Padding( + padding: const EdgeInsets.all(2.0), + child: FilledButton( + style: ButtonStyle( + shape: MaterialStateProperty.all< + RoundedRectangleBorder>( + RoundedRectangleBorder( + borderRadius: BorderRadius.zero, + side: BorderSide(color: Colors.white))), + backgroundColor: MaterialStateProperty.all( + Colors.primaries[generatedColor]), + ), + onLongPress: () => + print("Long pressed ${e.toString()}"), + onPressed: () => print("Tapped ${e.toString()}"), + child: Text(e.toString()), + ), + ), + ); + }, + ).toList(), + ), + ), ], ), ); diff --git a/ltx_flutter/pubspec.yaml b/ltx_flutter/pubspec.yaml index cd149e3..28024ab 100644 --- a/ltx_flutter/pubspec.yaml +++ b/ltx_flutter/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: intl: ^0.18.1 pluto_grid: ^7.0.2 string_to_hex: ^0.2.2 + flutter_form_builder: ^9.0.0 dev_dependencies: flutter_test: