tuuli_app/lib/pages/home_panels/assets_panel.dart

569 lines
17 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = <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("В ответе нет данных");
}
_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<void> 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<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("Ошибка", 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<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("Изменение ресурса"),
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)).paddingAll(2))
.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<void> removeAsset(Asset e) async {
final checkReferences = false.obs;
final deleteReferencing = false.obs;
final confirm = await Get.dialog<bool>(
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<AssetsPagePanelController> {
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),
),
);
}