568 lines
16 KiB
Dart
568 lines
16 KiB
Dart
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("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(),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => previewAsset(e),
|
|
child: const Text("View asset")),
|
|
],
|
|
).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",
|
|
);
|
|
}
|
|
}
|
|
|
|
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("Unsupported media type")
|
|
.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("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(
|
|
horizontalScrollController: controller.scrollController,
|
|
columns: const [
|
|
DataColumn2(label: Text(""), fixedWidth: 16),
|
|
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(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),
|
|
),
|
|
);
|
|
}
|