OpenContacts/lib/widgets/inventory/inventory_browser.dart
2023-06-17 17:36:52 +02:00

222 lines
11 KiB
Dart

import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/clients/inventory_client.dart';
import 'package:contacts_plus_plus/models/inventory/neos_path.dart';
import 'package:contacts_plus_plus/models/records/record.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/inventory/object_inventory_tile.dart';
import 'package:contacts_plus_plus/widgets/inventory/path_inventory_tile.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart';
class InventoryBrowser extends StatefulWidget {
const InventoryBrowser({super.key});
@override
State<StatefulWidget> createState() => _InventoryBrowserState();
}
class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepAliveClientMixin {
static const Duration _refreshLimit = Duration(seconds: 60);
final Set<String> _selectedIds = {};
Timer? _refreshLimiter;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final iClient = Provider.of<InventoryClient>(context, listen: false);
if (iClient.directoryFuture == null) {
iClient.loadInventoryRoot();
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return ChangeNotifierProvider.value(
value: Provider.of<InventoryClient>(context),
child: Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
return FutureBuilder<NeosDirectory>(
future: iClient.directoryFuture,
builder: (context, snapshot) {
final currentDir = snapshot.data;
return WillPopScope(
onWillPop: () async {
// Allow pop when at root or not loaded
if (currentDir?.isRoot ?? true) {
return true;
}
iClient.navigateUp();
return false;
},
child: RefreshIndicator(
onRefresh: () async {
if (_refreshLimiter?.isActive ?? false) return;
try {
//TODO: Reload path
_refreshLimiter = Timer(_refreshLimit, () {});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e")));
}
},
child: Builder(
builder: (context) {
if (snapshot.hasError) {
FlutterError.reportError(
FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
return DefaultErrorWidget(
message: snapshot.error.toString(),
onRetry: () {
iClient.loadInventoryRoot();
},
);
}
final directory = snapshot.data;
final records = directory?.records ?? [];
records.sort((a, b) => a.name.compareTo(b.name));
final paths = records
.where((element) =>
element.recordType == RecordType.link || element.recordType == RecordType.directory)
.toList();
final objects = records
.where((element) =>
element.recordType != RecordType.link && element.recordType != RecordType.directory)
.toList();
final pathSegments = directory?.absolutePathSegments ?? [];
return Stack(
children: [
ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
child: Wrap(
children: pathSegments
.mapIndexed(
(idx, segment) => Row(
mainAxisSize: MainAxisSize.min,
children: [
if (idx != 0) const Icon(Icons.chevron_right),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: idx == pathSegments.length - 1
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
iClient.navigateUp(times: pathSegments.length - 1 - idx);
},
child: Text(segment),
),
),
],
),
)
.toList(),
)),
GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: paths.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 256,
childAspectRatio: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8),
itemBuilder: (context, index) {
final record = paths[index];
return PathInventoryTile(
record: record,
onPressed: () {
iClient.navigateTo(record);
},
);
},
),
const SizedBox(
height: 8,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: objects.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 256,
childAspectRatio: 1,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, index) {
final record = objects[index];
return ObjectInventoryTile(
record: record,
selected: _selectedIds.contains(record.id),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PhotoView(
minScale: PhotoViewComputedScale.contained,
imageProvider:
CachedNetworkImageProvider(Aux.neosDbToHttp(record.thumbnailUri)),
heroAttributes: PhotoViewHeroAttributes(tag: record.id),
),
),
);
},
onLongPress: () async {
setState(() {
if (_selectedIds.contains(record.id)) {
_selectedIds.remove(record.id);
} else {
_selectedIds.add(record.id);
}
});
},
);
},
),
),
],
),
Align(
alignment: Alignment.topCenter,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: snapshot.connectionState == ConnectionState.waiting ? const LinearProgressIndicator() : null,
),
),
Align(
alignment: Alignment.topCenter,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: snapshot.connectionState == ConnectionState.waiting ? Container(
width: double.infinity,
height: double.infinity,
color: Colors.black38,
) : null,
),
)
],
);
},
),
),
);
});
}),
);
}
@override
bool get wantKeepAlive => true;
}