diff --git a/client/lib/ui/explorer/resource_details_row.dart b/client/lib/ui/explorer/resource_details_row.dart index 523027da..5c67e50f 100644 --- a/client/lib/ui/explorer/resource_details_row.dart +++ b/client/lib/ui/explorer/resource_details_row.dart @@ -12,27 +12,27 @@ import 'package:provider/provider.dart'; import '../../libphylum/explorer/explorer_controller.dart'; -enum _SyncState { +enum SyncState { none(Icons.block), waiting(Icons.cloud_queue), error(Icons.sync_problem), syncing(Icons.sync); - const _SyncState(this.icon); + const SyncState(this.icon); final IconData icon; - factory _SyncState.fromStatus(ActionStatus status) { + factory SyncState.fromStatus(ActionStatus status) { if (status is ActionStatusReady || status is ActionStatusWaiting) { - return _SyncState.waiting; + return SyncState.waiting; } if (status is ActionStatusUploading) { - return _SyncState.syncing; + return SyncState.syncing; } if (status is ActionStatusError) { - return _SyncState.error; + return SyncState.error; } - return _SyncState.none; + return SyncState.none; } } @@ -47,10 +47,10 @@ class ResourceDetailsRow extends StatelessWidget { final showBorder = context.select((state) => state.focusId == resource.id && state.showFocus); final highlight = context.select((state) => state.isSelected(resource.id)); final dim = context.select((state) => state.isSelected(resource.id) && state.dragging); - final sync = context.select((state) { - return state.actions.fold(_SyncState.none, (acc, action) { + final sync = context.select((state) { + return state.actions.fold(SyncState.none, (acc, action) { if (action is ResourceAction && action.resourceId == resource.id) { - final actionState = _SyncState.fromStatus(action.status); + final actionState = SyncState.fromStatus(action.status); if (actionState.index > acc.index) return actionState; } return acc; @@ -117,7 +117,7 @@ class ResourceDetailsRow extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (sync != _SyncState.none) + if (sync != SyncState.none) Padding( padding: const EdgeInsets.only(right: 6.0), child: Icon(sync.icon, size: 14), diff --git a/client/lib/ui/explorer/resource_info_view.dart b/client/lib/ui/explorer/resource_info_view.dart index f011fd74..c653e216 100644 --- a/client/lib/ui/explorer/resource_info_view.dart +++ b/client/lib/ui/explorer/resource_info_view.dart @@ -1,14 +1,19 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:offtheline/offtheline.dart'; +import 'package:phylum/libphylum/actions/action_resource.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/db/resource_helpers.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/libphylum/explorer/explorer_controller.dart'; +import 'package:phylum/libphylum/phylum_api_types.dart'; +import 'package:phylum/ui/explorer/resource_details_row.dart'; import 'package:phylum/util/file_size.dart'; import 'package:phylum/util/permissions.dart'; import 'package:phylum/util/time.dart'; import 'package:provider/provider.dart'; +import 'package:state_notifier/state_notifier.dart'; import 'resource_icon_extension.dart'; @@ -47,7 +52,7 @@ class ReactiveResourceInfoView extends StatelessWidget { class ResourceInfoView extends StatelessWidget { final Resource resource; - const ResourceInfoView({super.key, required this.resource}); + ResourceInfoView({required this.resource}) : super(key: ValueKey(resource.id)); @override Widget build(BuildContext context) { @@ -59,6 +64,7 @@ class ResourceInfoView extends StatelessWidget { ), child: Column( children: [ + PendingActionsTile(resourceId: resource.id), ListTile( title: const Text('Type'), subtitle: Text(resource.dir ? 'Folder' : resource.contentType), @@ -93,10 +99,75 @@ class ResourceInfoView extends StatelessWidget { } } +class PendingActionsTile extends StatefulWidget { + final String resourceId; + + const PendingActionsTile({super.key, required this.resourceId}); + + @override + State createState() => _PendingActionsTileState(); +} + +class _PendingActionsTileState extends State { + RemoveListener? _removeListener; + final Map _listeners = {}; + final Map _statusMap = {}; + + @override + void initState() { + super.initState(); + _removeListener = context.read().addListener((state) { + final actions = + state.actions.where((action) => action is ResourceAction && action.resourceId == widget.resourceId); + for (final action in actions) { + if (!_listeners.containsKey(action)) { + _listeners[action] = action.statusNotifier.addListener((status) { + if (_statusMap[action].runtimeType != status.runtimeType) { + if (status is ActionStatusDone) { + final l = _listeners.remove(action); + _statusMap.remove(action); + if (l != null) { + Future.microtask(() => l()); + } + } else { + _statusMap[action] = status; + } + if (mounted) { + setState(() {}); + } + } + }, fireImmediately: true); + } + } + }, fireImmediately: true); + } + + @override + void dispose() { + _removeListener?.call(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + for (final e in _statusMap.entries) + ListTile( + visualDensity: VisualDensity.compact, + dense: true, + leading: Icon(SyncState.fromStatus(e.value).icon), + title: Text(e.key.description, maxLines: 1), + ), + ], + ); + } +} + class PermissionsTile extends StatefulWidget { final String resourceId; - PermissionsTile({required this.resourceId}) : super(key: ValueKey(resourceId)); + const PermissionsTile({super.key, required this.resourceId}); @override State createState() => _PermissionsTileState(); @@ -105,8 +176,8 @@ class PermissionsTile extends StatefulWidget { class _PermissionsTileState extends State { Permission? myPermission; Map? otherPermissions; - StreamSubscription>? sub; + @override void initState() { super.initState();