import 'package:data_table_2/data_table_2.dart'; import 'package:dio/dio.dart'; import 'package:file_icon/file_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_fast_forms/flutter_fast_forms.dart'; import 'package:get/get.dart' hide MultipartFile; import 'package:photo_view/photo_view.dart'; 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'; import 'package:tuuli_app/widgets/audio_player_widget.dart'; class AssetsPagePanelController extends GetxController { @override void onInit() { super.onInit(); refreshData(); } final scrollController = ScrollController(); 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("В ответе нет данных"); } _assetsList.clear(); _assetsList.addAll(respData); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка получения ресурсов", "${respData['error']}", ); } else { Get.snackbar( "Ошибка получения ресурсов", "$e", ); } } catch (e) { Get.snackbar( "Ошибка получения ресурсов", "$e", ); } } Future refreshTag() async { try { final resp = await ApiController.to.apiClient.getAssetsTags(); final respData = resp.data; if (respData == null) { throw Exception("В ответе нет данных"); } _tagsList.clear(); _tagsList.addAll(respData); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка получения тегов", "${respData['error']}", ); } else { Get.snackbar( "Ошибка получения тегов", "$e", ); } } catch (e) { Get.snackbar( "Ошибка получения тегов", "$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("Ошибка", value.toString()); }, ); }, child: const Text("Перенеите файл сюда") .paddingAll(8) .fittedBox() .constrained(height: 200, width: 200), ).border(all: 2, color: Colors.lightBlueAccent), actions: [ TextButton( onPressed: () => Get.back(result: null), child: const Text("Отменить"), ) ], ), ); 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("В ответе нет данных"); } refreshData(); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка загрузки ресурса", "${respData['error']}", ); } else { Get.snackbar( "Ошибка загрузки ресурса", "$e", ); } } catch (e) { Get.snackbar( "Ошибка загрузки ресурса", "$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("Изменение ресурса"), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: TextEditingController(text: description.value), onChanged: (value) => description.value = value, decoration: const InputDecoration( labelText: "Описание", ), ), TextField( controller: TextEditingController(text: tags.join(", ")), onChanged: (value) => tags.value = value.split(",").map((e) => e.trim()).toList(growable: false), decoration: const InputDecoration( labelText: "Теги", ), ), 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(), ), ElevatedButton( onPressed: () => previewAsset(e), child: const Text("Предпросмотр")), ], ).constrained(width: Get.width * 0.5, height: Get.width * 0.5), actions: [ TextButton( onPressed: () => Get.back(result: false), child: const Text("Отменить"), ), TextButton( onPressed: () => Get.back(result: true), child: const Text("Подтвердить"), ), ], ), ); 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("В ответе нет данных"); } refreshData(); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка изменения ресурса", "${respData['error']}", ); } else { Get.snackbar( "Ошибка изменения ресурса", "$e", ); } } catch (e) { Get.snackbar( "Ошибка изменения ресурса", "$e", ); } } Future removeAsset(Asset e) async { final checkReferences = false.obs; final deleteReferencing = false.obs; final confirm = await Get.dialog( AlertDialog( title: const Text("Удаление ресурса"), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( "Вы планируете удалить ресурс. Это действие нельзя отменить."), Obx( () => CheckboxListTile( value: checkReferences.value, onChanged: (value) => checkReferences.value = value ?? false, title: const Text("Проверить ссылки"), ), ), Obx( () => CheckboxListTile( value: deleteReferencing.value, onChanged: (value) => deleteReferencing.value = value ?? false, title: const Text("Удалить ссылающиеся сущности"), ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(result: false), child: const Text("Отменить"), ), TextButton( onPressed: () => Get.back(result: true), child: const Text("Удалить"), ), ], ), ); 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("В ответе нет данных"); } refreshData(); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка удаления ресурса", "${respData['error']}", ); } else { Get.snackbar( "Ошибка удаления ресурса", "$e", ); } } catch (e) { Get.snackbar( "Ошибка удаления ресурса", "$e", ); } } void previewAsset(Asset e) { Get.dialog( Stack( children: [ if (e.mime.split("/").first == "image") PhotoView( imageProvider: NetworkImage("${ApiController.to.endPoint}/assets/${e.fid}"), enablePanAlways: true, ) else if (e.mime.split("/").first == "audio") AudioPlayerWidget.create( title: e.name, url: "${ApiController.to.endPoint}/assets/${e.fid}", ) else const Text("На данный момент этот тип не поддерживает предпросмотр") .fontSize(16) .paddingAll(8) .card() .center(), Positioned( top: 8, right: 8, child: Material( color: Colors.transparent, child: IconButton( onPressed: () => Get.back(), icon: const Icon(Icons.close), ), ), ), ], ), ); } } 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("Теги:"), 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( horizontalScrollController: controller.scrollController, columns: const [ DataColumn2(label: Text(""), fixedWidth: 16), DataColumn2(label: Text("Имя файла"), size: ColumnSize.M), DataColumn2(label: Text("Описание"), size: ColumnSize.L), DataColumn2(label: Text("ID файла"), size: ColumnSize.M), DataColumn2(label: Text("Теги"), size: ColumnSize.L), DataColumn2(label: Text("Действия")), ], empty: const Text("Ресурсы не найдены"), rows: controller.assetsList .where((element) { if (controller.filterTags.isEmpty) return true; return element.tags .split(",") .any(controller.filterTags.contains); }) .map((e) => DataRow2( cells: [ DataCell(FileIcon(e.name)), 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.preview), onPressed: () => controller.previewAsset(e), ), IconButton( icon: const Icon(Icons.edit), onPressed: () => controller.editAsset(e), ), IconButton( icon: const Icon(Icons.delete), onPressed: () => controller.removeAsset(e), ), ])), ], )) .toList(growable: false), ), ); }