Assets page main functionality
This commit is contained in:
parent
61f3184f85
commit
64a435ccb2
8 changed files with 606 additions and 929 deletions
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