231 lines
6.5 KiB
Dart
231 lines
6.5 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import 'package:collection/collection.dart';
|
||
|
||
import 'package:pshared/models/permissions/bound/storable.dart';
|
||
import 'package:pshared/provider/resource.dart';
|
||
import 'package:pshared/service/template.dart';
|
||
import 'package:pshared/utils/exception.dart';
|
||
|
||
|
||
List<T> mergeLists<T>({
|
||
required List<T> lhs,
|
||
required List<T> rhs,
|
||
required Comparable Function(T) getKey, // Extracts ID dynamically
|
||
required int Function(T, T) compare,
|
||
required T Function(T, T) merge,
|
||
}) {
|
||
final result = <T>[];
|
||
final map = {for (var item in lhs) getKey(item): item};
|
||
|
||
for (var updated in rhs) {
|
||
final key = getKey(updated);
|
||
map[key] = merge(map[key] ?? updated, updated);
|
||
}
|
||
|
||
result.addAll(map.values);
|
||
result.sort(compare);
|
||
return result;
|
||
}
|
||
|
||
/// A generic provider that wraps a [BasicService] instance
|
||
/// to manage state (loading, error, data) without re‑implementing service logic.
|
||
class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier {
|
||
final BasicService<T> service;
|
||
bool _isLoaded = false;
|
||
|
||
Resource<List<T>> _resource = Resource(data: []);
|
||
Resource<List<T>> get resource => _resource;
|
||
|
||
List<T> get items => List.unmodifiable(_resource.data ?? []);
|
||
bool get isLoading => _resource.isLoading;
|
||
Object? get error => _resource.error;
|
||
bool get isReady => (error == null) && _isLoaded;
|
||
|
||
bool get isCurrentSet => _currentObjectRef != null;
|
||
String? _currentObjectRef; // Stores the currently selected project ref
|
||
T? get currentObject => _resource.data?.firstWhereOrNull(
|
||
(object) => object.id == _currentObjectRef,
|
||
);
|
||
|
||
T? getItemByRef(String id) => items.firstWhereOrNull((item) => item.id == id);
|
||
|
||
GenericProvider({required this.service});
|
||
|
||
|
||
bool Function(T)? _filterPredicate;
|
||
|
||
List<T> get filteredItems => _filterPredicate != null ? items.where(_filterPredicate!).toList() : items;
|
||
|
||
void setFilterPredicate(bool Function(T)? predicate) {
|
||
_filterPredicate = predicate;
|
||
notifyListeners();
|
||
}
|
||
|
||
void clearFilter() => setFilterPredicate(null);
|
||
|
||
void _setResource(Resource<List<T>> newResource) {
|
||
_resource = newResource;
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<List<T>> loadFuture(Future<List<T>> future) async {
|
||
_setResource(_resource.copyWith(isLoading: true));
|
||
try {
|
||
final list = await future;
|
||
_isLoaded = true;
|
||
_setResource(Resource(data: list, isLoading: false));
|
||
return list;
|
||
} catch (e) {
|
||
_setResource(
|
||
_resource.copyWith(isLoading: false, error: toException(e)),
|
||
);
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> load(
|
||
String organizationRef,
|
||
String? parentRef, {
|
||
int? limit,
|
||
int? offset,
|
||
bool? Function()? fetchArchived,
|
||
}) async {
|
||
if (parentRef != null) {
|
||
await loadFuture(
|
||
service.list(
|
||
organizationRef,
|
||
parentRef,
|
||
limit: limit,
|
||
offset: offset,
|
||
fetchArchived: fetchArchived == null ? null : fetchArchived(),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> loadItem(String itemRef) async {
|
||
await loadFuture((() async => [await service.get(itemRef)])());
|
||
}
|
||
|
||
List<T> merge(List<T> rhs) => mergeLists<T>(
|
||
lhs: items,
|
||
rhs: rhs,
|
||
getKey: (item) => item.id, // Key extractor
|
||
compare: (a, b) => a.id.compareTo(b.id), // Sorting logic
|
||
merge: (existing, updated) => updated, // Replace with the updated version
|
||
);
|
||
|
||
Future<T> get(String objectRef) async {
|
||
_setResource(_resource.copyWith(isLoading: true));
|
||
try {
|
||
final item = await service.get(objectRef);
|
||
_setResource(Resource(data: merge([item]), isLoading: false));
|
||
return item;
|
||
} catch (e) {
|
||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<T> createObject(String organizationRef, Map<String, dynamic> request) async {
|
||
_setResource(_resource.copyWith(isLoading: true));
|
||
try {
|
||
final newObject = await service.create(organizationRef, request);
|
||
_setResource(Resource(data: [...items, ...newObject], isLoading: false));
|
||
return newObject.first;
|
||
} catch (e) {
|
||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> update(Map<String, dynamic> request) async {
|
||
_setResource(_resource.copyWith(isLoading: true));
|
||
try {
|
||
final list = await service.update(request);
|
||
_setResource(Resource(data: merge(list), isLoading: false));
|
||
} catch (e) {
|
||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> delete(String objectRef, {Map<String, dynamic>? request}) async {
|
||
_setResource(_resource.copyWith(isLoading: true));
|
||
|
||
try {
|
||
await service.delete(objectRef, request: request);
|
||
if (_currentObjectRef == objectRef) {
|
||
_currentObjectRef = null;
|
||
}
|
||
|
||
_setResource(Resource(
|
||
data: _resource.data?.where((p) => p.id != objectRef).toList(),
|
||
isLoading: false,
|
||
));
|
||
} catch (e) {
|
||
_setResource(Resource(data: _resource.data, isLoading: false, error: toException(e)));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> toggleArchived(T item, bool currentState, {bool? cascade}) => setArchived(
|
||
organizationRef: item.organizationRef,
|
||
objectRef: item.id,
|
||
newIsArchived: !currentState,
|
||
cascade: cascade ?? true,
|
||
);
|
||
|
||
Future<void> setArchived({
|
||
required String organizationRef,
|
||
required String objectRef,
|
||
required bool newIsArchived,
|
||
bool? cascade,
|
||
}) async {
|
||
_setResource(_resource.copyWith(isLoading: true));
|
||
|
||
try {
|
||
await service.archive(
|
||
organizationRef: organizationRef,
|
||
objectRef: objectRef,
|
||
newIsArchived: newIsArchived,
|
||
cascade: cascade,
|
||
);
|
||
if (_currentObjectRef == objectRef) {
|
||
_currentObjectRef = null;
|
||
}
|
||
|
||
_setResource(Resource(
|
||
data: _resource.data?.where((p) => p.id != objectRef).toList(),
|
||
isLoading: false,
|
||
));
|
||
} catch (e) {
|
||
_setResource(Resource(data: _resource.data, isLoading: false, error: toException(e)));
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
bool setCurrentObject(String? objectRef) {
|
||
if (_currentObjectRef == objectRef) {
|
||
// No change, skip notification
|
||
return true;
|
||
}
|
||
|
||
if (objectRef == null) {
|
||
_currentObjectRef = null;
|
||
notifyListeners();
|
||
return true;
|
||
}
|
||
|
||
if (_resource.data?.any((p) => p.id == objectRef) ?? false) {
|
||
_currentObjectRef = objectRef;
|
||
notifyListeners();
|
||
return true;
|
||
}
|
||
|
||
return false; // Object not found
|
||
}
|
||
|
||
}
|