tuuli_app/lib/pages/home_panels/assets_panel.dart

518 lines
15 KiB
Dart

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 assets",
"${respData['error']}",
);
} else {
Get.snackbar(
"Error trying to get assets",
"$e",
);
}
} catch (e) {
Get.snackbar(
"Error trying to get assets",
"$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 tags",
"${respData['error']}",
);
} else {
Get.snackbar(
"Error trying to get tags",
"$e",
);
}
} catch (e) {
Get.snackbar(
"Error trying to get tags",
"$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 put asset",
"${respData['error']}",
);
} else {
Get.snackbar(
"Error trying to put asset",
"$e",
);
}
} catch (e) {
Get.snackbar(
"Error trying to put asset",
"$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 edit asset",
"${respData['error']}",
);
} else {
Get.snackbar(
"Error trying to edit asset",
"$e",
);
}
} catch (e) {
Get.snackbar(
"Error trying to edit asset",
"$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 remove asset",
"${respData['error']}",
);
} else {
Get.snackbar(
"Error trying to remove asset",
"$e",
);
}
} catch (e) {
Get.snackbar(
"Error trying to remove asset",
"$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),
),
);
}