Assets page main functionality

This commit is contained in:
Andrew 2023-04-28 02:12:15 +07:00
parent 61f3184f85
commit 64a435ccb2
8 changed files with 606 additions and 929 deletions

View file

@ -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<StatefulWidget> createState() => _CreateTableItemBottomSheetState();
}
class _CreateTableItemBottomSheetState
extends State<CreateTableItemBottomSheet> {
final _formKey = GlobalKey<FormState>();
final _values = <String, dynamic>{};
@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");
}
}
}
*/

View file

@ -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<StatefulWidget> createState() => _CreateUserBottomSheetState();
}
class _CreateUserBottomSheetState extends State<CreateUserBottomSheet> {
final _formKey = GlobalKey<FormState>();
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"),
),
],
),
],
),
),
);
}
}
*/

View file

@ -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<TableField> fields;
EditTableBottomSheetResult(this.tableName, this.fields);
}
class EditTableBottomSheet extends StatefulWidget {
final TableModel? table;
const EditTableBottomSheet({super.key, this.table});
@override
State<StatefulWidget> createState() => _EditTableBottomSheetState();
}
class _EditTableBottomSheetState extends State<EditTableBottomSheet> {
var tableName = "".obs;
late final List<TableField> 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);
});
}
}
*/

View file

@ -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<StatefulWidget> createState() => _OpenTableBottomSheetState();
}
class _OpenTableBottomSheetState extends State<OpenTableBottomSheet> {
final apiClient = Get.find<ApiClient>();
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<void> _dropTable() async {
final really = await Get.defaultDialog<bool>(
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<void> _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<void> _addNewItem() async {
final newItem = await showFlexibleBottomSheet<Map<String, dynamic>>(
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<void> _updateExistingItem(TableItemsData oldItem) async {
final newItem = await showFlexibleBottomSheet<Map<String, dynamic>>(
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<void> _deleteItem(TableItemsData e) async {
final really = await Get.defaultDialog<bool>(
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"),
),
);
});
}
}
*/

View file

@ -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>(
() => AssetsPagePanelController(),
fenix: true,
);
}
Future<void> logout() async {
@ -49,6 +56,7 @@ class HomePageController extends GetxController {
await Future.wait([
Get.delete<TablesListPanelController>(),
Get.delete<UserListPanelController>(),
Get.delete<AssetsPagePanelController>(),
Get.delete<HomePageController>(),
]);
@ -100,6 +108,16 @@ class HomePage extends GetView<HomePageController> {
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<HomePageController> {
return const TablesListPanel();
case PageType.users:
return const UsersListPanel();
case PageType.assets:
return const AssetsPagePanel();
case PageType.settings:
return const SettingsPanel();
case PageType.none:

View file

@ -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 = <Asset>[].obs;
List<Asset> get assetsList => _assetsList.toList();
final _tagsList = <String>[].obs;
List<String> get tagsList => _tagsList.toList();
final _filterTags = <String>[].obs;
List<String> get filterTags => _filterTags.toList();
set filterTags(List<String> value) => _filterTags.value = value;
Future<void> refreshData() async {
_isLoading.value = true;
await Future.wait([
refreshAssets(),
refreshTag(),
]);
_isLoading.value = false;
}
Future<void> 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<void> 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<void> openUploadDialog() async {
final file = await Get.dialog<MultipartFile>(
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<void> editAsset(Asset e) async {
final description = e.description.obs;
final tags = e.tags.split(",").obs;
final confirm = await Get.dialog<bool>(
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<void> removeAsset(Asset e) async {
final checkReferences = false.obs;
final deleteReferencing = false.obs;
final confirm = await Get.dialog<bool>(
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<AssetsPagePanelController> {
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),
),
);
}

View file

@ -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:

View file

@ -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