253 lines
12 KiB
Dart
253 lines
12 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:photo_view/photo_view.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:OpenContacts/auxiliary.dart';
|
|
import 'package:OpenContacts/clients/inventory_client.dart';
|
|
import 'package:OpenContacts/models/inventory/resonite_directory.dart';
|
|
import 'package:OpenContacts/models/records/record.dart';
|
|
import 'package:OpenContacts/widgets/default_error_widget.dart';
|
|
import 'package:OpenContacts/widgets/inventory/object_inventory_tile.dart';
|
|
import 'package:OpenContacts/widgets/inventory/path_inventory_tile.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);
|
|
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 Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
|
return FutureBuilder<ResoniteDirectory>(
|
|
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 {
|
|
await iClient.reloadCurrentDirectory();
|
|
_refreshLimiter = Timer(_refreshLimit, () {});
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
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();
|
|
iClient.forceNotify();
|
|
},
|
|
);
|
|
}
|
|
final directory = snapshot.data;
|
|
final records = directory?.records ?? [];
|
|
records.sort(
|
|
(Record a, Record b) => iClient.sortMode.sortFunction(a, b, reverse: iClient.sortReverse),
|
|
);
|
|
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: 6, 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: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
|
),
|
|
child: Text(
|
|
segment,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
softWrap: true,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
),
|
|
GridView.builder(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
shrinkWrap: true,
|
|
itemCount: paths.length,
|
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
maxCrossAxisExtent: 256,
|
|
childAspectRatio: 3.5,
|
|
crossAxisSpacing: 0,
|
|
mainAxisSpacing: 0,
|
|
),
|
|
itemBuilder: (context, index) {
|
|
final record = paths[index];
|
|
return PathInventoryTile(
|
|
record: record,
|
|
selected: iClient.isRecordSelected(record),
|
|
onLongPress: () async {
|
|
iClient.toggleRecordSelected(record);
|
|
},
|
|
onTap: iClient.isAnyRecordSelected
|
|
? () {
|
|
iClient.toggleRecordSelected(record);
|
|
}
|
|
: () async {
|
|
try {
|
|
await iClient.navigateTo(record);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text("Failed to open directory: $e")),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
},
|
|
),
|
|
GridView.builder(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
shrinkWrap: true,
|
|
itemCount: objects.length,
|
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
maxCrossAxisExtent: 256,
|
|
childAspectRatio: 1,
|
|
crossAxisSpacing: 0,
|
|
mainAxisSpacing: 0,
|
|
),
|
|
itemBuilder: (context, index) {
|
|
final record = objects[index];
|
|
return ObjectInventoryTile(
|
|
record: record,
|
|
selected: iClient.isRecordSelected(record),
|
|
onTap: iClient.isAnyRecordSelected
|
|
? () async {
|
|
iClient.toggleRecordSelected(record);
|
|
}
|
|
: () async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(record.formattedName.toString()),
|
|
),
|
|
body: Center (
|
|
child: CachedNetworkImage(
|
|
imageUrl: (Aux.resdbToHttp(record.thumbnailUri)),
|
|
placeholder: (context, url) => const CircularProgressIndicator(),
|
|
errorWidget: (context, url, error) => const Icon(Icons.error),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
onLongPress: () async {
|
|
iClient.toggleRecordSelected(record);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
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;
|
|
}
|