import "dart:async"; import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:logging/logging.dart"; import "package:photos/core/constants.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/events/files_updated_event.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/events/tab_changed_event.dart"; import "package:photos/models/search/search_result.dart"; import "package:photos/models/search/search_types.dart"; import "package:photos/utils/standalone/debouncer.dart"; class AllSectionsExamplesProvider extends StatefulWidget { final Widget child; const AllSectionsExamplesProvider({super.key, required this.child}); @override State createState() => _AllSectionsExamplesProviderState(); } class _AllSectionsExamplesProviderState extends State { //Some section results in [allSectionsExamplesFuture] can be out of sync //with what is displayed on UI. This happens when some section is //independently listening to some set of events and is rebuilt. Sections //can listen to a list of events and rebuild (see sectionUpdateEvents() //in search_types.dart) and new results will not reflect in //[allSectionsExamplesFuture] unless reloadAllSections() is called. Future>> allSectionsExamplesFuture = Future.value([]); late StreamSubscription _filesUpdatedEvent; late StreamSubscription _onPeopleChangedEvent; late StreamSubscription _tabChangeEvent; bool hasPendingUpdate = false; bool isOnSearchTab = false; bool _firstLoadInProgressOrComplete = false; final _logger = Logger("AllSectionsExamplesProvider"); final _debouncer = Debouncer( const Duration(seconds: 3), executionInterval: const Duration(seconds: 12), leading: true, ); @override void initState() { super.initState(); //add all common events for all search sections to reload to here. _filesUpdatedEvent = Bus.instance.on().listen((event) { onDataUpdate(); }); _onPeopleChangedEvent = Bus.instance.on().listen((event) { onDataUpdate(); }); _tabChangeEvent = Bus.instance.on().listen((event) { if (event.source == TabChangedEventSource.pageView && event.selectedIndex == 3) { isOnSearchTab = true; if (hasPendingUpdate) { hasPendingUpdate = false; reloadAllSections(); } } else { isOnSearchTab = false; } }); Future.delayed(const Duration(seconds: 3), () { if (!_firstLoadInProgressOrComplete) { reloadAllSections(); } }); } void onDataUpdate() { if (!isOnSearchTab) { if (kDebugMode) { _logger.finest('Skip reload till user clicks on search tab'); } hasPendingUpdate = true; } else { hasPendingUpdate = false; reloadAllSections(); } } void reloadAllSections() { _firstLoadInProgressOrComplete = true; _logger.info('queue reload all sections'); _debouncer.run(() async { setState(() { _logger.info("'_debounceTimer: reloading all sections in search tab"); final allSectionsExamples = >>[]; for (SectionType sectionType in SectionType.values) { allSectionsExamples.add( sectionType.getData(context, limit: kSearchSectionLimit), ); } try { allSectionsExamplesFuture = Future.wait>( allSectionsExamples, eagerError: false, ); } catch (e) { _logger.severe("Error reloading all sections: $e"); } }); }); } @override void dispose() { _onPeopleChangedEvent.cancel(); _filesUpdatedEvent.cancel(); _tabChangeEvent.cancel(); _debouncer.cancelDebounceTimer(); super.dispose(); } @override Widget build(BuildContext context) { return InheritedAllSectionsExamples( allSectionsExamplesFuture, _debouncer.debounceActiveNotifier, child: widget.child, ); } } class InheritedAllSectionsExamples extends InheritedWidget { final Future>> allSectionsExamplesFuture; final ValueNotifier isDebouncingNotifier; const InheritedAllSectionsExamples( this.allSectionsExamplesFuture, this.isDebouncingNotifier, { super.key, required super.child, }); static InheritedAllSectionsExamples of(BuildContext context) { return context .dependOnInheritedWidgetOfExactType()!; } @override bool updateShouldNotify(covariant InheritedAllSectionsExamples oldWidget) { return !identical( oldWidget.allSectionsExamplesFuture, allSectionsExamplesFuture, ); } }