diff --git a/lib/pages/bottomsheets/create_table_item_bottomsheet.dart b/lib/pages/bottomsheets/create_table_item_bottomsheet.dart deleted file mode 100644 index 5fac541..0000000 --- a/lib/pages/bottomsheets/create_table_item_bottomsheet.dart +++ /dev/null @@ -1,199 +0,0 @@ -/*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'; -import 'package:tuuli_app/api/model/table_field_model.dart'; -import 'package:tuuli_app/api/model/tables_list_model.dart'; -import 'package:uuid/uuid.dart'; -import 'package:uuid/uuid_util.dart'; - -class CreateTableItemBottomSheet extends StatefulWidget { - final TableModel table; - final TableItemsData? existingItem; - - const CreateTableItemBottomSheet({ - super.key, - required this.table, - this.existingItem, - }); - - @override - State createState() => _CreateTableItemBottomSheetState(); -} - -class _CreateTableItemBottomSheetState - extends State { - final _formKey = GlobalKey(); - - final _values = {}; - - @override - void initState() { - super.initState(); - for (final field in widget.table.columns.where((e) => !e.isPrimary)) { - _values[field.fieldName] = null; - } - widget.existingItem?.forEach((key, value) { - _values[key] = value; - }); - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: FastForm( - formKey: _formKey, - children: [ - Text( - widget.existingItem == null ? "Create new item" : "Update item", - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - for (final field in widget.table.columns.where((e) => !e.isPrimary)) - Card( - margin: const EdgeInsets.all(8), - child: Container( - padding: const EdgeInsets.all(8), - child: _createFormField(field), - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("Cancel"), - ), - ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - Navigator.of(context).pop(_values); - return; - } - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please fill in all fields"), - ), - ); - }, - child: - Text(widget.existingItem == null ? "Create" : "Update"), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _createFormField(TableField field) { - switch (field.fieldType) { - case "serial": - case "bigint": - // ignore: no_duplicate_case_values - case "int": - return FastTextField( - name: field.fieldName, - labelText: field.fieldName, - validator: (value) { - if (value == null || - value.isEmpty || - double.tryParse(value) is! double) { - return "Please enter a value"; - } - return null; - }, - initialValue: (_values[field.fieldName] ?? "").toString(), - onChanged: (value) { - _values[field.fieldName] = int.tryParse(value ?? ""); - }, - ); - case "uuid": - return FastTextField( - name: field.fieldName, - labelText: field.fieldName, - validator: (value) { - if (value == null || - value.isEmpty || - !Uuid.isValidUUID(fromString: value)) { - return "Please enter a value"; - } - return null; - }, - initialValue: (_values[field.fieldName] ?? "").toString(), - onChanged: (value) { - _values[field.fieldName] = - Uuid.isValidUUID(fromString: value ?? "") ? value : null; - }, - ); - case "str": - return FastTextField( - name: field.fieldName, - labelText: field.fieldName, - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter a value"; - } - return null; - }, - initialValue: _values[field.fieldName], - onChanged: (value) { - _values[field.fieldName] = value; - }, - ); - case "bool": - return FastCheckbox( - name: field.fieldName, - labelText: field.fieldName, - titleText: field.fieldName, - initialValue: _values[field.fieldName], - onChanged: (value) { - _values[field.fieldName] = value; - }, - ); - case "date": - // ignore: no_duplicate_case_values - case "datetime": - return FastCalendar( - name: field.fieldName, - firstDate: DateTime.now().subtract(const Duration(days: 365 * 200)), - lastDate: DateTime.now().add(const Duration(days: 365 * 200)), - initialValue: DateTime.tryParse(_values[field.fieldName] ?? ""), - validator: (value) { - if (value == null) { - return "Please enter a value"; - } - return null; - }, - onChanged: (value) { - _values[field.fieldName] = value; - }, - ); - case "float": - return FastTextField( - name: field.fieldName, - labelText: field.fieldName, - validator: (value) { - if (value == null || - value.isEmpty || - double.tryParse(value) is! double) { - return "Please enter a value"; - } - return null; - }, - initialValue: (_values[field.fieldName] ?? "").toString(), - onChanged: (value) { - _values[field.fieldName] = double.tryParse(value ?? ""); - }, - ); - default: - return const Text("Unknown field type"); - } - } -} -*/ \ No newline at end of file diff --git a/lib/pages/bottomsheets/create_user_bottomsheet.dart b/lib/pages/bottomsheets/create_user_bottomsheet.dart deleted file mode 100644 index 72c8223..0000000 --- a/lib/pages/bottomsheets/create_user_bottomsheet.dart +++ /dev/null @@ -1,218 +0,0 @@ -/*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'; -import 'package:tuuli_app/api/model/table_field_model.dart'; -import 'package:tuuli_app/api/model/tables_list_model.dart'; -import 'package:tuuli_app/api/model/user_model.dart'; -import 'package:tuuli_app/utils.dart'; -import 'package:uuid/uuid.dart'; -import 'package:uuid/uuid_util.dart'; - -class CreateUserResult { - final String username; - final String password; - - CreateUserResult(this.username, this.password); -} - -class UpdateUserResult { - final String username; - final String password; - final String accessToken; - - UpdateUserResult(this.username, this.password, this.accessToken); -} - -// TODO: Add a way to change user's group -class CreateUserBottomSheet extends StatefulWidget { - final UserModel? existingUser; - - const CreateUserBottomSheet({ - super.key, - this.existingUser, - }); - - @override - State createState() => _CreateUserBottomSheetState(); -} - -class _CreateUserBottomSheetState extends State { - final _formKey = GlobalKey(); - - String? newUsername; - String? newPassword; - String? newAccessToken; - - bool obscurePassword = true; - bool obscureToken = true; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: FastForm( - formKey: _formKey, - children: [ - Text( - widget.existingUser == null ? "Create new user" : "Update user", - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Card( - margin: const EdgeInsets.all(8), - child: Container( - padding: const EdgeInsets.all(8), - child: FastTextField( - name: "Username", - labelText: "Username", - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter a value"; - } - return null; - }, - readOnly: widget.existingUser != null, - initialValue: widget.existingUser?.username, - onChanged: (value) { - newUsername = value; - }, - ), - ), - ), - Card( - margin: const EdgeInsets.all(8), - child: Container( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Flexible( - child: FastTextField( - name: "Password", - labelText: "Password", - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter a value"; - } - return null; - }, - obscureText: obscurePassword, - onChanged: (value) { - newPassword = value; - }, - ), - ), - IconButton( - icon: Icon( - obscurePassword - ? Icons.visibility_off - : Icons.visibility, - ), - onPressed: () { - setState(() { - obscurePassword = !obscurePassword; - }); - }, - ), - ], - ), - ), - ), - if (widget.existingUser != null) - Card( - margin: const EdgeInsets.all(8), - child: Container( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Flexible( - child: FastTextField( - name: "Access token", - labelText: "Access token", - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter a value"; - } - return null; - }, - obscureText: obscureToken, - initialValue: newAccessToken ?? - widget.existingUser?.accessToken, - readOnly: true, - onChanged: (value) { - newAccessToken = value; - }, - ), - ), - IconButton( - icon: const Icon(Icons.shuffle), - onPressed: () { - setState(() { - newAccessToken = randomHexString(64); - }); - }, - ), - IconButton( - icon: Icon( - obscurePassword - ? Icons.visibility_off - : Icons.visibility, - ), - onPressed: () { - setState(() { - obscureToken = !obscureToken; - }); - }, - ), - ], - ), - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("Cancel"), - ), - ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - Navigator.of(context).pop(widget.existingUser == null - ? CreateUserResult( - newUsername!, - newPassword!, - ) - : UpdateUserResult( - newUsername!, - newPassword!, - newAccessToken!, - )); - return; - } - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please fill in all fields"), - ), - ); - }, - child: - Text(widget.existingUser == null ? "Create" : "Update"), - ), - ], - ), - ], - ), - ), - ); - } -} -*/ \ No newline at end of file diff --git a/lib/pages/bottomsheets/edit_table_bottomsheet.dart b/lib/pages/bottomsheets/edit_table_bottomsheet.dart deleted file mode 100644 index 064e38f..0000000 --- a/lib/pages/bottomsheets/edit_table_bottomsheet.dart +++ /dev/null @@ -1,243 +0,0 @@ -/*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'; -import 'package:tuuli_app/api/model/tables_list_model.dart'; -import 'package:tuuli_app/widgets/table_field_widget.dart'; - -class EditTableBottomSheetResult { - final String tableName; - final List fields; - - EditTableBottomSheetResult(this.tableName, this.fields); -} - -class EditTableBottomSheet extends StatefulWidget { - final TableModel? table; - - const EditTableBottomSheet({super.key, this.table}); - - @override - State createState() => _EditTableBottomSheetState(); -} - -class _EditTableBottomSheetState extends State { - var tableName = "".obs; - - late final List fields; - - final newFieldName = TextEditingController(); - String? newFieldType; - var newFieldPrimary = false; - var newFieldUnique = false; - - @override - void initState() { - super.initState(); - fields = widget.table?.columns ?? []; - if (widget.table != null) { - tableName.value = widget.table!.tableName; - } - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Obx( - () => Text( - tableName.isEmpty - ? "Edit table" - : "Edit table \"${tableName.value.pascalCase}\"", - style: Theme.of(context).textTheme.headlineSmall, - ), - ), - const Spacer(), - IconButton( - onPressed: Get.back, - icon: const Icon(Icons.cancel), - ) - ], - ), - if (widget.table == null) const Divider(), - if (widget.table == null) - TextFormField( - decoration: const InputDecoration( - labelText: 'Table name', - hintText: 'Enter table name', - ), - readOnly: widget.table != null, - maxLength: 15, - onChanged: (value) => tableName.value = value, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter table name'; - } - return null; - }, - ), - const Divider(), - Text( - "Fields", - style: Theme.of(context).textTheme.titleLarge, - ), - if (fields.isEmpty) const Text("No fields"), - ...fields - .map((e) => TableFieldWidget( - field: e, - onRemove: () => _removeColumn(e.fieldName), - )) - .toList(growable: false), - if (widget.table == null) const SizedBox(width: 16), - if (widget.table == null) - Card( - child: Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.all(8), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text("Add new field"), - const SizedBox(height: 8), - Row( - children: [ - Flexible( - child: TextFormField( - controller: newFieldName, - decoration: const InputDecoration( - labelText: 'Column name', - hintText: 'Enter column name', - border: OutlineInputBorder(), - ), - ), - ), - const SizedBox(width: 8), - Flexible( - child: DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Column type', - hintText: 'Choose column type', - border: OutlineInputBorder(), - ), - items: possibleFieldTypes.keys - .map((e) => DropdownMenuItem( - value: e, - child: Text(e.pascalCase), - )) - .toList(growable: false), - value: newFieldType, - onChanged: (value) => newFieldType = value, - ), - ), - const SizedBox(width: 16), - ToggleButtons( - isSelected: [ - newFieldPrimary, - newFieldUnique, - !newFieldPrimary && !newFieldUnique - ], - onPressed: (index) { - setState(() { - newFieldPrimary = index == 0; - newFieldUnique = index == 1; - }); - }, - children: const [ - Text("Primary"), - Text("Unique"), - Text("Normal"), - ], - ), - const SizedBox(width: 16), - ElevatedButton( - onPressed: _addNewField, - child: const Text("Add field"), - ), - ], - ), - ], - ), - ), - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: _saveTable, - child: const Text("Save table"), - ), - ], - ), - ), - ); - } - - void _addNewField() { - if (newFieldType == null) { - return; - } - - final fieldName = newFieldName.text; - if (fieldName.isEmpty || - fields.any((element) => element.fieldName == fieldName)) { - Get.defaultDialog( - title: "Error", - middleText: "Field name is empty or already exists", - ); - return; - } - - final field = TableField.parseTableField( - "$fieldName:$newFieldType${newFieldUnique ? ":unique" : ""}${newFieldPrimary ? ":primary" : ""}", - ); - - if (field.isPrimary && !field.canBePrimary()) { - Get.defaultDialog( - title: "Error", - middleText: "Field type \"${field.fieldType}\" can't be primary", - ); - return; - } - - setState(() { - newFieldName.clear(); - newFieldType = null; - newFieldPrimary = false; - newFieldUnique = false; - fields.add(field); - }); - } - - void _saveTable() { - if (tableName.isEmpty) { - Get.defaultDialog( - title: "Error", - middleText: "Table name is empty", - ); - return; - } - - if (fields.isEmpty) { - Get.defaultDialog( - title: "Error", - middleText: "Table must have at least one field", - ); - return; - } - - Get.back(result: EditTableBottomSheetResult(tableName.value, fields)); - } - - void _removeColumn(String name) { - setState(() { - fields.removeWhere((element) => element.fieldName == name); - }); - } -} -*/ \ No newline at end of file diff --git a/lib/pages/bottomsheets/open_table_bottomsheet.dart b/lib/pages/bottomsheets/open_table_bottomsheet.dart deleted file mode 100644 index 045c043..0000000 --- a/lib/pages/bottomsheets/open_table_bottomsheet.dart +++ /dev/null @@ -1,258 +0,0 @@ -/*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'; -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/pages/bottomsheets/create_table_item_bottomsheet.dart'; - -class OpenTableBottomSheet extends StatefulWidget { - final TableModel table; - - const OpenTableBottomSheet({super.key, required this.table}); - - @override - State createState() => _OpenTableBottomSheetState(); -} - -class _OpenTableBottomSheetState extends State { - final apiClient = Get.find(); - final tableItems = TableItemsDataList.empty(growable: true); - - @override - void initState() { - super.initState(); - - _refreshTableData(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Text( - widget.table.tableName.pascalCase, - style: Theme.of(context).textTheme.headlineSmall, - ), - const Spacer(), - IconButton( - onPressed: _addNewItem, - icon: const Icon(Icons.add), - ), - IconButton( - onPressed: _refreshTableData, - icon: const Icon(Icons.refresh), - ), - IconButton( - onPressed: _dropTable, - icon: const Icon(Icons.delete), - ), - IconButton( - onPressed: Get.back, - icon: const Icon(Icons.cancel), - ), - ], - ), - const Divider(), - Expanded( - child: DataTable2( - columnSpacing: 12, - horizontalMargin: 12, - headingRowColor: - MaterialStateColor.resolveWith((states) => Colors.black), - columns: [ - ...widget.table.columns.map((e) => DataColumn( - label: Text(e.fieldName), - )), - const DataColumn(label: Text("Actions")), - ], - rows: tableItems - .map((e) => DataRow(cells: [ - for (int i = 0; i < widget.table.columns.length; i++) - DataCell( - Text(e[widget.table.columns[i].fieldName] - ?.toString() ?? - "null"), - ), - DataCell( - Row( - children: [ - IconButton( - onPressed: () => _updateExistingItem(e), - icon: const Icon(Icons.edit), - ), - IconButton( - onPressed: () => _deleteItem(e), - icon: const Icon(Icons.delete), - ), - ], - ), - ), - ])) - .toList(growable: false), - empty: const Center(child: Text("No data")), - ), - ), - ], - ), - ), - ); - } - - Future _dropTable() async { - final really = await Get.defaultDialog( - title: "Drop table", - middleText: - "Are you sure you want to drop this table \"${widget.table.tableName}\"?", - textConfirm: "Drop", - onConfirm: () => Get.back(result: true), - onCancel: () {}, - barrierDismissible: false, - ); - - if (really != true) { - return; - } - - final result = await apiClient.dropTable(widget.table.tableName); - result.unfold((data) { - Get.back(); - }, (error) { - Get.snackbar("Error", error.toString()); - }); - } - - Future _refreshTableData() async { - final result = await apiClient.getTableItems(widget.table); - result.unfold((data) { - setState(() { - tableItems.clear(); - tableItems.addAll(data); - }); - }, (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error: $error"), - ), - ); - }); - } - - Future _addNewItem() async { - final newItem = await showFlexibleBottomSheet>( - minHeight: 1, - initHeight: 1, - maxHeight: 1, - context: context, - builder: (_, __, ___) => CreateTableItemBottomSheet(table: widget.table), - anchors: [0, 0.5, 1], - isSafeArea: true, - isDismissible: false, - ); - - if (newItem == null) { - return; - } - - final result = await apiClient.insertItem(widget.table, newItem); - result.unfold((data) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Item added"), - ), - ); - _refreshTableData(); - }, (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error: $error"), - ), - ); - }); - } - - Future _updateExistingItem(TableItemsData oldItem) async { - final newItem = await showFlexibleBottomSheet>( - minHeight: 1, - initHeight: 1, - maxHeight: 1, - context: context, - builder: (_, __, ___) => CreateTableItemBottomSheet( - table: widget.table, - existingItem: Map.fromEntries(widget.table.columns - .where((el) => !el.isPrimary) - .map((el) => MapEntry(el.fieldName, oldItem[el.fieldName]))), - ), - anchors: [0, 0.5, 1], - isSafeArea: true, - isDismissible: false, - ); - - if (newItem == null) { - return; - } - - final result = await apiClient.updateItem(widget.table, newItem, oldItem); - result.unfold((data) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Item added"), - ), - ); - _refreshTableData(); - }, (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error: $error"), - ), - ); - }); - } - - Future _deleteItem(TableItemsData e) async { - final really = await Get.defaultDialog( - title: "Delete item", - middleText: "Are you sure you want to delete this item?", - textConfirm: "Delete", - onConfirm: () => Get.back(result: true), - onCancel: () {}, - barrierDismissible: false, - ); - - if (really != true) { - return; - } - - final result = await apiClient.deleteItem(widget.table, e); - result.unfold((data) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Item deleted"), - ), - ); - _refreshTableData(); - }, (error) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error: $error"), - ), - ); - }); - } -} -*/ \ No newline at end of file diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index a6f43d7..88f3adb 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:tuuli_app/api_controller.dart'; import 'package:tuuli_app/c.dart'; +import 'package:tuuli_app/pages/home_panels/assets_panel.dart'; import 'package:tuuli_app/pages/home_panels/none_panel.dart'; import 'package:tuuli_app/pages/home_panels/settings_panel.dart'; import 'package:tuuli_app/pages/home_panels/tables_list_panel.dart'; @@ -12,6 +13,7 @@ enum PageType { none, tables, users, + assets, settings, } @@ -26,6 +28,7 @@ class HomePageController extends GetxController { PageType.none: "Home", PageType.tables: "Tables", PageType.users: "Users", + PageType.assets: "Assets", PageType.settings: "Settings", }; @@ -41,6 +44,10 @@ class HomePageController extends GetxController { () => UserListPanelController(), fenix: true, ); + Get.lazyPut( + () => AssetsPagePanelController(), + fenix: true, + ); } Future logout() async { @@ -49,6 +56,7 @@ class HomePageController extends GetxController { await Future.wait([ Get.delete(), Get.delete(), + Get.delete(), Get.delete(), ]); @@ -100,6 +108,16 @@ class HomePage extends GetView { selected: controller.currentPage == PageType.users, ), ), + Obx( + () => ListTile( + leading: const Icon(Icons.dataset_outlined), + title: const Text("Assets"), + onTap: () { + controller.currentPage = PageType.assets; + }, + selected: controller.currentPage == PageType.assets, + ), + ), Obx( () => ListTile( leading: const Icon(Icons.settings), @@ -154,6 +172,8 @@ class HomePage extends GetView { return const TablesListPanel(); case PageType.users: return const UsersListPanel(); + case PageType.assets: + return const AssetsPagePanel(); case PageType.settings: return const SettingsPanel(); case PageType.none: diff --git a/lib/pages/home_panels/assets_panel.dart b/lib/pages/home_panels/assets_panel.dart new file mode 100644 index 0000000..dbd2d70 --- /dev/null +++ b/lib/pages/home_panels/assets_panel.dart @@ -0,0 +1,518 @@ +import 'package:data_table_2/data_table_2.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_fast_forms/flutter_fast_forms.dart'; +import 'package:get/get.dart' hide MultipartFile; +import 'package:styled_widget/styled_widget.dart'; +import 'package:super_drag_and_drop/super_drag_and_drop.dart'; +import 'package:tuuli_api/tuuli_api.dart'; +import 'package:tuuli_app/api_controller.dart'; +import 'package:http_parser/http_parser.dart'; + +class AssetsPagePanelController extends GetxController { + @override + void onInit() { + super.onInit(); + + refreshData(); + } + + final _isLoading = false.obs; + bool get isLoading => _isLoading.value; + + final _assetsList = [].obs; + List get assetsList => _assetsList.toList(); + + final _tagsList = [].obs; + List get tagsList => _tagsList.toList(); + + final _filterTags = [].obs; + List get filterTags => _filterTags.toList(); + set filterTags(List value) => _filterTags.value = value; + + Future refreshData() async { + _isLoading.value = true; + + await Future.wait([ + refreshAssets(), + refreshTag(), + ]); + + _isLoading.value = false; + } + + Future refreshAssets() async { + try { + final resp = await ApiController.to.apiClient.getAssets(); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + _assetsList.clear(); + _assetsList.addAll(respData); + } 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 refreshTag() async { + try { + final resp = await ApiController.to.apiClient.getAssetsTags(); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + _tagsList.clear(); + _tagsList.addAll(respData); + } 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 openUploadDialog() async { + final file = await Get.dialog( + AlertDialog( + content: DropRegion( + formats: Formats.standardFormats, + hitTestBehavior: HitTestBehavior.opaque, + onDropOver: (event) { + if (event.session.items.length == 1 && + event.session.allowedOperations.contains(DropOperation.copy)) { + return DropOperation.copy; + } + return DropOperation.none; + }, + onPerformDrop: (event) async { + final item = event.session.items.first; + final reader = item.dataReader; + if (reader == null) return; + + reader.getFile( + null, + (dataReader) async { + final data = await dataReader.readAll(); + + final fileName = + dataReader.fileName ?? await reader.getSuggestedName(); + const mimeType = "application/octet-stream"; + + final file = MultipartFile.fromBytes( + data, + filename: fileName, + contentType: MediaType.parse(mimeType), + ); + Get.back(result: file); + }, + onError: (value) { + Get.snackbar("Error", value.toString()); + }, + ); + }, + child: const Text("Drop file here") + .paddingAll(8) + .fittedBox() + .constrained(height: 200, width: 200), + ).border(all: 2, color: Colors.lightBlueAccent), + actions: [ + TextButton( + onPressed: () => Get.back(result: null), + child: const Text("Cancel"), + ) + ], + ), + ); + + if (file == null) return; + + final sendProgress = 0.obs; + final receiveProgress = 0.obs; + final req = ApiController.to.apiClient.putAsset( + asset: file, + onSendProgress: (count, _) { + sendProgress.value = count; + }, + onReceiveProgress: (count, _) { + receiveProgress.value = count; + }, + ); + Get.dialog( + SizedBox( + width: 32, + height: 32, + child: Obx( + () => CircularProgressIndicator( + value: + sendProgress.value == 0 ? null : sendProgress.value.toDouble(), + ), + ), + ).paddingAll(32).card().center(), + barrierDismissible: false, + ); + + try { + final resp = await req; + + final 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 get users", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to get users", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to get users", + "$e", + ); + } finally { + Get.back(); + } + } + + Future editAsset(Asset e) async { + final description = e.description.obs; + final tags = e.tags.split(",").obs; + + final confirm = await Get.dialog( + AlertDialog( + title: const Text("Edit asset"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: TextEditingController(text: description.value), + onChanged: (value) => description.value = value, + decoration: const InputDecoration( + labelText: "Description", + ), + ), + TextField( + controller: TextEditingController(text: tags.join(", ")), + onChanged: (value) => tags.value = + value.split(",").map((e) => e.trim()).toList(growable: false), + decoration: const InputDecoration( + labelText: "Tags", + ), + ), + const SizedBox(height: 16), + Obx( + () => Wrap( + children: tags + .where((p0) => p0.isNotEmpty) + .map((tag) => Chip(label: Text(tag))) + .toList(growable: false), + ).paddingAll(8).card(color: Colors.blueGrey.shade200).expanded(), + ), + ], + ).constrained(width: Get.width * 0.5, height: Get.width * 0.5), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text("Confirm"), + ), + ], + ), + ); + + if (confirm != true) return; + + try { + final resp = + await ApiController.to.apiClient.updateAssetDescriptionAndTags( + assetId: e.id, + assetDescription: description.value, + assetTags: tags, + ); + + final 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 get users", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to get users", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to get users", + "$e", + ); + } + } + + Future removeAsset(Asset e) async { + final checkReferences = false.obs; + final deleteReferencing = false.obs; + + final confirm = await Get.dialog( + AlertDialog( + title: const Text("Remove asset"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("You are about to remove an asset."), + Obx( + () => CheckboxListTile( + value: checkReferences.value, + onChanged: (value) => checkReferences.value = value ?? false, + title: const Text("Check references"), + ), + ), + Obx( + () => CheckboxListTile( + value: deleteReferencing.value, + onChanged: (value) => deleteReferencing.value = value ?? false, + title: const Text("Delete referencing"), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text("Remove"), + ), + ], + ), + ); + + if (confirm != true) return; + + try { + final resp = await ApiController.to.apiClient.removeAsset( + assetId: e.id, + checkReferences: checkReferences.value, + deleteReferencing: deleteReferencing.value, + ); + + final 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 get users", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to get users", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to get users", + "$e", + ); + } + } +} + +class AssetsPagePanel extends GetView { + const AssetsPagePanel({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Column( + children: [ + AppBar( + elevation: 0, + title: Row( + children: [ + const Text("Tags:"), + Obx( + () => FastChipsInput( + name: "FastChipsInput", + options: controller.tagsList, + crossAxisAlignment: WrapCrossAlignment.center, + decoration: const InputDecoration( + border: InputBorder.none, + ), + chipBuilder: (chipValue, chipIndex, field) => InputChip( + label: Text(chipValue), + isEnabled: field.widget.enabled, + onDeleted: () => field + .didChange([...field.value!]..remove(chipValue)), + selected: chipIndex == field.selectedChipIndex, + showCheckmark: false, + backgroundColor: Colors.green.shade200, + ), + onChanged: (value) => controller.filterTags = value ?? [], + ), + ).expanded(), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () => controller.openUploadDialog(), + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => controller.refreshData(), + ), + ], + ), + Expanded( + child: assetsPanel, + ), + ], + ), + Obx( + () => Positioned( + right: 16, + bottom: 16, + child: controller.isLoading + ? const CircularProgressIndicator() + : const SizedBox(), + ), + ), + ], + ); + } + + Widget get assetsPanel => Obx( + () => DataTable2( + columns: const [ + DataColumn2(label: Text("ID"), size: ColumnSize.S, numeric: true), + DataColumn2(label: Text("Filename"), size: ColumnSize.M), + DataColumn2(label: Text("Description"), size: ColumnSize.L), + DataColumn2(label: Text("File ID"), size: ColumnSize.M), + DataColumn2(label: Text("Tags"), size: ColumnSize.L), + DataColumn2(label: Text("Actions")), + ], + empty: const Text("No assets found"), + rows: controller.assetsList + .where((element) { + if (controller.filterTags.isEmpty) return true; + + return element.tags + .split(",") + .any(controller.filterTags.contains); + }) + .map((e) => DataRow2( + cells: [ + DataCell(Text(e.id.toString())), + DataCell(Tooltip( + message: e.name, + child: Text( + e.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + )), + DataCell(Tooltip( + message: e.description, + child: Text( + e.description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + )), + DataCell(Tooltip( + message: e.fid, + child: Text( + e.fid, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + )), + DataCell(Tooltip( + message: e.tags, + child: Text( + e.tags, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + )), + DataCell(Row(children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => controller.editAsset(e), + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => controller.removeAsset(e), + ), + ])), + ], + )) + .toList(growable: false), + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index e24b1d4..bb78cf0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -254,6 +254,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_chips_input: + dependency: "direct main" + description: + name: flutter_chips_input + sha256: "9828e45e75f268ff51a08e8d05848776b0a4d8327867d2b347a733030bafe64f" + url: "https://pub.dev" + source: hosted + version: "2.0.0" flutter_fast_forms: dependency: "direct main" description: @@ -360,6 +368,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + irondash_engine_context: + dependency: transitive + description: + name: irondash_engine_context + sha256: "086fbbcaef07b821b304b0371e472c687715f326219b69b18dc5c962dab09b55" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + irondash_message_channel: + dependency: transitive + description: + name: irondash_message_channel + sha256: "081ff9631a2c6782a47ef4fdf9c97206053af1bb174e2a25851692b04f3bc126" + url: "https://pub.dev" + source: hosted + version: "0.1.1" js: dependency: transitive description: @@ -476,10 +500,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a + sha256: "3e58242edc02624f2c712e3f8bea88e0e341c4ae1abd3a6ff661318a3aefd829" url: "https://pub.dev" source: hosted - version: "2.0.25" + version: "2.0.26" path_provider_foundation: dependency: transitive description: @@ -508,10 +532,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" platform: dependency: transitive description: @@ -588,10 +612,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46" + sha256: "5d7b3bd0400bdd0c03e59a3d3d5314651141a145b58196cd9018b12a2adc0c1b" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" shared_preferences_foundation: dependency: transitive description: @@ -709,6 +733,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + super_clipboard: + dependency: transitive + description: + name: super_clipboard + sha256: "7628464b63fd18df486e87a989ecfd73565f7b01a15ee47a2448283931c3d14d" + url: "https://pub.dev" + source: hosted + version: "0.3.0+2" + super_drag_and_drop: + dependency: "direct main" + description: + name: super_drag_and_drop + sha256: "9a31045fcb264bcfe57e5001b24b68cb8d4880d8d4ecfd594dd4c11a92b1ca56" + url: "https://pub.dev" + source: hosted + version: "0.3.0+2" + super_native_extensions: + dependency: transitive + description: + name: super_native_extensions + sha256: "18ad4c367cea763654d458d9e9aec8c0c00c6b1d542c4f746c94a223e8e4bd0c" + url: "https://pub.dev" + source: hosted + version: "0.3.0+2" term_glyph: dependency: transitive description: @@ -738,10 +786,10 @@ packages: description: path: "." ref: master - resolved-ref: "46d8acd4a54fd716a16f119e4296ee5583b8bc98" + resolved-ref: ab60426db27a2441107e529f527df0e502dae104 url: "https://glab.nuark.xyz/nuark/tuuli_api.git" source: git - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: @@ -750,6 +798,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: @@ -778,10 +834,10 @@ packages: dependency: transitive description: name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "4.1.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 754700c..62182bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: built_value: ^8.4.4 data_table_2: ^2.4.2 dio: ^5.1.1 + flutter_chips_input: ^2.0.0 flutter_fast_forms: ^10.0.0 get: ^4.6.5 get_storage: ^2.1.1 @@ -24,7 +25,7 @@ dependencies: recase: ^4.1.0 shared_preferences: ^2.1.0 styled_widget: ^0.4.1 - + super_drag_and_drop: ^0.3.0+2 tuuli_api: git: url: https://glab.nuark.xyz/nuark/tuuli_api.git