From 36dd576fc5ab2dfa4254ea48436567c45dfc50c8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 4 Jan 2025 01:14:51 +0000 Subject: [PATCH] Fixed bug where explicitly disabled stores would not be excluded from reading whilst browse caching Improved example app capabilities (added ability to explictily disable stores when neccessary) --- .../main/map_view/components/attribution.dart | 82 ++++++++++ .../src/screens/main/map_view/map_view.dart | 52 +++--- ...lumn_headers_and_inheritable_settings.dart | 151 ++++++++++-------- .../components/export_stores/button.dart | 6 +- .../example_app_limitations_text.dart | 5 + .../components/new_store_button.dart | 6 +- .../stores_list/components/no_stores.dart | 6 +- .../browse_store_strategy_selector.dart | 94 +++++++++-- .../checkbox.dart | 4 +- .../dropdown.dart | 38 +++-- .../tiles/store_tile/store_tile.dart | 39 ----- .../components/tiles/unspecified_tile.dart | 4 +- .../src/shared/state/general_provider.dart | 3 + .../impls/objectbox/backend/internal.dart | 22 ++- .../backend/internal_workers/shared.dart | 22 +++ .../internal_workers/standard/cmd_type.dart | 3 +- .../internal_workers/standard/worker.dart | 109 +++++++------ .../backend/interfaces/backend/internal.dart | 41 +++-- .../image_provider/internal_tile_browser.dart | 14 +- .../tile_provider/tile_provider.dart | 49 +++--- 20 files changed, 478 insertions(+), 272 deletions(-) create mode 100644 example/lib/src/screens/main/map_view/components/attribution.dart create mode 100644 example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/export_stores/example_app_limitations_text.dart diff --git a/example/lib/src/screens/main/map_view/components/attribution.dart b/example/lib/src/screens/main/map_view/components/attribution.dart new file mode 100644 index 00000000..398e1e69 --- /dev/null +++ b/example/lib/src/screens/main/map_view/components/attribution.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; + +import '../map_view.dart'; + +class Attribution extends StatelessWidget { + const Attribution({ + super.key, + required this.urlTemplate, + required this.mode, + required this.stores, + required this.otherStoresStrategy, + }); + + final String urlTemplate; + final MapViewMode mode; + final Map stores; + final BrowseStoreStrategy? otherStoresStrategy; + + @override + Widget build(BuildContext context) => RichAttributionWidget( + alignment: AttributionAlignment.bottomLeft, + popupInitialDisplayDuration: const Duration(seconds: 3), + popupBorderRadius: BorderRadius.circular(12), + attributions: [ + TextSourceAttribution(Uri.parse(urlTemplate).host), + const TextSourceAttribution( + 'For demonstration purposes only', + prependCopyright: false, + textStyle: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSourceAttribution( + 'Offline mapping made with FMTC', + prependCopyright: false, + textStyle: TextStyle(fontStyle: FontStyle.italic), + ), + LogoSourceAttribution( + mode == MapViewMode.standard + ? const Icon(Icons.bug_report) + : const SizedBox.shrink(), + tooltip: 'Show resolved store configuration', + onTap: () => showDialog( + context: context, + builder: (context) => AlertDialog.adaptive( + title: const Text('Resolved store configuration'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + stores.entries.isEmpty + ? 'No stores set explicitly' + : stores.entries + .map( + (e) => '${e.key}: ${e.value ?? 'Explicitly ' + 'disabled'}', + ) + .join('\n'), + ), + Text( + otherStoresStrategy == null + ? 'No other stores in use' + : 'All unspecified stores: $otherStoresStrategy', + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Understood'), + ), + ], + ), + ), + ), + LogoSourceAttribution( + Image.asset('assets/icons/ProjectIcon.png'), + tooltip: 'flutter_map_tile_caching', + ), + ], + ); +} diff --git a/example/lib/src/screens/main/map_view/map_view.dart b/example/lib/src/screens/main/map_view/map_view.dart index bce63ed8..a2bfe56b 100644 --- a/example/lib/src/screens/main/map_view/map_view.dart +++ b/example/lib/src/screens/main/map_view/map_view.dart @@ -18,6 +18,7 @@ import '../../../shared/state/general_provider.dart'; import '../../../shared/state/region_selection_provider.dart'; import '../../../shared/state/selected_tab_state.dart'; import 'components/additional_overlay/additional_overlay.dart'; +import 'components/attribution.dart'; import 'components/debugging_tile_builder/debugging_tile_builder.dart'; import 'components/download_progress/download_progress_masker.dart'; import 'components/recovery_regions/recovery_regions.dart'; @@ -264,6 +265,9 @@ class _MapViewState extends State with TickerProviderStateMixin { builder: (context, provider, _) { final urlTemplate = provider.urlTemplate; + final otherStoresStrategy = provider.currentStores['(unspecified)'] + ?.toBrowseStoreStrategy(); + final compiledStoreNames = Map.fromEntries([ ...stores.entries.where((e) => e.value == urlTemplate).map((e) { @@ -276,37 +280,31 @@ class _MapViewState extends State with TickerProviderStateMixin { if (behaviour == null) return null; return MapEntry(e.key, behaviour); }).nonNulls, - ...stores.entries - .where((e) => e.value != urlTemplate) - .map((e) => MapEntry(e.key, null)), + ...stores.entries.where( + (e) { + if (e.value != urlTemplate) return true; + + final internalBehaviour = provider.currentStores[e.key]; + final behaviour = internalBehaviour == null + ? provider.inheritableBrowseStoreStrategy + : internalBehaviour.toBrowseStoreStrategy( + provider.inheritableBrowseStoreStrategy, + ); + + return provider.explicitlyExcludedStores.contains(e.key) && + behaviour == null && + otherStoresStrategy != null; + }, + ).map((e) => MapEntry(e.key, null)), ]); - final attribution = RichAttributionWidget( - alignment: AttributionAlignment.bottomLeft, - popupInitialDisplayDuration: const Duration(seconds: 3), - popupBorderRadius: BorderRadius.circular(12), - attributions: [ - TextSourceAttribution(Uri.parse(urlTemplate).host), - const TextSourceAttribution( - 'For demonstration purposes only', - prependCopyright: false, - textStyle: TextStyle(fontWeight: FontWeight.bold), - ), - const TextSourceAttribution( - 'Offline mapping made with FMTC', - prependCopyright: false, - textStyle: TextStyle(fontStyle: FontStyle.italic), - ), - LogoSourceAttribution( - Image.asset('assets/icons/ProjectIcon.png'), - tooltip: 'flutter_map_tile_caching', - ), - ], + final attribution = Attribution( + urlTemplate: urlTemplate, + mode: widget.mode, + stores: compiledStoreNames, + otherStoresStrategy: otherStoresStrategy, ); - final otherStoresStrategy = provider.currentStores['(unspecified)'] - ?.toBrowseStoreStrategy(); - final tileLayer = TileLayer( urlTemplate: urlTemplate, userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/column_headers_and_inheritable_settings.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/column_headers_and_inheritable_settings.dart index 33be3ce8..a84dcec0 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/column_headers_and_inheritable_settings.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/column_headers_and_inheritable_settings.dart @@ -66,74 +66,93 @@ class ColumnHeadersAndInheritableSettings extends StatelessWidget { ), ), const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: Selector( - selector: (context, provider) => - provider.inheritableBrowseStoreStrategy, - builder: (context, currentBehaviour, child) { - if (useCompactLayout) { - return Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: DropdownButton( - items: [null] - .followedBy(BrowseStoreStrategy.values) - .map( - (e) => DropdownMenuItem( - value: e, - alignment: Alignment.center, - child: switch (e) { - null => const Icon( - Icons.disabled_by_default_rounded, - ), - BrowseStoreStrategy.read => - const Icon(Icons.visibility), - BrowseStoreStrategy.readUpdate => - const Icon(Icons.edit), - BrowseStoreStrategy.readUpdateCreate => - const Icon(Icons.add), - }, - ), - ) - .toList(), - value: currentBehaviour, - onChanged: (v) => context - .read() - .inheritableBrowseStoreStrategy = v, - ), - ), - ); - } - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: BrowseStoreStrategy.values.map( - (e) { - final value = currentBehaviour == e - ? true - : InternalBrowseStoreStrategy.priority - .indexOf(currentBehaviour) < - InternalBrowseStoreStrategy.priority - .indexOf(e) - ? false - : null; - - return Checkbox.adaptive( - value: value, - onChanged: (v) => context + Row( + children: [ + const SizedBox(width: 20), + Tooltip( + message: 'These inheritance options are tracked manually by\n' + 'the app and not FMTC. This enables both inheritance\n' + 'and "All unspecified" (which uses `otherStoresStrategy`\n' + 'in FMTC) to be represented in the example app. Tap\n' + 'the debug icon in the map attribution to see how the\n' + 'store configuration is resolved and passed to FMTC.', + textAlign: TextAlign.center, + child: Icon( + Icons.help_outline, + color: Colors.black.withAlpha(255 ~/ 3), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Selector( + selector: (context, provider) => + provider.inheritableBrowseStoreStrategy, + builder: (context, currentBehaviour, child) { + if (useCompactLayout) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: DropdownButton( + items: [null] + .followedBy(BrowseStoreStrategy.values) + .map( + (e) => DropdownMenuItem( + value: e, + alignment: Alignment.center, + child: switch (e) { + null => const Icon( + Icons.disabled_by_default_rounded, + ), + BrowseStoreStrategy.read => + const Icon(Icons.visibility), + BrowseStoreStrategy.readUpdate => + const Icon(Icons.edit), + BrowseStoreStrategy.readUpdateCreate => + const Icon(Icons.add), + }, + ), + ) + .toList(), + value: currentBehaviour, + onChanged: (v) => context .read() - .inheritableBrowseStoreStrategy = - v == null ? null : e, - tristate: true, - materialTapTargetSize: MaterialTapTargetSize.padded, - visualDensity: VisualDensity.comfortable, + .inheritableBrowseStoreStrategy = v, + ), + ), ); - }, - ).toList(growable: false), - ); - }, - ), + } + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: BrowseStoreStrategy.values.map( + (e) { + final value = currentBehaviour == e + ? true + : InternalBrowseStoreStrategy.priority + .indexOf(currentBehaviour) < + InternalBrowseStoreStrategy.priority + .indexOf(e) + ? false + : null; + + return Checkbox.adaptive( + value: value, + onChanged: (v) => context + .read() + .inheritableBrowseStoreStrategy = + v == null ? null : e, + tristate: true, + materialTapTargetSize: MaterialTapTargetSize.padded, + visualDensity: VisualDensity.comfortable, + ); + }, + ).toList(growable: false), + ); + }, + ), + ), + ], ), const Divider(height: 8, indent: 12, endIndent: 12), ], diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/export_stores/button.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/export_stores/button.dart index 42a53465..533899d2 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/export_stores/button.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/export_stores/button.dart @@ -10,6 +10,7 @@ import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import '../../state/export_selection_provider.dart'; +import 'example_app_limitations_text.dart'; part 'name_input_dialog.dart'; part 'progress_dialog.dart'; @@ -48,10 +49,7 @@ class ExportStoresButton extends StatelessWidget { ), const SizedBox(height: 24), Text( - 'Within the example app, for simplicity, each store contains ' - 'tiles from a single URL template. Additionally, only one tile ' - 'layer with a single URL template can be used at any one time. ' - 'These are not limitations with FMTC.', + exampleAppLimitationsText, textAlign: TextAlign.center, style: Theme.of(context).textTheme.labelSmall, ), diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/export_stores/example_app_limitations_text.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/export_stores/example_app_limitations_text.dart new file mode 100644 index 00000000..46832528 --- /dev/null +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/export_stores/example_app_limitations_text.dart @@ -0,0 +1,5 @@ +const exampleAppLimitationsText = + 'There are some limitations to the example app which do not exist in FMTC, ' + 'because it is difficult to express in this UI design.\nEach store only ' + 'contains tiles from a single URL template. Only a single tile layer is ' + 'used/available (only a single URL template can be used at any one time).'; diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/new_store_button.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/new_store_button.dart index d926a38d..e09f1bea 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/new_store_button.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/new_store_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../../../../../../import/import.dart'; import '../../../../../../../store_editor/store_editor.dart'; +import 'export_stores/example_app_limitations_text.dart'; class NewStoreButton extends StatelessWidget { const NewStoreButton({super.key}); @@ -36,10 +37,7 @@ class NewStoreButton extends StatelessWidget { ), const SizedBox(height: 24), Text( - 'Within the example app, for simplicity, each store contains ' - 'tiles from a single URL template. Additionally, only one tile ' - 'layer with a single URL template can be used at any one time. ' - 'These are not limitations with FMTC.', + exampleAppLimitationsText, textAlign: TextAlign.center, style: Theme.of(context).textTheme.labelSmall, ), diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/no_stores.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/no_stores.dart index c6544fef..9d2ec96e 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/no_stores.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/no_stores.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../../../../../../import/import.dart'; import '../../../../../../../store_editor/store_editor.dart'; +import 'export_stores/example_app_limitations_text.dart'; class NoStores extends StatelessWidget { const NoStores({super.key}); @@ -50,10 +51,7 @@ class NoStores extends StatelessWidget { ), const SizedBox(height: 32), Text( - 'Within the example app, for simplicity, each store contains ' - 'tiles from a single URL template. Additionally, only one ' - 'tile layer with a single URL template can be used at any ' - 'one time. These are not limitations with FMTC.', + exampleAppLimitationsText, textAlign: TextAlign.center, style: Theme.of(context).textTheme.labelSmall, ), diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/browse_store_strategy_selector.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/browse_store_strategy_selector.dart index 6ef7854a..0b6599b5 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/browse_store_strategy_selector.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/browse_store_strategy_selector.dart @@ -23,6 +23,7 @@ class BrowseStoreStrategySelector extends StatelessWidget { final bool useCompactLayout; static const _unspecifiedSelectorColor = Colors.pinkAccent; + static const _unspecifiedSelectorExcludedColor = Colors.purple; @override Widget build(BuildContext context) { @@ -30,10 +31,11 @@ class BrowseStoreStrategySelector extends StatelessWidget { context.select( (provider) => provider.currentStores[storeName], ); - final unspecifiedStrategy = - context.select( - (provider) => provider.currentStores['(unspecified)'], - ); + final unspecifiedStrategy = context + .select( + (provider) => provider.currentStores['(unspecified)'], + ) + ?.toBrowseStoreStrategy(); final inheritableStrategy = inheritable ? context.select( (provider) => provider.inheritableBrowseStoreStrategy, @@ -44,9 +46,17 @@ class BrowseStoreStrategySelector extends StatelessWidget { ? inheritableStrategy : currentStrategy.toBrowseStoreStrategy(inheritableStrategy); final isUsingUnselectedStrategy = resolvedCurrentStrategy == null && - unspecifiedStrategy != InternalBrowseStoreStrategy.disable && + unspecifiedStrategy != null && enabled; + final showExplicitExcludeCheckbox = + resolvedCurrentStrategy == null && isUsingUnselectedStrategy; + + final isExplicitlyExcluded = showExplicitExcludeCheckbox && + context.select( + (provider) => provider.explicitlyExcludedStores.contains(storeName), + ); + // Parameter meaning obvious from context, also callback // ignore: avoid_positional_boolean_parameters void changedInheritCheckbox(bool? value) { @@ -61,10 +71,66 @@ class BrowseStoreStrategySelector extends StatelessWidget { ..changedCurrentStores(); } + // Parameter meaning obvious from context, also callback + // ignore: avoid_positional_boolean_parameters + void changedExplicitlyExcludeCheckbox(bool? value) { + final provider = context.read(); + + if (value!) { + provider.explicitlyExcludedStores.add(storeName); + } else { + provider.explicitlyExcludedStores.remove(storeName); + } + + provider.changedExplicitlyExcludedStores(); + } + return Row( mainAxisSize: MainAxisSize.min, children: [ if (inheritable) ...[ + DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceDim, + borderRadius: BorderRadius.circular(99), + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + transitionBuilder: (child, animation) => SizeTransition( + sizeFactor: Tween(begin: 0, end: 1).animate(animation), + axis: Axis.horizontal, + axisAlignment: 1, + child: SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ), + child: showExplicitExcludeCheckbox + ? Tooltip( + message: 'Explicitly disable', + child: Padding( + padding: const EdgeInsets.all(4) + + const EdgeInsets.symmetric(horizontal: 4), + child: Row( + spacing: 6, + children: [ + const Icon(Icons.disabled_by_default_rounded), + Checkbox.adaptive( + value: isExplicitlyExcluded, + onChanged: changedExplicitlyExcludeCheckbox, + activeColor: BrowseStoreStrategySelector + ._unspecifiedSelectorExcludedColor, + ), + ], + ), + ), + ) + : const SizedBox.shrink(), + ), + ), Checkbox.adaptive( value: currentStrategy == InternalBrowseStoreStrategy.inherit || currentStrategy == null, @@ -80,6 +146,7 @@ class BrowseStoreStrategySelector extends StatelessWidget { currentStrategy: resolvedCurrentStrategy, enabled: enabled, isUnspecifiedSelector: storeName == '(unspecified)', + isExplicitlyExcluded: isExplicitlyExcluded, ) else Stack( @@ -87,18 +154,21 @@ class BrowseStoreStrategySelector extends StatelessWidget { Transform.translate( offset: const Offset(2, 0), child: AnimatedContainer( - duration: const Duration(milliseconds: 100), + duration: const Duration(milliseconds: 150), decoration: BoxDecoration( - color: BrowseStoreStrategySelector._unspecifiedSelectorColor - .withValues(alpha: 0.75), + color: isExplicitlyExcluded + ? BrowseStoreStrategySelector + ._unspecifiedSelectorExcludedColor + .withAlpha(255 ~/ 2) + : BrowseStoreStrategySelector._unspecifiedSelectorColor + .withValues(alpha: 0.75), borderRadius: BorderRadius.circular(99), ), width: isUsingUnselectedStrategy ? switch (unspecifiedStrategy) { - InternalBrowseStoreStrategy.read => 40, - InternalBrowseStoreStrategy.readUpdate => 85, - InternalBrowseStoreStrategy.readUpdateCreate => 128, - _ => 0, + BrowseStoreStrategy.read => 40, + BrowseStoreStrategy.readUpdate => 85, + BrowseStoreStrategy.readUpdateCreate => 128, } : 0, ), diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/checkbox.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/checkbox.dart index 20fe60cb..2a1ad6af 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/checkbox.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/checkbox.dart @@ -57,9 +57,9 @@ class _BrowseStoreStrategySelectorCheckbox extends StatelessWidget { activeColor: isUnspecifiedSelector ? BrowseStoreStrategySelector._unspecifiedSelectorColor : null, - fillColor: WidgetStateProperty.resolveWith((states) { + /*fillColor: WidgetStateProperty.resolveWith((states) { if (states.isEmpty) return Theme.of(context).colorScheme.surface; return null; - }), + }),*/ ); } diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/dropdown.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/dropdown.dart index ce5c6392..55b6fbad 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/dropdown.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/components/browse_store_strategy_selector/dropdown.dart @@ -6,12 +6,14 @@ class _BrowseStoreStrategySelectorDropdown extends StatelessWidget { required this.currentStrategy, required this.enabled, required this.isUnspecifiedSelector, + required this.isExplicitlyExcluded, }); final String storeName; final BrowseStoreStrategy? currentStrategy; final bool enabled; final bool isUnspecifiedSelector; + final bool isExplicitlyExcluded; @override Widget build(BuildContext context) => Padding( @@ -36,25 +38,37 @@ class _BrowseStoreStrategySelectorDropdown extends StatelessWidget { .select( (provider) => provider.currentStores['(unspecified)'], )) { - InternalBrowseStoreStrategy.read => const Icon( + InternalBrowseStoreStrategy.read => Icon( Icons.visibility, - color: BrowseStoreStrategySelector - ._unspecifiedSelectorColor, + color: isExplicitlyExcluded + ? BrowseStoreStrategySelector + ._unspecifiedSelectorExcludedColor + : BrowseStoreStrategySelector + ._unspecifiedSelectorColor, ), - InternalBrowseStoreStrategy.readUpdate => const Icon( + InternalBrowseStoreStrategy.readUpdate => Icon( Icons.edit, - color: BrowseStoreStrategySelector - ._unspecifiedSelectorColor, + color: isExplicitlyExcluded + ? BrowseStoreStrategySelector + ._unspecifiedSelectorExcludedColor + : BrowseStoreStrategySelector + ._unspecifiedSelectorColor, ), - InternalBrowseStoreStrategy.readUpdateCreate => const Icon( + InternalBrowseStoreStrategy.readUpdateCreate => Icon( Icons.add, - color: BrowseStoreStrategySelector - ._unspecifiedSelectorColor, + color: isExplicitlyExcluded + ? BrowseStoreStrategySelector + ._unspecifiedSelectorExcludedColor + : BrowseStoreStrategySelector + ._unspecifiedSelectorColor, ), - _ => const Icon( + _ => Icon( Icons.disabled_by_default_rounded, - color: BrowseStoreStrategySelector - ._unspecifiedSelectorColor, + color: isExplicitlyExcluded + ? BrowseStoreStrategySelector + ._unspecifiedSelectorExcludedColor + : BrowseStoreStrategySelector + ._unspecifiedSelectorColor, ), }, BrowseStoreStrategy.read => diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/store_tile.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/store_tile.dart index b146e198..247d2222 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/store_tile.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/store_tile/store_tile.dart @@ -93,45 +93,6 @@ class _StoreTileState extends State { visualDensity: widget.useCompactLayout ? VisualDensity.compact : null, ), - /*FutureBuilder( - future: widget.stats, - builder: (context, statsSnapshot) { - if (statsSnapshot.data?.length == 0) { - return IconButton( - onPressed: _deleteStore, - icon: const Icon( - Icons.delete_forever, - color: Colors.red, - ), - visualDensity: widget.useCompactLayout - ? VisualDensity.compact - : null, - ); - } - - if (_toolsEmptyLoading) { - return const IconButton( - onPressed: null, - icon: SizedBox.square( - dimension: 18, - child: Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 3, - ), - ), - ), - ); - } - - return IconButton( - onPressed: _emptyStore, - icon: const Icon(Icons.delete), - visualDensity: widget.useCompactLayout - ? VisualDensity.compact - : null, - ); - }, - ),*/ const SizedBox(width: 4), ]; diff --git a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/unspecified_tile.dart b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/unspecified_tile.dart index 006d88bb..0b8a2018 100644 --- a/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/unspecified_tile.dart +++ b/example/lib/src/screens/main/secondary_view/contents/home/components/stores_list/components/tiles/unspecified_tile.dart @@ -37,7 +37,7 @@ class _UnspecifiedTileState extends State { color: Colors.transparent, child: ListTile( title: const Text( - 'All disabled', + 'All unspecified', maxLines: 2, overflow: TextOverflow.fade, style: TextStyle(fontStyle: FontStyle.italic), @@ -65,9 +65,9 @@ class _UnspecifiedTileState extends State { borderRadius: BorderRadius.circular(99), ), child: Row( + spacing: 4, children: [ const Icon(Icons.last_page), - const SizedBox(width: 4), Switch.adaptive( value: !isAllUnselectedDisabled && context.select( diff --git a/example/lib/src/shared/state/general_provider.dart b/example/lib/src/shared/state/general_provider.dart index cb356cfb..7b57cb66 100644 --- a/example/lib/src/shared/state/general_provider.dart +++ b/example/lib/src/shared/state/general_provider.dart @@ -44,6 +44,9 @@ class GeneralProvider extends ChangeNotifier { final Map currentStores = {}; void changedCurrentStores() => notifyListeners(); + final Set explicitlyExcludedStores = {}; + void changedExplicitlyExcludedStores() => notifyListeners(); + String _urlTemplate = sharedPrefs.getString(SharedPrefsKeys.urlTemplate.name) ?? (() { diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index f7b90b14..b6e63090 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -376,7 +376,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override Future tileExists({ required String url, - List? storeNames, + required ({bool includeOrExclude, List storeNames}) storeNames, }) async => (await _sendCmdOneShot( type: _CmdType.tileExists, @@ -391,7 +391,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { List allStoreNames, })> readTile({ required String url, - List? storeNames, + required ({bool includeOrExclude, List storeNames}) storeNames, }) async { final res = (await _sendCmdOneShot( type: _CmdType.readTile, @@ -441,13 +441,21 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ))!['wasOrphan']; @override - Future registerHitOrMiss({ - required List? storeNames, - required bool hit, + Future incrementStoreHits({ + required List storeNames, + }) => + _sendCmdOneShot( + type: _CmdType.incrementStoreHits, + args: {'storeNames': storeNames}, + ); + + @override + Future incrementStoreMisses({ + required ({bool includeOrExclude, List storeNames}) storeNames, }) => _sendCmdOneShot( - type: _CmdType.registerHitOrMiss, - args: {'storeNames': storeNames, 'hit': hit}, + type: _CmdType.incrementStoreMisses, + args: {'storeNames': storeNames}, ); @override diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart index 053b05e0..eae17e56 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart @@ -3,6 +3,28 @@ part of '../backend.dart'; +List _resolveReadableStoresFormat( + ({bool includeOrExclude, List storeNames}) readableStores, { + required Store root, +}) { + final availableStoreNames = + root.box().getAll().map((e) => e.name); + + if (!readableStores.includeOrExclude) { + return availableStoreNames + .whereNot((e) => readableStores.storeNames.contains(e)) + .toList(growable: false); + } + + for (final storeName in readableStores.storeNames) { + if (!availableStoreNames.contains(storeName)) { + throw StoreNotExists(storeName: storeName); + } + } + + return readableStores.storeNames; +} + Map _sharedWriteSingleTile({ required Store root, required List storeNames, diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart index f2f4b36a..b64b53e2 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart @@ -25,7 +25,8 @@ enum _CmdType { readLatestTile, writeTile, deleteTile, - registerHitOrMiss, + incrementStoreHits, + incrementStoreMisses, removeOldestTilesAboveLimit, removeTilesOlderThan, readMetadata, diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart index fa1a0c22..c63b22de 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart @@ -424,17 +424,18 @@ Future _worker( tilesQuery.close(); case _CmdType.tileExists: final url = cmd.args['url']! as String; - final storeNames = cmd.args['storeNames']! as List?; - - final stores = root.box(); + final storeNames = cmd.args['storeNames']! as ({ + bool includeOrExclude, + List storeNames, + }); - final queryPart = stores.query(ObjectBoxTile_.url.equals(url)); - final query = storeNames == null - ? queryPart.build() - : (queryPart + final query = + (root.box().query(ObjectBoxTile_.url.equals(url)) ..linkMany( ObjectBoxTile_.stores, - ObjectBoxStore_.name.oneOf(storeNames), + ObjectBoxStore_.name.oneOf( + _resolveReadableStoresFormat(storeNames, root: root), + ), )) .build(); @@ -443,18 +444,19 @@ Future _worker( query.close(); case _CmdType.readTile: final url = cmd.args['url']! as String; - final storeNames = cmd.args['storeNames'] as List?; + final storeNames = cmd.args['storeNames'] as ({ + bool includeOrExclude, + List storeNames, + }); - final stores = root.box(); - final specifiedStores = storeNames?.isNotEmpty ?? false; + final resolvedStores = + _resolveReadableStoresFormat(storeNames, root: root); - final queryPart = stores.query(ObjectBoxTile_.url.equals(url)); - final query = !specifiedStores - ? queryPart.build() - : (queryPart + final query = + (root.box().query(ObjectBoxTile_.url.equals(url)) ..linkMany( ObjectBoxTile_.stores, - ObjectBoxStore_.name.oneOf(storeNames!), + ObjectBoxStore_.name.oneOf(resolvedStores), )) .build(); @@ -471,19 +473,18 @@ Future _worker( }, ); } else { - final tileStores = tile.stores.map((s) => s.name); - final listTileStores = tileStores.toList(growable: false); + final listTileStores = + tile.stores.map((s) => s.name).toList(growable: false); + final intersectedStoreNames = listTileStores + .where(resolvedStores.contains) + .toList(growable: false); sendRes( id: cmd.id, data: { 'tile': tile, 'allStoreNames': listTileStores, - 'intersectedStoreNames': !specifiedStores - ? listTileStores - : SplayTreeSet.from(tileStores) - .intersection(SplayTreeSet.from(storeNames!)) - .toList(growable: false), + 'intersectedStoreNames': intersectedStoreNames, }, ); } @@ -541,40 +542,56 @@ Future _worker( storesQuery.close(); tilesQuery.close(); - case _CmdType.registerHitOrMiss: - final storeNames = cmd.args['storeNames'] as List?; - final hit = cmd.args['hit']! as bool; + case _CmdType.incrementStoreHits: + final storeNames = cmd.args['storeNames'] as List; final storesBox = root.box(); - final specifiedStores = storeNames?.isNotEmpty ?? false; - late final Query query; - if (specifiedStores) { - query = - storesBox.query(ObjectBoxStore_.name.oneOf(storeNames!)).build(); - } + final query = + storesBox.query(ObjectBoxStore_.name.oneOf(storeNames)).build(); root.runInTransaction( TxMode.write, () { - final stores = specifiedStores ? query.find() : storesBox.getAll(); - if (specifiedStores) { - if (stores.length != storeNames!.length) { - return StoreNotExists( - storeName: - storeNames.toSet().difference(stores.toSet()).join('; '), - ); - } + final stores = query.find(); + + if (stores.length != storeNames.length) { + return StoreNotExists( + storeName: storeNames + .toSet() + .difference(stores.map((s) => s.name).toSet()) + .join('; '), + ); + } - query.close(); + for (final store in stores) { + storesBox.put(store..hits += 1); } + }, + ); + + sendRes(id: cmd.id); + case _CmdType.incrementStoreMisses: + final storeNames = cmd.args['storeNames'] as ({ + bool includeOrExclude, + List storeNames, + }); + + final resolvedStoreNames = + _resolveReadableStoresFormat(storeNames, root: root); + + final storesBox = root.box(); + final query = storesBox + .query(ObjectBoxStore_.name.oneOf(resolvedStoreNames)) + .build(); + + root.runInTransaction( + TxMode.write, + () { + final stores = query.find(); for (final store in stores) { - storesBox.put( - store - ..hits += hit ? 1 : 0 - ..misses += hit ? 0 : 1, - ); + storesBox.put(store..misses += 1); } }, ); diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index c9c26910..904400e6 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -157,22 +157,26 @@ abstract interface class FMTCBackendInternal required String storeName, }); - /// Check whether the specified tile exists in any of the specified stores (or - /// any store is [storeNames] is `null`) + /// Check whether the specified tile exists in any of the specified stores + /// + /// {@template fmtc.backend._readableStoresFormat} + /// [storeNames] uses the "readable stores" format. If `includeOrExclude` is + /// `true`, the operation will apply to all stores included only in + /// `storeNames`. Otherwise, the operation will apply to all existing stores + /// NOT included in `storeNames`. This should reflect the settings of an + /// [FMTCTileProvider] (see [FMTCTileProvider._compileReadableStores]). + /// {@endtemplate} Future tileExists({ required String url, - List? storeNames, + required ({List storeNames, bool includeOrExclude}) storeNames, }); - /// Retrieve a raw `tile` from any of the specified [storeNames] (or all store - /// names if `null` or empty) by the specified URL + /// Retrieve a raw `tile` by URL from any of the specified stores /// - /// Returns the list of store names the tile belongs to - `allStoreNames` - - /// and were present in [storeNames] if specified - `intersectedStoreNames`. + /// {@macro fmtc.backend._readableStoresFormat} /// - /// If [storeNames] is `null` or empty, tiles may be retrieved from any store - /// (which may be slower depending on the size of the root, as queries may - /// be unconstrained). + /// Returns the list of store names the tile belongs to (`allStoreNames`), + /// and were present in the resolved stores (`intersectedStoreNames`). /// /// `intersectedStoreNames` & `allStoreNames` will be empty if `tile` is /// `null`. @@ -183,7 +187,7 @@ abstract interface class FMTCBackendInternal List allStoreNames, })> readTile({ required String url, - List? storeNames, + required ({List storeNames, bool includeOrExclude}) storeNames, }); /// {@template fmtc.backend.readLatestTile} @@ -223,11 +227,16 @@ abstract interface class FMTCBackendInternal required String url, }); - /// Register a cache hit or miss on the specified stores, or all stores if - /// null or empty - Future registerHitOrMiss({ - required List? storeNames, - required bool hit, + /// Add a cache hit to all specified stores + Future incrementStoreHits({ + required List storeNames, + }); + + /// Add a cache miss to all specified stores + /// + /// {@macro fmtc.backend._readableStoresFormat} + Future incrementStoreMisses({ + required ({List storeNames, bool includeOrExclude}) storeNames, }); /// Remove tiles in excess of the specified limit in each specified store, diff --git a/lib/src/providers/image_provider/internal_tile_browser.dart b/lib/src/providers/image_provider/internal_tile_browser.dart index db91667a..26a8dd05 100644 --- a/lib/src/providers/image_provider/internal_tile_browser.dart +++ b/lib/src/providers/image_provider/internal_tile_browser.dart @@ -10,21 +10,20 @@ Future _internalTileBrowser({ required bool requireValidImage, required _TLIRConstructor? currentTLIR, }) async { + late final compiledReadableStores = provider._compileReadableStores(); + void registerHit(List storeNames) { currentTLIR?.hitOrMiss = true; if (provider.recordHitsAndMisses) { - FMTCBackendAccess.internal - .registerHitOrMiss(storeNames: storeNames, hit: true); + FMTCBackendAccess.internal.incrementStoreHits(storeNames: storeNames); } } void registerMiss() { currentTLIR?.hitOrMiss = false; if (provider.recordHitsAndMisses) { - FMTCBackendAccess.internal.registerHitOrMiss( - storeNames: provider._getSpecifiedStoresOrNull(), - hit: false, - ); + FMTCBackendAccess.internal + .incrementStoreMisses(storeNames: compiledReadableStores); } } @@ -43,7 +42,7 @@ Future _internalTileBrowser({ allStoreNames: allExistingStores, ) = await FMTCBackendAccess.internal.readTile( url: matcherUrl, - storeNames: provider._getSpecifiedStoresOrNull(), + storeNames: compiledReadableStores, ); currentTLIR?.cacheFetchDuration = @@ -202,6 +201,7 @@ Future _internalTileBrowser({ } } + // TODO: This isn't resolving properly! // Find the stores that need to have this tile written to, depending on // their read/write settings // At this point, we've downloaded the tile anyway, so we might as well diff --git a/lib/src/providers/tile_provider/tile_provider.dart b/lib/src/providers/tile_provider/tile_provider.dart index e75a7a7e..d737ce46 100644 --- a/lib/src/providers/tile_provider/tile_provider.dart +++ b/lib/src/providers/tile_provider/tile_provider.dart @@ -369,7 +369,7 @@ class FMTCTileProvider extends TileProvider { }) { final networkUrl = getTileUrl(coords, options); return FMTCBackendAccess.internal.tileExists( - storeNames: _getSpecifiedStoresOrNull(), + storeNames: _compileReadableStores(), url: urlTransformer?.call(networkUrl) ?? networkUrl, ); } @@ -411,13 +411,18 @@ class FMTCTileProvider extends TileProvider { return mutableUrl; } - // TODO: This does not work correctly. Needs a complex system like writing. - List? _getSpecifiedStoresOrNull() => otherStoresStrategy != null - ? null - : /*stores.keys.toList()*/ stores.entries - .where((e) => e.value != null) - .map((e) => e.key) - .toList(); + /// Compile the [FMTCTileProvider.stores] & + /// [FMTCTileProvider.otherStoresStrategy] into a format which can be resolved + /// by the backend once all available stores are known + ({List storeNames, bool includeOrExclude}) _compileReadableStores() { + final excludeOrInclude = otherStoresStrategy != null; + final storeNames = (excludeOrInclude + ? stores.entries.where((e) => e.value == null) + : stores.entries.where((e) => e.value != null)) + .map((e) => e.key) + .toList(growable: false); + return (storeNames: storeNames, includeOrExclude: !excludeOrInclude); + } @override bool operator ==(Object other) => @@ -436,19 +441,17 @@ class FMTCTileProvider extends TileProvider { mapEquals(other.headers, headers)); @override - int get hashCode => Object.hashAllUnordered( - [ - otherStoresStrategy, - loadingStrategy, - useOtherStoresAsFallbackOnly, - recordHitsAndMisses, - cachedValidDuration, - urlTransformer, - errorHandler, - tileLoadingInterceptor, - httpClient, - ...stores.entries.map((e) => (e.key, e.value)), - ...headers.entries.map((e) => (e.key, e.value)), - ], - ); + int get hashCode => Object.hashAllUnordered([ + otherStoresStrategy, + loadingStrategy, + useOtherStoresAsFallbackOnly, + recordHitsAndMisses, + cachedValidDuration, + urlTransformer, + errorHandler, + tileLoadingInterceptor, + httpClient, + ...stores.entries.map((e) => (e.key, e.value)), + ...headers.entries.map((e) => (e.key, e.value)), + ]); }