Assets page main functionality
This commit is contained in:
parent
61f3184f85
commit
64a435ccb2
8 changed files with 606 additions and 929 deletions
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
@ -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"),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
518
lib/pages/home_panels/assets_panel.dart
Normal file
518
lib/pages/home_panels/assets_panel.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue