OpenContacts/lib/widgets/inventory/inventory_browser_app_bar.dart
2024-07-15 00:23:04 -04:00

374 lines
18 KiB
Dart

import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:background_downloader/background_downloader.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:OpenContacts/auxiliary.dart';
import 'package:OpenContacts/clients/inventory_client.dart';
import 'package:share_plus/share_plus.dart';
class InventoryBrowserAppBar extends StatefulWidget {
const InventoryBrowserAppBar({super.key});
@override
State<InventoryBrowserAppBar> createState() => _InventoryBrowserAppBarState();
}
class _InventoryBrowserAppBarState extends State<InventoryBrowserAppBar> {
final Future<Directory> _tempDirectoryFuture = getTemporaryDirectory();
@pragma('vm:entry-point')
static void downloadCallback(TaskUpdate event) {
final id = event.task.taskId;
final status = event is TaskStatusUpdate ? event.status : null;
final progress = event is TaskProgressUpdate ? event.progress : null;
final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port');
send?.send([id, status, progress]);
}
@override
Widget build(BuildContext context) {
return Consumer<InventoryClient>(
builder: (BuildContext context, InventoryClient iClient, Widget? _) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: !iClient.isAnyRecordSelected
? AppBar(
key: const ValueKey("default-appbar"),
title: const Text("Inventory"),
actions: [
PopupMenuButton(
icon: const Icon(Icons.swap_vert),
onSelected: (value) {
iClient.sortReverse = value;
},
itemBuilder: (context) {
return [
PopupMenuItem(
value: false,
child: Row(
children: [
Icon(
Icons.arrow_upward,
color: iClient.sortReverse == false
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(
width: 8,
),
Text(
"Ascending",
style: TextStyle(
color: iClient.sortReverse == false
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
PopupMenuItem(
value: true,
child: Row(
children: [
Icon(Icons.arrow_downward,
color: iClient.sortReverse == true
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
const SizedBox(
width: 8,
),
Text(
"Descending",
style: TextStyle(
color: iClient.sortReverse == true
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
],
),
)
];
},
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: PopupMenuButton(
icon: const Icon(Icons.sort),
onSelected: (value) {
iClient.sortMode = value;
},
itemBuilder: (context) {
return SortMode.values
.map(
(e) => PopupMenuItem(
value: e,
child: Row(
children: [
Icon(
e.icon,
color: iClient.sortMode == e
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(
width: 8,
),
Text(
toBeginningOfSentenceCase(e.name) ?? e.name,
style: TextStyle(
color: iClient.sortMode == e
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
)
],
),
),
)
.toList();
},
),
),
],
)
: AppBar(
key: const ValueKey("selection-appbar"),
title: Text("${iClient.selectedRecordCount} Selected"),
leading: IconButton(
onPressed: () {
iClient.clearSelectedRecords();
},
icon: const Icon(Icons.close),
),
actions: [
if (iClient.selectedRecordCount == 1 &&
(iClient.selectedRecords.firstOrNull?.isLink == true ||
iClient.selectedRecords.firstOrNull?.isItem == true))
IconButton(
onPressed: () {
Share.share(iClient.selectedRecords.first.assetUri);
},
icon: const Icon(Icons.share),
),
if (iClient.onlyFilesSelected)
IconButton(
onPressed: () async {
final selectedRecords = iClient.selectedRecords;
final assetUris = selectedRecords.map((record) => record.assetUri).toList();
final thumbUris = selectedRecords.map((record) => record.thumbnailUri).toList();
final selectedUris = await showDialog<List<String>>(
context: context,
builder: (context) {
return AlertDialog(
icon: const Icon(Icons.download),
title: const Text("Download what?"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
const SizedBox(
height: 8,
),
TextButton.icon(
onPressed: () {
Navigator.of(context).pop(assetUris);
},
icon: const Icon(Icons.data_object),
label: Text(
"Asset${iClient.selectedRecordCount != 1 ? "s" : ""} (${assetUris.map((e) => extension(e)).toList().unique().join(", ")})",
),
),
TextButton.icon(
onPressed: () {
Navigator.of(context).pop(thumbUris);
},
icon: const Icon(Icons.image),
label: Text(
"Thumbnail${iClient.selectedRecordCount != 1 ? "s" : ""} (${thumbUris.map((e) => extension(e)).toList().unique().join(", ")})",
),
),
],
),
);
},
);
if (selectedUris == null) return;
final directory = await FilePicker.platform.getDirectoryPath(dialogTitle: "Download to...");
if (directory == null) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Selection aborted."),
),
);
}
return;
}
if (directory == "/") {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Selected directory is invalid"),
),
);
}
return;
}
for (var record in selectedRecords) {
final uri = selectedUris == thumbUris ? record.thumbnailUri : record.assetUri;
final filename =
"${record.id.split("-")[1]}-${record.formattedName.toString()}${extension(uri)}";
try {
final downloadTask = DownloadTask(
url: Aux.resdbToHttp(uri),
allowPause: true,
baseDirectory: BaseDirectory.temporary,
filename: filename,
updates: Updates.statusAndProgress,
);
final downloadStatus = await FileDownloader().download(downloadTask);
if (downloadStatus.status == TaskStatus.complete) {
final tempDirectory = await _tempDirectoryFuture;
final file = File(
"${tempDirectory.path}/${record.id.split("-")[1]}-${record.formattedName.toString()}${extension(uri)}");
if (await file.exists()) {
final newFile = File("$directory/$filename");
await file.copy(newFile.absolute.path);
await file.delete();
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Downloaded ${record.formattedName.toString()}"),
),
);
} else {
throw downloadStatus.exception ?? "Unknown Error";
}
}
} catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Failed to download '${record.formattedName.toString()}':\n$e",
),
),
);
}
}
}
iClient.clearSelectedRecords();
},
icon: const Icon(Icons.download),
),
const SizedBox(
width: 4,
),
IconButton(
onPressed: () async {
var loading = false;
await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
icon: const Icon(Icons.delete),
title: Text(iClient.selectedRecordCount == 1
? "Really delete this Record?"
: "Really delete ${iClient.selectedRecordCount} Records?"),
content: const Text("This action cannot be undone!"),
actionsAlignment: MainAxisAlignment.spaceBetween,
actions: [
TextButton(
onPressed: loading
? null
: () {
Navigator.of(context).pop(false);
},
child: const Text("Cancel"),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (loading)
const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(
width: 4,
),
TextButton(
onPressed: loading
? null
: () async {
setState(() {
loading = true;
});
try {
await iClient.deleteSelectedRecords();
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Failed to delete one or more records: $e"),
),
);
}
setState(() {
loading = false;
});
}
if (context.mounted) {
Navigator.of(context).pop(true);
}
iClient.reloadCurrentDirectory();
},
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
child: const Text("Delete"),
),
],
),
],
);
},
);
},
);
},
icon: const Icon(Icons.delete),
),
const SizedBox(
width: 4,
),
],
),
);
},
);
}
}