diff --git a/.metadata b/.metadata index 1c353fa..5f0d710 100644 --- a/.metadata +++ b/.metadata @@ -15,21 +15,6 @@ migration: - platform: root create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - - platform: android - create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - - platform: ios - create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - - platform: linux - create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - - platform: macos - create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - - platform: web - create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 - platform: windows create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198 diff --git a/lib/models/db_column_definition.dart b/lib/models/db_column_definition.dart new file mode 100644 index 0000000..3443120 --- /dev/null +++ b/lib/models/db_column_definition.dart @@ -0,0 +1,106 @@ +import 'package:tuuli_api/tuuli_api.dart'; + +extension ColumnsParser on TableDefinition { + List get parsedColumns { + return columns + .split(",") + .map((e) { + final parts = e.split(":"); + final name = parts[0]; + switch (parts[1]) { + case "serial": + return PrimarySerialColumnDefinition(name); + case "str": + return TextColumnDefinition( + name, + parts.contains("unique"), + parts.contains("default"), + ); + case "bool": + return BooleanColumnDefinition( + name, + parts.contains("unique"), + parts.contains("default"), + ); + case "datetime": + return TimestampColumnDefinition( + name, + parts.contains("unique"), + parts.contains("default"), + ); + case "float": + return DoubleColumnDefinition( + name, + parts.contains("unique"), + parts.contains("default"), + ); + case "int": + return IntegerColumnDefinition( + name, + parts.contains("unique"), + parts.contains("default"), + ); + case "int-user": + return UserRefColumnDefinition(name); + case "int-asset": + return AssetRefColumnDefinition(name); + } + + return null; + }) + .whereType() + .toList(growable: false); + } +} + +abstract class DBColumnDefinition { + final String name; + final bool unique; + final bool hasDefault; + + DBColumnDefinition({ + required this.name, + required this.unique, + required this.hasDefault, + }); +} + +class PrimarySerialColumnDefinition extends DBColumnDefinition { + PrimarySerialColumnDefinition(String name) + : super(name: name, unique: false, hasDefault: false); +} + +class TextColumnDefinition extends DBColumnDefinition { + TextColumnDefinition(String name, bool unique, bool hasDefault) + : super(name: name, unique: unique, hasDefault: hasDefault); +} + +class BooleanColumnDefinition extends DBColumnDefinition { + BooleanColumnDefinition(String name, bool unique, bool hasDefault) + : super(name: name, unique: unique, hasDefault: hasDefault); +} + +class TimestampColumnDefinition extends DBColumnDefinition { + TimestampColumnDefinition(String name, bool unique, bool hasDefault) + : super(name: name, unique: unique, hasDefault: hasDefault); +} + +class DoubleColumnDefinition extends DBColumnDefinition { + DoubleColumnDefinition(String name, bool unique, bool hasDefault) + : super(name: name, unique: unique, hasDefault: hasDefault); +} + +class IntegerColumnDefinition extends DBColumnDefinition { + IntegerColumnDefinition(String name, bool unique, bool hasDefault) + : super(name: name, unique: unique, hasDefault: hasDefault); +} + +class UserRefColumnDefinition extends DBColumnDefinition { + UserRefColumnDefinition(String name) + : super(name: name, unique: false, hasDefault: false); +} + +class AssetRefColumnDefinition extends DBColumnDefinition { + AssetRefColumnDefinition(String name) + : super(name: name, unique: false, hasDefault: false); +} diff --git a/lib/models/group_definition.dart b/lib/models/group_definition.dart new file mode 100644 index 0000000..22c4a17 --- /dev/null +++ b/lib/models/group_definition.dart @@ -0,0 +1,11 @@ +class GroupDefinition { + final int id; + final String name; + final String description; + + const GroupDefinition({ + required this.id, + required this.name, + required this.description, + }); +} diff --git a/lib/models/user_definition.dart b/lib/models/user_definition.dart new file mode 100644 index 0000000..31aa60a --- /dev/null +++ b/lib/models/user_definition.dart @@ -0,0 +1,13 @@ +class UserDefinition { + final int id; + final String username; + final String password; + final String accessToken; + + const UserDefinition({ + required this.id, + required this.username, + required this.password, + required this.accessToken, + }); +} diff --git a/lib/models/user_in_group_definition.dart b/lib/models/user_in_group_definition.dart new file mode 100644 index 0000000..360ed47 --- /dev/null +++ b/lib/models/user_in_group_definition.dart @@ -0,0 +1,11 @@ +class UserInGroupDefinition { + final int id; + final int userId; + final int groupId; + + const UserInGroupDefinition({ + required this.id, + required this.userId, + required this.groupId, + }); +} diff --git a/lib/pages/bottomsheets/create_table_item_bottomsheet.dart b/lib/pages/bottomsheets/create_table_item_bottomsheet.dart index 45c5621..5fac541 100644 --- a/lib/pages/bottomsheets/create_table_item_bottomsheet.dart +++ b/lib/pages/bottomsheets/create_table_item_bottomsheet.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +/*import 'package:flutter/material.dart'; import 'package:flutter_fast_forms/flutter_fast_forms.dart'; import 'package:get/get.dart'; import 'package:tuuli_app/api/api_client.dart'; @@ -196,3 +196,4 @@ class _CreateTableItemBottomSheetState } } } +*/ \ No newline at end of file diff --git a/lib/pages/bottomsheets/create_user_bottomsheet.dart b/lib/pages/bottomsheets/create_user_bottomsheet.dart index 886ef86..72c8223 100644 --- a/lib/pages/bottomsheets/create_user_bottomsheet.dart +++ b/lib/pages/bottomsheets/create_user_bottomsheet.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +/*import 'package:flutter/material.dart'; import 'package:flutter_fast_forms/flutter_fast_forms.dart'; import 'package:get/get.dart'; import 'package:tuuli_app/api/api_client.dart'; @@ -215,3 +215,4 @@ class _CreateUserBottomSheetState extends State { ); } } +*/ \ No newline at end of file diff --git a/lib/pages/bottomsheets/edit_table_bottomsheet.dart b/lib/pages/bottomsheets/edit_table_bottomsheet.dart index 0bf8b4a..064e38f 100644 --- a/lib/pages/bottomsheets/edit_table_bottomsheet.dart +++ b/lib/pages/bottomsheets/edit_table_bottomsheet.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +/*import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:recase/recase.dart'; import 'package:tuuli_app/api/model/table_field_model.dart'; @@ -240,3 +240,4 @@ class _EditTableBottomSheetState extends State { }); } } +*/ \ No newline at end of file diff --git a/lib/pages/bottomsheets/open_table_bottomsheet.dart b/lib/pages/bottomsheets/open_table_bottomsheet.dart index 44b95db..045c043 100644 --- a/lib/pages/bottomsheets/open_table_bottomsheet.dart +++ b/lib/pages/bottomsheets/open_table_bottomsheet.dart @@ -1,4 +1,4 @@ -import 'package:bottom_sheet/bottom_sheet.dart'; +/*import 'package:bottom_sheet/bottom_sheet.dart'; import 'package:data_table_2/data_table_2.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -255,3 +255,4 @@ class _OpenTableBottomSheetState extends State { }); } } +*/ \ No newline at end of file diff --git a/lib/pages/checkup_page.dart b/lib/pages/checkup_page.dart index aab6c9a..a4fe508 100644 --- a/lib/pages/checkup_page.dart +++ b/lib/pages/checkup_page.dart @@ -1,23 +1,17 @@ -import 'package:animated_background/animated_background.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tuuli_app/api_controller.dart'; class CheckupPageController extends GetxController { - @override - void onInit() { - super.onInit(); - } - Future checkCredentials() async { if (ApiController.to.token.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { + ApiController.to.token = ""; Get.offAllNamed("/login"); }); } else { try { - final resp = - await ApiController.to.apiClient.listTablesApiListTablesGet(); + final resp = await ApiController.to.apiClient.listTables(); WidgetsBinding.instance.addPostFrameCallback((_) { if (resp.statusCode == 200) { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 26e351c..a6f43d7 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:tuuli_app/api/api_client.dart'; -import 'package:tuuli_app/api/model/tables_list_model.dart'; +import 'package:tuuli_app/api_controller.dart'; import 'package:tuuli_app/c.dart'; import 'package:tuuli_app/pages/home_panels/none_panel.dart'; import 'package:tuuli_app/pages/home_panels/settings_panel.dart'; @@ -16,35 +15,13 @@ enum PageType { settings, } -mixin HomePageStateRef { - void refreshData(); -} +class HomePageController extends GetxController { + final _currentPage = PageType.none.obs; + PageType get currentPage => _currentPage.value; + set currentPage(PageType value) => _currentPage.value = value; -class HomePage extends StatefulWidget { - const HomePage({super.key}); + String get currentPageName => pageNames[currentPage]!; - @override - State createState() => _HomePageState(); -} - -class _HomePageState extends State with HomePageStateRef { - final apiClient = Get.find(); - TablesListModel tables = TablesListModel([]); - - TableModel get usersTable => - tables.tables.firstWhere((element) => element.tableName == "users"); - - bool get isNarrow => - (MediaQuery.of(context).size.width - C.materialDrawerWidth) <= 600; - - @override - void initState() { - super.initState(); - - refreshData(); - } - - var currentPage = PageType.none; final pageNames = { PageType.none: "Home", PageType.tables: "Tables", @@ -52,8 +29,41 @@ class _HomePageState extends State with HomePageStateRef { PageType.settings: "Settings", }; + @override + void onInit() { + super.onInit(); + + Get.lazyPut( + () => TablesListPanelController(), + fenix: true, + ); + Get.lazyPut( + () => UserListPanelController(), + fenix: true, + ); + } + + Future logout() async { + ApiController.to.token = ""; + + await Future.wait([ + Get.delete(), + Get.delete(), + Get.delete(), + ]); + + Get.offAllNamed("/"); + } +} + +class HomePage extends GetView { + const HomePage({super.key}); + + bool get isNarrow => + (Get.mediaQuery.size.width - C.materialDrawerWidth) <= 600; + AppBar get appBar => AppBar( - title: Text(pageNames[currentPage]!), + title: Obx(() => Text(controller.currentPageName)), actions: [ IconButton( icon: const Icon(Icons.home), @@ -62,51 +72,49 @@ class _HomePageState extends State with HomePageStateRef { }, ), IconButton( - icon: const Icon(Icons.refresh), - onPressed: refreshData, + icon: const Icon(Icons.logout), + onPressed: () => controller.logout(), ), ], ); ListView get drawerOptions => ListView( children: [ - ListTile( - leading: const Icon(Icons.table_chart), - title: const Text("Tables"), - onTap: () { - setState(() { - currentPage = PageType.tables; - }); - }, + Obx( + () => ListTile( + leading: const Icon(Icons.table_chart), + title: const Text("Tables"), + onTap: () { + controller.currentPage = PageType.tables; + }, + selected: controller.currentPage == PageType.tables, + ), ), - ListTile( - leading: const Icon(Icons.person), - title: const Text("Users"), - onTap: () { - setState(() { - currentPage = PageType.users; - }); - }, + Obx( + () => ListTile( + leading: const Icon(Icons.person), + title: const Text("Users"), + onTap: () { + controller.currentPage = PageType.users; + }, + selected: controller.currentPage == PageType.users, + ), ), - ListTile( - leading: const Icon(Icons.settings), - title: const Text("Settings"), - onTap: () { - setState(() { - currentPage = PageType.settings; - }); - }, + Obx( + () => ListTile( + leading: const Icon(Icons.settings), + title: const Text("Settings"), + onTap: () { + controller.currentPage = PageType.settings; + }, + selected: controller.currentPage == PageType.settings, + ), ), const Divider(), ListTile( leading: const Icon(Icons.logout), title: const Text("Logout"), - onTap: () { - GetStorage().erase().then((value) { - GetStorage().save(); - Get.offAllNamed("/"); - }); - }, + onTap: () => controller.logout(), ), ], ); @@ -140,12 +148,12 @@ class _HomePageState extends State with HomePageStateRef { ), LimitedBox( maxWidth: MediaQuery.of(context).size.width - C.materialDrawerWidth, - child: Builder(builder: (context) { - switch (currentPage) { + child: Obx(() { + switch (controller.currentPage) { case PageType.tables: - return TablesListPanel(parent: this, tables: tables); + return const TablesListPanel(); case PageType.users: - return UsersListPanel(usersTable: usersTable); + return const UsersListPanel(); case PageType.settings: return const SettingsPanel(); case PageType.none: @@ -158,25 +166,4 @@ class _HomePageState extends State with HomePageStateRef { ), ); } - - @override - void refreshData() { - apiClient.tablesList().then( - (value) => value.unfold( - (tables) { - setState(() { - this.tables = tables; - }); - }, - (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(error.toString()), - ), - ); - }, - ), - ); - } } diff --git a/lib/pages/home_panels/tables_list_panel.dart b/lib/pages/home_panels/tables_list_panel.dart index 569269f..66e70e9 100644 --- a/lib/pages/home_panels/tables_list_panel.dart +++ b/lib/pages/home_panels/tables_list_panel.dart @@ -1,110 +1,106 @@ -import 'package:bottom_sheet/bottom_sheet.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tuuli_api/tuuli_api.dart'; +import 'package:tuuli_app/api_controller.dart'; import 'package:recase/recase.dart'; -import 'package:tuuli_app/api/api_client.dart'; -import 'package:tuuli_app/api/model/tables_list_model.dart'; -import 'package:tuuli_app/c.dart'; -import 'package:tuuli_app/pages/bottomsheets/edit_table_bottomsheet.dart'; -import 'package:tuuli_app/pages/bottomsheets/open_table_bottomsheet.dart'; -import 'package:tuuli_app/pages/home_page.dart'; - -class TablesListPanel extends StatefulWidget { - final TablesListModel tables; - final HomePageStateRef parent; - - const TablesListPanel({ - super.key, - required this.tables, - required this.parent, - }); +import 'package:tuuli_app/models/db_column_definition.dart'; +class TablesListPanelController extends GetxController { @override - State createState() => _TablesListPanelState(); + void onInit() { + super.onInit(); + + refreshData(); + } + + final _isLoading = false.obs; + bool get isLoading => _isLoading.value; + + final _tables = [].obs; + List get tables => _tables; + + Future refreshData() async { + _isLoading.value = true; + try { + final resp = await ApiController.to.apiClient.listTables(); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + _tables.clear(); + _tables.addAll(respData); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to get tables", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to get tables", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to get tables", + "$e", + ); + } + _isLoading.value = false; + } + + Future createNewTable() async {} + + Future openTable(TableDefinition table) async {} } -class _TablesListPanelState extends State { - final apiClient = Get.find(); - - @override - void initState() { - super.initState(); - } +class TablesListPanel extends GetView { + const TablesListPanel({super.key}); @override Widget build(BuildContext context) { - return _buildTableList(); - } - - Widget _buildTableCard(BuildContext ctx, TableModel table) { - return Card( - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () => _openTable(table), - child: Container( - margin: const EdgeInsets.all(5), - padding: const EdgeInsets.all(5), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - table.tableName.pascalCase, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Row( - children: [ - Text( - table.system ? "System" : "Userland", - style: const TextStyle( - fontSize: 12, - ), - ), - ], - ), - Row( - children: [ - Text( - "${table.columns.length} column(s)", - style: const TextStyle( - fontSize: 11, - fontStyle: FontStyle.italic, - ), - ), - ], - ) - ], - ), - ], + return Stack( + children: [ + Column( + children: [ + AppBar( + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () => controller.createNewTable(), + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => controller.refreshData(), + ), + ], + ), + Expanded( + child: Obx( + () => controller.tables.isEmpty ? whenNoTables : cardGrid), + ), + ], + ), + Obx( + () => Positioned( + right: 16, + bottom: 16, + child: controller.isLoading + ? const CircularProgressIndicator() + : const SizedBox(), ), ), - ), + ], ); } - Widget get newTableCard => Card( - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: _createNewTable, - child: Container( - margin: const EdgeInsets.all(5), - padding: const EdgeInsets.all(5), - child: const Icon(Icons.add), - ), - ), - ); - - Widget _buildTableList() { - var tableItems = widget.tables.tables - .where((table) => !table.hidden && !table.system) - .toList(growable: false); - - if (tableItems.isEmpty) { - return Center( + Widget get whenNoTables => Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, @@ -113,7 +109,7 @@ class _TablesListPanelState extends State { Text( "No tables found", style: TextStyle( - color: Theme.of(context).disabledColor, + color: Get.theme.disabledColor, fontSize: 24, ), ), @@ -121,81 +117,66 @@ class _TablesListPanelState extends State { Text( "Maybe create one?", style: TextStyle( - color: Theme.of(context).disabledColor, + color: Get.theme.disabledColor, fontSize: 14, ), ), - const SizedBox(height: 16), - ElevatedButton.icon( - onPressed: _createNewTable, - icon: const Icon(Icons.add), - label: const Text("Create table"), - ) ], ), ); - } - return GridView.builder( - shrinkWrap: true, - itemCount: tableItems.length + 1, - itemBuilder: (ctx, i) => - i == 0 ? newTableCard : _buildTableCard(ctx, tableItems[i - 1]), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, + Widget get cardGrid => GridView.count( + shrinkWrap: true, + crossAxisCount: 3, childAspectRatio: 5 / 2, + children: [...controller.tables.map((e) => _buildTableCard(e))], + ); + + Widget _buildTableCard(TableDefinition table) { + return Card( + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () => controller.openTable(table), + child: Container( + margin: const EdgeInsets.all(5), + padding: const EdgeInsets.all(5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + table.tableName.pascalCase, + maxLines: 1, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + ), + Row( + children: [ + Text( + table.system ? "System" : "Userland", + style: const TextStyle( + fontSize: 12, + ), + ), + ], + ), + Row( + children: [ + Text( + "${table.parsedColumns.length} column(s)", + style: const TextStyle( + fontSize: 11, + fontStyle: FontStyle.italic, + ), + ), + ], + ) + ], + ), + ), ), ); } - - void _createNewTable() async { - final result = await showFlexibleBottomSheet( - minHeight: 0, - initHeight: 0.75, - maxHeight: 1, - context: context, - builder: (_, __, ___) => const EditTableBottomSheet(), - anchors: [0, 0.5, 1], - isSafeArea: true, - isDismissible: false, - ); - - if (result == null) return; - - await apiClient - .createTable(result.tableName, result.fields) - .then((e) => WidgetsBinding.instance.addPostFrameCallback((_) { - e.unfold((_) { - Get.snackbar( - "Success", - "Table created", - colorText: Colors.white, - ); - }, (error) { - Get.defaultDialog( - title: "Error", - middleText: error.toString(), - textConfirm: "OK", - onConfirm: () => Get.back(), - ); - }); - })); - - widget.parent.refreshData(); - } - - void _openTable(TableModel table) async { - await showFlexibleBottomSheet( - minHeight: 1, - initHeight: 1, - maxHeight: 1, - context: context, - builder: (_, __, ___) => OpenTableBottomSheet(table: table), - anchors: [0, 0.5, 1], - isSafeArea: true, - isDismissible: false, - ); - - widget.parent.refreshData(); - } } diff --git a/lib/pages/home_panels/users_list_panel.dart b/lib/pages/home_panels/users_list_panel.dart index aa316f5..9e8f73c 100644 --- a/lib/pages/home_panels/users_list_panel.dart +++ b/lib/pages/home_panels/users_list_panel.dart @@ -1,225 +1,899 @@ -import 'package:bottom_sheet/bottom_sheet.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tuuli_app/api/api_client.dart'; -import 'package:tuuli_app/api/model/tables_list_model.dart'; -import 'package:tuuli_app/api/model/user_model.dart'; -import 'package:tuuli_app/pages/bottomsheets/create_user_bottomsheet.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:tuuli_api/tuuli_api.dart'; +import 'package:tuuli_app/api_controller.dart'; +import 'package:tuuli_app/models/group_definition.dart'; +import 'package:tuuli_app/models/user_definition.dart'; +import 'package:tuuli_app/models/user_in_group_definition.dart'; -class UsersListPanel extends StatefulWidget { - final TableModel usersTable; - - const UsersListPanel({super.key, required this.usersTable}); - - @override - State createState() => _UsersListPanelState(); +enum UserListPanelTab { + users, + groups, } -class _UsersListPanelState extends State { - final apiClient = Get.find(); - - final users = []; - +class UserListPanelController extends GetxController { @override - void initState() { - super.initState(); + void onInit() { + super.onInit(); - _refreshUsers(); + refreshData(); } - @override - Widget build(BuildContext context) { - return _buildUserList(); + final _currentTab = UserListPanelTab.users.obs; + UserListPanelTab get currentTab => _currentTab.value; + set currentTab(UserListPanelTab value) => _currentTab.value = value; + + final _isLoading = false.obs; + bool get isLoading => _isLoading.value; + + final _users = [].obs; + List get users => _users; + + final _groups = [].obs; + List get groups => _groups; + + final _usersInGroups = >{}.obs; + Map> get usersInGroups => + _usersInGroups; + + Future refreshData() async { + _isLoading.value = true; + + await Future.wait([ + refreshUsers(), + refreshGroups(), + ]); + + await Future.wait([ + for (final group in groups) getGroupUsers(group), + ]); + + _isLoading.value = false; } - Widget _buildUserCard(BuildContext ctx, UserModel user) { - return Card( - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () => _openUser(user), - child: Container( - margin: const EdgeInsets.all(5), - padding: const EdgeInsets.all(5), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - user.username, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Row( - children: [ - Text( - "User id: ${user.id}", - style: const TextStyle( - fontSize: 12, - ), - ), - ], - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget get newUserCard => Card( - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: _createNewUser, - child: Container( - margin: const EdgeInsets.all(5), - padding: const EdgeInsets.all(5), - child: const Icon(Icons.add), - ), + Future refreshUsers() async { + try { + final resp = await ApiController.to.apiClient.getItemsFromTable( + tableName: "users", + itemsSelector: const ItemsSelector( + fields: ["id", "username", "password", "access_token"], + where: [], ), ); - Widget _buildUserList() { - if (users.isEmpty) { - return Center( + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + _users.clear(); + _users.addAll(respData.map((e) => UserDefinition( + id: e["id"], + username: e["username"], + password: e["password"], + accessToken: e["access_token"], + ))); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to get users", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to get users", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to get users", + "$e", + ); + } + } + + Future refreshGroups() async { + try { + final resp = await ApiController.to.apiClient.getItemsFromTable( + tableName: "user_group", + itemsSelector: const ItemsSelector( + fields: ["id", "name", "description"], + where: [], + )); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + _groups.clear(); + _groups.addAll(respData.map((e) => GroupDefinition( + id: e["id"], + name: e["name"], + description: e["description"], + ))); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to get groups", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to get groups", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to get groups", + "$e", + ); + } + } + + Future createNewUser({UserDefinition? user}) async { + final username = "".obs; + final password = "".obs; + if (user != null) { + username.value = user.username; + } + final accept = await Get.dialog( + AlertDialog( + title: user == null + ? const Text("Create new user") + : const Text("Edit user"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: user != null + ? TextEditingController(text: user.username) + : null, + decoration: const InputDecoration( + labelText: "Username", + ), + readOnly: user != null, + onChanged: (value) => username.value = value, + ), + TextField( + decoration: const InputDecoration( + labelText: "Password", + ), + obscureText: true, + onChanged: (value) => password.value = value, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: user == null ? const Text("Create") : const Text("Save"), + ), + ], + ), + ); + + if (accept != true || username.isEmpty || password.isEmpty) return; + + try { + OkResponse? respData; + if (user == null) { + final resp = await ApiController.to.apiClient.createUser( + createUserDefinition: CreateUserDefinition( + username: username.value.trim(), + password: password.value.trim(), + ), + ); + respData = resp.data; + } else { + final resp = await ApiController.to.apiClient.updateUser( + userUpdateDefinition: UserUpdateDefinition( + userId: user.id, + password: password.value.trim(), + accessToken: user.accessToken, + ), + ); + respData = resp.data; + } + + if (respData == null) { + throw Exception("No data in response"); + } + + refreshData(); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to create user", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to create user", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to create user", + "$e", + ); + } + } + + Future createNewGroup() async { + final groupName = "".obs; + final groupDescription = "".obs; + final accept = await Get.dialog( + AlertDialog( + title: const Text("Create new group"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: const InputDecoration( + labelText: "Group name", + ), + onChanged: (value) => groupName.value = value, + ), + TextField( + decoration: const InputDecoration( + labelText: "Group description (optional)", + ), + onChanged: (value) => groupDescription.value = value, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text("Create"), + ), + ], + ), + ); + + if (accept != true || groupName.isEmpty) return; + + try { + final resp = await ApiController.to.apiClient.createItem( + tableName: "user_group", + itemDefinition: { + "name": groupName.trim(), + "description": groupDescription.trim() + }, + ); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + Get.snackbar( + "Group created", + "Group $groupName created", + ); + + refreshData(); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to create group", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to create group", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to create group", + "$e", + ); + } + } + + Future getGroupUsers(GroupDefinition group) async { + final resp = await ApiController.to.apiClient.getItemsFromTable( + tableName: "user_in_user_group", + itemsSelector: ItemsSelector( + fields: ["id", "user_id", "user_group_id"], + where: [ + ColumnConditionCompat( + column: "user_group_id", + operator_: ColumnConditionCompatOperator.eq, + value: group.id, + ), + ], + ), + ); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + final data = respData + .map( + (e) => UserInGroupDefinition( + id: e["id"], + userId: e["user_id"], + groupId: e["user_group_id"], + ), + ) + .map((e) => users.firstWhereOrNull((e1) => e1.id == e.userId)) + .whereType() + .toList(); + + _usersInGroups[group] = data; + } + + Future addUserToGroup(GroupDefinition group) async { + final groupUsers = _usersInGroups[group] ?? []; + final usersNotInGroup = + users.where((e) => groupUsers.contains(e) != true).toList(); + + final selectedUser = await Get.dialog( + AlertDialog( + title: const Text("Select user to add to group"), + content: SizedBox( + width: 400, + height: 400, + child: ListView.builder( + itemCount: usersNotInGroup.length, + itemBuilder: (context, index) { + final user = usersNotInGroup[index]; + return ListTile( + title: Text(user.username), + onTap: () => Get.back(result: user), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(result: null), + child: const Text("Cancel"), + ), + ], + ), + ); + + if (selectedUser == null) return; + + try { + final resp = await ApiController.to.apiClient.createItem( + tableName: "user_in_user_group", + itemDefinition: { + "user_id": selectedUser.id, + "user_group_id": group.id, + }, + ); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + Get.snackbar( + "User added to group", + "User ${selectedUser.username} added to group ${group.name}", + ); + + refreshData(); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to add user to group", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to add user to group", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to add user to group", + "$e", + ); + } + } + + Future removeUserFromGroup( + GroupDefinition group, + UserDefinition user, + ) async { + final accept = await Get.dialog( + AlertDialog( + title: const Text("Remove user from group"), + content: const Text( + "Are you sure you want to remove this user from the group?"), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text("Remove"), + ), + ], + ), + ); + + if (accept != true) return; + + try { + final resp = await ApiController.to.apiClient.deleteItemFromTable( + tableName: "user_in_user_group", + columnConditionCompat: [ + ColumnConditionCompat( + column: "user_group_id", + operator_: ColumnConditionCompatOperator.eq, + value: group.id, + ), + ColumnConditionCompat( + column: "user_id", + operator_: ColumnConditionCompatOperator.eq, + value: user.id, + ), + ], + ); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + Get.snackbar( + "User removed from group", + "User ${user.username} removed from group ${group.name}", + ); + + refreshData(); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to remove user from group", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to remove user from group", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to remove user from group", + "$e", + ); + } + } + + Future deleteGroup(GroupDefinition group) async { + final accept = await Get.dialog( + AlertDialog( + title: const Text("Delete group"), + content: const Text( + "Are you sure you want to delete this group? This action cannot be undone."), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text("Delete"), + ), + ], + ), + ); + + if (accept != true) return; + + try { + final resp1 = await ApiController.to.apiClient.deleteItemFromTable( + tableName: "user_in_user_group", + columnConditionCompat: [ + ColumnConditionCompat( + column: "user_group_id", + operator_: ColumnConditionCompatOperator.eq, + value: group.id, + ), + ], + ); + + if (resp1.data == null) { + throw Exception("Could not delete users from group"); + } + + final resp2 = await ApiController.to.apiClient.deleteItemFromTable( + tableName: "user_group", + columnConditionCompat: [ + ColumnConditionCompat( + column: "id", + operator_: ColumnConditionCompatOperator.eq, + value: group.id, + ), + ], + ); + + if (resp2.data == null) { + throw Exception("Could not delete group"); + } + + Get.snackbar( + "Group deleted", + "Group ${group.name} deleted", + ); + + refreshData(); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to delete group", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to delete group", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to delete group", + "$e", + ); + } + } + + Future editUser(UserDefinition user) async { + createNewUser(user: user); + } + + Future deleteUser(UserDefinition user) async { + final accept = await Get.dialog( + AlertDialog( + title: const Text("Delete user"), + content: const Text( + "Are you sure you want to delete this user? This action cannot be undone.\n" + "Note: This will not remove references to this user in other tables.", + ), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text("Delete"), + ), + ], + ), + ); + + if (accept != true) return; + + try { + final resp1 = await ApiController.to.apiClient.deleteItemFromTable( + tableName: "user_in_user_group", + columnConditionCompat: [ + ColumnConditionCompat( + column: "user_id", + operator_: ColumnConditionCompatOperator.eq, + value: user.id, + ), + ], + ); + + if (resp1.data == null) { + throw Exception("Could not delete users from group"); + } + + final resp2 = await ApiController.to.apiClient.removeUser( + userId: user.id, + ); + + if (resp2.data == null) { + throw Exception("Could not delete group"); + } + + Get.snackbar( + "User deleted", + "User ${user.username} deleted", + ); + + refreshData(); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to delete user", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to delete user", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to delete user", + "$e", + ); + } + } +} + +class UsersListPanel extends GetView { + const UsersListPanel({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Column( + children: [ + AppBar( + elevation: 0, + title: Obx( + () => DropdownButton( + items: const [ + DropdownMenuItem( + value: UserListPanelTab.users, + child: Text("Users"), + ), + DropdownMenuItem( + value: UserListPanelTab.groups, + child: Text("Groups"), + ), + ], + value: controller.currentTab, + onChanged: (value) { + controller.currentTab = value ?? UserListPanelTab.users; + }, + ), + ).marginAll(8), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () => + controller.currentTab == UserListPanelTab.users + ? controller.createNewUser() + : controller.createNewGroup(), + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => controller.refreshData(), + ), + ], + ), + Expanded( + child: Obx(() => controller.currentTab == UserListPanelTab.users + ? usersPanel + : groupsPanel), + ), + ], + ), + Obx( + () => Positioned( + right: 16, + bottom: 16, + child: controller.isLoading + ? const CircularProgressIndicator() + : const SizedBox(), + ), + ), + ], + ); + } + + Widget get usersPanel => Obx(() => + controller.users.isEmpty ? whenNoSomething("No users found") : cardList); + + Widget get groupsPanel => Obx(() => controller.groups.isEmpty + ? whenNoSomething("No groups found") + : groupsList); + + Widget whenNoSomething(message) => Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( - "No users found", + message, style: TextStyle( - color: Theme.of(context).disabledColor, + color: Get.theme.disabledColor, fontSize: 24, ), ), - const SizedBox(height: 4), - Text( - "Maybe create one? Or maybe you don't have permission to view them?", - style: TextStyle( - color: Theme.of(context).disabledColor, - fontSize: 14, - ), - ), - const SizedBox(height: 16), - ElevatedButton.icon( - onPressed: _createNewUser, - icon: const Icon(Icons.add), - label: const Text("Create user"), - ) ], ), ); - } - return GridView.builder( - shrinkWrap: true, - itemCount: users.length + 1, - itemBuilder: (ctx, i) => - i == 0 ? newUserCard : _buildUserCard(ctx, users[i - 1]), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, - childAspectRatio: 5 / 2, + Widget get cardList => ListView.builder( + shrinkWrap: true, + itemCount: controller.users.length, + itemBuilder: (context, index) => + _buildUserCard(controller.users[index]), + ); + + Widget get groupsList => ListView.builder( + shrinkWrap: true, + itemCount: controller.groups.length, + itemBuilder: (context, index) => + _buildGroupCard(controller.groups[index]), + ); + + Widget _buildUserCard(UserDefinition user) { + return Card( + clipBehavior: Clip.antiAlias, + child: ExpansionTile( + title: Text( + user.username, + maxLines: 1, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: Text( + "ID: ${user.id}", + style: const TextStyle( + fontSize: 12, + ), + ), + childrenPadding: const EdgeInsets.all(8), + children: [ + Row( + children: [ + ElevatedButton.icon( + onPressed: () => controller.editUser(user), + icon: const Icon(Icons.edit_attributes), + label: const Text("Edit user"), + ).paddingAll(8).expanded(), + Obx( + () { + final adminGroup = controller.usersInGroups.keys + .toList() + .firstWhereOrNull((element) => element.id == 2); + if (adminGroup == null) return const SizedBox(); + + final adminList = controller.usersInGroups[adminGroup]; + if (adminList == null) return const SizedBox(); + + final isAdmin = + adminList.any((element) => element.id == user.id); + if (isAdmin) return const SizedBox(); + + return ElevatedButton.icon( + onPressed: () => controller.deleteUser(user), + icon: const Icon(Icons.delete_forever), + label: const Text("Delete user"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + ), + ).paddingAll(8).expanded(); + }, + ), + ], + ), + ], ), ); } - Future _createNewUser() async { - var result = await showFlexibleBottomSheet( - minHeight: 1, - initHeight: 1, - maxHeight: 1, - context: context, - builder: (_, __, ___) => const CreateUserBottomSheet(), - anchors: [0, 0.5, 1], - isSafeArea: true, - isDismissible: false, + Widget _buildGroupCard(GroupDefinition group) { + return Card( + clipBehavior: Clip.antiAlias, + child: ExpansionTile( + title: Text( + group.name, + maxLines: 1, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + ), + leading: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (group.id != 1) + IconButton( + icon: const Icon(Icons.person_add), + onPressed: () => controller.addUserToGroup(group), + ), + if (group.id > 2) + IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: () => controller.deleteGroup(group), + ), + ], + ), + subtitle: Text( + group.description, + style: const TextStyle( + fontSize: 12, + ), + ), + children: [ + Obx( + () { + final groupData = controller.usersInGroups[group]; + if (group.id == 1) { + return const ListTile( + title: Text("No users can be added to this group"), + ); + } + if (groupData == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (groupData.isEmpty) { + return const ListTile( + title: Text("No users in group"), + ); + } + return ListView.builder( + shrinkWrap: true, + itemCount: groupData.length, + itemBuilder: (context, index) { + final user = controller.usersInGroups[group]![index]; + final isYou = user.accessToken == ApiController.to.token; + return ListTile( + title: Text(user.username), + subtitle: isYou ? const Text("You") : null, + trailing: isYou && + (group.id == 2 || + group.name.toLowerCase() == "admin") + ? null + : IconButton( + icon: const Icon(Icons.delete), + onPressed: () => controller.removeUserFromGroup( + group, + user, + ), + ), + ); + }, + ); + }, + ), + ], + ), ); - - if (result == null) { - return; - } - - final response = await apiClient.createUser( - widget.usersTable, result.username, result.password); - response.unfold((data) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("User created"), - ), - ); - _refreshUsers(); - }, (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error: $error"), - ), - ); - }); - } - - Future _openUser(UserModel user) async { - final result = await showFlexibleBottomSheet( - minHeight: 1, - initHeight: 1, - maxHeight: 1, - context: context, - builder: (_, __, ___) => CreateUserBottomSheet(existingUser: user), - anchors: [0, 0.5, 1], - isSafeArea: true, - isDismissible: false, - ); - - if (result == null) { - return; - } - - final response = await apiClient.updateItem(widget.usersTable, { - "username": result.username, - "password": result.password, - "access_token": result.accessToken, - }, { - "id": user.id, - }); - response.unfold((data) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("User updated"), - ), - ); - _refreshUsers(); - }, (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error: $error"), - ), - ); - }); - } - - Future _refreshUsers() async { - final result = await apiClient.getTableItems(widget.usersTable); - result.unfold((data) { - setState(() { - users.clear(); - users.addAll(data.map((e) => UserModel.fromJson(e))); - }); - }, (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error: $error"), - ), - ); - }); } } diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index d412533..d774e1d 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:animated_background/animated_background.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -8,9 +7,9 @@ import 'package:tuuli_api/tuuli_api.dart'; import 'package:tuuli_app/api_controller.dart'; class LoginPageController extends GetxController { - final _login = "".obs; - String get login => _login.value; - set login(String value) => _login.value = value; + final _username = "".obs; + String get username => _username.value; + set username(String value) => _username.value = value; final _password = "".obs; String get password => _password.value; @@ -20,18 +19,18 @@ class LoginPageController extends GetxController { bool get submitted => _submitted.value; set submitted(bool value) => _submitted.value = value; - bool get isFormValid => login.isNotEmpty && password.isNotEmpty; + bool get isFormValid => username.isNotEmpty && password.isNotEmpty; Future submitForm() async { submitted = true; if (isFormValid) { - final amb = AuthModelBuilder() - ..username = login - ..password = password; - try { - final resp = await ApiController.to.apiClient - .getAccessTokenApiGetAccessTokenPost(authModel: amb.build()); + final resp = await ApiController.to.apiClient.getAccessToken( + authModel: AuthModel( + username: username, + password: password, + ), + ); final respData = resp.data; if (resp.statusCode == 200 && respData != null) { @@ -105,9 +104,9 @@ class LoginPage extends GetView { enabled: !controller.submitted, decoration: const InputDecoration( labelText: 'Login', - hintText: 'Enter your login', + hintText: 'Enter your username', ), - onChanged: (value) => controller.login = value, + onChanged: (value) => controller.username = value, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your Login'; @@ -149,147 +148,3 @@ class LoginPage extends GetView { ); } } -/* -class LoginPage extends StatefulWidget { - const LoginPage({super.key}); - - @override - State createState() => _LoginPageState(); -} - -class _LoginPageState extends State with TickerProviderStateMixin { - final _formKey = GlobalKey(); - - final apiClient = Get.find(); - var submitted = false; - - final loginPageController = TextEditingController(); - final passwordController = TextEditingController(); - - @override - Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; - final formWidth = screenSize.width <= 600 ? screenSize.width : 300.0; - return Scaffold( - body: Stack( - children: [ - AnimatedBackground( - behaviour: RandomParticleBehaviour(), - vsync: this, - child: const SizedBox.square( - dimension: 0, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - LimitedBox( - maxWidth: formWidth, - child: Container( - color: Colors.black.withAlpha(100), - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - controller: loginPageController, - enabled: !submitted, - decoration: const InputDecoration( - labelText: 'Login', - hintText: 'Enter your login', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your Login'; - } - return null; - }, - ), - TextFormField( - controller: passwordController, - obscureText: true, - enabled: !submitted, - decoration: const InputDecoration( - labelText: 'Password', - hintText: 'Enter your password', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your password'; - } - return null; - }, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: submitted ? null : _submit, - child: const Text('Login'), - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ); - } - - Future _submit() async { - if (!_formKey.currentState!.validate()) { - return; - } - - setState(() { - submitted = true; - }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Trying to login...'), - ), - ); - - final response = await apiClient.login( - loginPageController.text.trim(), - passwordController.text.trim(), - ); - response.unfold((data) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Login successful'), - ), - ); - apiClient.setAccessToken(data.accessToken); - GetStorage() - .write("accessToken", data.accessToken) - .then((value) => GetStorage().save()) - .then((value) { - Timer(1.seconds, () { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - WidgetsBinding.instance.addPostFrameCallback((_) { - Get.offAllNamed("/home"); - }); - }); - }); - }, (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(error.toString()), - ), - ); - setState(() { - submitted = false; - }); - }); - } -} -*/ \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index bdceca6..e24b1d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214 + sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" url: "https://pub.dev" source: hosted - version: "58.0.0" + version: "59.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27 + sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.11.1" animated_background: dependency: "direct main" description: @@ -275,6 +275,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -343,10 +348,10 @@ packages: dependency: transitive description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" io: dependency: transitive description: @@ -371,6 +376,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + lint: + dependency: transitive + description: + name: lint + sha256: "4a539aa34ec5721a2c7574ae2ca0336738ea4adc2a34887d54b7596310b33c85" + url: "https://pub.dev" + source: hosted + version: "1.10.0" lints: dependency: transitive description: @@ -463,18 +476,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.25" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7" + sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_linux: dependency: transitive description: @@ -563,6 +576,62 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + url: "https://pub.dev" + source: hosted + version: "2.2.0" shelf: dependency: transitive description: @@ -632,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + styled_widget: + dependency: "direct main" + description: + name: styled_widget + sha256: "4d439802919b6ccf10d1488798656da8804633b03012682dd1c8ca70a084aa84" + url: "https://pub.dev" + source: hosted + version: "0.4.1" term_glyph: dependency: transitive description: @@ -656,6 +733,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tuuli_api: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "46d8acd4a54fd716a16f119e4296ee5583b8bc98" + url: "https://glab.nuark.xyz/nuark/tuuli_api.git" + source: git + version: "1.0.0" typed_data: dependency: transitive description: @@ -664,14 +750,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" - url: "https://pub.dev" - source: hosted - version: "3.0.7" vector_math: dependency: transitive description: @@ -700,10 +778,10 @@ packages: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d1c368d..754700c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,13 @@ dependencies: http: ^0.13.5 one_of_serializer: ^1.5.0 recase: ^4.1.0 - uuid: ^3.0.7 + shared_preferences: ^2.1.0 + styled_widget: ^0.4.1 + + tuuli_api: + git: + url: https://glab.nuark.xyz/nuark/tuuli_api.git + ref: master dev_dependencies: flutter_test: