374 lines
18 KiB
Dart
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|