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), ), ); }