import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:aves/app_flavor.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/modules/app.dart'; import 'package:aves/model/settings/modules/collection.dart'; import 'package:aves/model/settings/modules/display.dart'; import 'package:aves/model/settings/modules/filter_grids.dart'; import 'package:aves/model/settings/modules/info.dart'; import 'package:aves/model/settings/modules/navigation.dart'; import 'package:aves/model/settings/modules/privacy.dart'; import 'package:aves/model/settings/modules/search.dart'; import 'package:aves/model/settings/modules/viewer.dart'; import 'package:aves/ref/bursts.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/common/search/page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/places_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves_map/aves_map.dart'; import 'package:aves_model/aves_model.dart'; import 'package:aves_utils/aves_utils.dart'; import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; final Settings settings = Settings._private(); class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings, NavigationSettings, SearchSettings, CollectionSettings, FilterGridsSettings, PrivacySettings, ViewerSettings, VideoSettings, SubtitlesSettings, InfoSettings { final List _subscriptions = []; final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change'); final StreamController _updateStreamController = StreamController.broadcast(); final StreamController _updateTileExtentStreamController = StreamController.broadcast(); @override Stream get updateStream => _updateStreamController.stream; Stream get updateTileExtentStream => _updateTileExtentStreamController.stream; @override bool get initialized => store.initialized; @override SettingsStore get store => settingsStore; Settings._private() { if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this); } Future init({required bool monitorPlatformSettings}) async { await store.init(); resetAppliedLocale(); if (monitorPlatformSettings) { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); _subscriptions.add(_platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChanged(event as Map?))); } initAppSettings(); } Future reload() => store.reload(); Future reset({required bool includeInternalKeys}) async { if (includeInternalKeys) { await store.clear(); } else { await Future.forEach(store.getKeys().whereNot(SettingKeys.isInternalKey), store.remove); } } Future setContextualDefaults(AppFlavor flavor) async { // performance final performanceClass = await deviceService.getPerformanceClass(); enableBlurEffect = performanceClass >= 29; final androidInfo = await DeviceInfoPlugin().androidInfo; final manufacturer = androidInfo.manufacturer.toLowerCase(); final pattern = BurstPatterns.byManufacturer[manufacturer]; collectionBurstPatterns = pattern != null ? [pattern] : []; // availability if (flavor.hasMapStyleDefault) { final defaultMapStyle = mobileServices.defaultMapStyle; if (mobileServices.mapStyles.contains(defaultMapStyle)) { mapStyle = defaultMapStyle; } else { final styles = EntryMapStyle.values.whereNot((v) => v.needMobileService).toList(); mapStyle = styles[Random().nextInt(styles.length)]; } } if (settings.useTvLayout) { applyTvSettings(); } } void applyTvSettings() { themeBrightness = AvesThemeBrightness.dark; maxBrightness = MaxBrightness.never; mustBackTwiceToExit = false; // address `TV-BU` / `TV-BY` requirements from https://developer.android.com/docs/quality-guidelines/tv-app-quality keepScreenOn = KeepScreenOn.videoPlayback; enableBottomNavigationBar = false; drawerTypeBookmarks = [ null, MimeFilter.video, FavouriteFilter.instance, ]; drawerPageBookmarks = [ AlbumListPage.routeName, CountryListPage.routeName, PlaceListPage.routeName, TagListPage.routeName, SearchPage.routeName, ]; showOverlayOnOpening = false; showOverlayMinimap = false; showOverlayThumbnailPreview = false; viewerGestureSideTapNext = false; viewerUseCutout = true; videoBackgroundMode = VideoBackgroundMode.disabled; videoControlActions = []; videoGestureDoubleTapTogglePlay = false; videoGestureSideDoubleTapSeek = false; enableBin = false; showPinchGestureAlternatives = true; } Future sanitize() async { if (timeToTakeAction == AccessibilityTimeout.system && !await AccessibilityService.hasRecommendedTimeouts()) { set(SettingKeys.timeToTakeActionKey, null); } if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) { set(SettingKeys.viewerUseCutoutKey, null); } if (videoBackgroundMode == VideoBackgroundMode.pip && !device.supportPictureInPicture) { set(SettingKeys.videoBackgroundModeKey, null); } collectionBurstPatterns = collectionBurstPatterns.where(BurstPatterns.options.contains).toList(); } // tag editor bool get tagEditorCurrentFilterSectionExpanded => getBool(SettingKeys.tagEditorCurrentFilterSectionExpandedKey) ?? SettingsDefaults.tagEditorCurrentFilterSectionExpanded; set tagEditorCurrentFilterSectionExpanded(bool newValue) => set(SettingKeys.tagEditorCurrentFilterSectionExpandedKey, newValue); String? get tagEditorExpandedSection => getString(SettingKeys.tagEditorExpandedSectionKey); set tagEditorExpandedSection(String? newValue) => set(SettingKeys.tagEditorExpandedSectionKey, newValue); // converter String get convertMimeType => getString(SettingKeys.convertMimeTypeKey) ?? SettingsDefaults.convertMimeType; set convertMimeType(String newValue) => set(SettingKeys.convertMimeTypeKey, newValue); int get convertQuality => getInt(SettingKeys.convertQualityKey) ?? SettingsDefaults.convertQuality; set convertQuality(int newValue) => set(SettingKeys.convertQualityKey, newValue); bool get convertWriteMetadata => getBool(SettingKeys.convertWriteMetadataKey) ?? SettingsDefaults.convertWriteMetadata; set convertWriteMetadata(bool newValue) => set(SettingKeys.convertWriteMetadataKey, newValue); // map EntryMapStyle? get mapStyle { final preferred = getEnumOrDefault(SettingKeys.mapStyleKey, null, EntryMapStyle.values); if (preferred == null) return null; final available = availability.mapStyles; return available.contains(preferred) ? preferred : available.first; } set mapStyle(EntryMapStyle? newValue) => set(SettingKeys.mapStyleKey, newValue?.toString()); LatLng? get mapDefaultCenter { final json = getString(SettingKeys.mapDefaultCenterKey); return json != null ? LatLng.fromJson(jsonDecode(json)) : null; } set mapDefaultCenter(LatLng? newValue) => set(SettingKeys.mapDefaultCenterKey, newValue != null ? jsonEncode(newValue.toJson()) : null); // bin bool get enableBin => getBool(SettingKeys.enableBinKey) ?? SettingsDefaults.enableBin; set enableBin(bool newValue) => set(SettingKeys.enableBinKey, newValue); // accessibility bool get showPinchGestureAlternatives => getBool(SettingKeys.showPinchGestureAlternativesKey) ?? SettingsDefaults.showPinchGestureAlternatives; set showPinchGestureAlternatives(bool newValue) => set(SettingKeys.showPinchGestureAlternativesKey, newValue); AccessibilityAnimations get accessibilityAnimations => getEnumOrDefault(SettingKeys.accessibilityAnimationsKey, SettingsDefaults.accessibilityAnimations, AccessibilityAnimations.values); bool get animate => accessibilityAnimations.animate; set accessibilityAnimations(AccessibilityAnimations newValue) => set(SettingKeys.accessibilityAnimationsKey, newValue.toString()); AccessibilityTimeout get timeToTakeAction => getEnumOrDefault(SettingKeys.timeToTakeActionKey, SettingsDefaults.timeToTakeAction, AccessibilityTimeout.values); set timeToTakeAction(AccessibilityTimeout newValue) => set(SettingKeys.timeToTakeActionKey, newValue.toString()); // file picker bool get filePickerShowHiddenFiles => getBool(SettingKeys.filePickerShowHiddenFilesKey) ?? SettingsDefaults.filePickerShowHiddenFiles; set filePickerShowHiddenFiles(bool newValue) => set(SettingKeys.filePickerShowHiddenFilesKey, newValue); // screen saver bool get screenSaverFillScreen => getBool(SettingKeys.screenSaverFillScreenKey) ?? SettingsDefaults.slideshowFillScreen; set screenSaverFillScreen(bool newValue) => set(SettingKeys.screenSaverFillScreenKey, newValue); bool get screenSaverAnimatedZoomEffect => getBool(SettingKeys.screenSaverAnimatedZoomEffectKey) ?? SettingsDefaults.slideshowAnimatedZoomEffect; set screenSaverAnimatedZoomEffect(bool newValue) => set(SettingKeys.screenSaverAnimatedZoomEffectKey, newValue); ViewerTransition get screenSaverTransition => getEnumOrDefault(SettingKeys.screenSaverTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); set screenSaverTransition(ViewerTransition newValue) => set(SettingKeys.screenSaverTransitionKey, newValue.toString()); SlideshowVideoPlayback get screenSaverVideoPlayback => getEnumOrDefault(SettingKeys.screenSaverVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values); set screenSaverVideoPlayback(SlideshowVideoPlayback newValue) => set(SettingKeys.screenSaverVideoPlaybackKey, newValue.toString()); int get screenSaverInterval => getInt(SettingKeys.screenSaverIntervalKey) ?? SettingsDefaults.slideshowInterval; set screenSaverInterval(int newValue) => set(SettingKeys.screenSaverIntervalKey, newValue); Set get screenSaverCollectionFilters => (getStringList(SettingKeys.screenSaverCollectionFiltersKey) ?? []).map(CollectionFilter.fromJson).nonNulls.toSet(); set screenSaverCollectionFilters(Set newValue) => set(SettingKeys.screenSaverCollectionFiltersKey, newValue.map((filter) => filter.toJson()).toList()); // slideshow bool get slideshowRepeat => getBool(SettingKeys.slideshowRepeatKey) ?? SettingsDefaults.slideshowRepeat; set slideshowRepeat(bool newValue) => set(SettingKeys.slideshowRepeatKey, newValue); bool get slideshowShuffle => getBool(SettingKeys.slideshowShuffleKey) ?? SettingsDefaults.slideshowShuffle; set slideshowShuffle(bool newValue) => set(SettingKeys.slideshowShuffleKey, newValue); bool get slideshowFillScreen => getBool(SettingKeys.slideshowFillScreenKey) ?? SettingsDefaults.slideshowFillScreen; set slideshowFillScreen(bool newValue) => set(SettingKeys.slideshowFillScreenKey, newValue); bool get slideshowAnimatedZoomEffect => getBool(SettingKeys.slideshowAnimatedZoomEffectKey) ?? SettingsDefaults.slideshowAnimatedZoomEffect; set slideshowAnimatedZoomEffect(bool newValue) => set(SettingKeys.slideshowAnimatedZoomEffectKey, newValue); ViewerTransition get slideshowTransition => getEnumOrDefault(SettingKeys.slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); set slideshowTransition(ViewerTransition newValue) => set(SettingKeys.slideshowTransitionKey, newValue.toString()); SlideshowVideoPlayback get slideshowVideoPlayback => getEnumOrDefault(SettingKeys.slideshowVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values); set slideshowVideoPlayback(SlideshowVideoPlayback newValue) => set(SettingKeys.slideshowVideoPlaybackKey, newValue.toString()); int get slideshowInterval => getInt(SettingKeys.slideshowIntervalKey) ?? SettingsDefaults.slideshowInterval; set slideshowInterval(int newValue) => set(SettingKeys.slideshowIntervalKey, newValue); // widget WidgetOutline getWidgetOutline(int widgetId) => getEnumOrDefault('${SettingKeys.widgetOutlinePrefixKey}$widgetId', WidgetOutline.none, WidgetOutline.values); void setWidgetOutline(int widgetId, WidgetOutline newValue) => set('${SettingKeys.widgetOutlinePrefixKey}$widgetId', newValue.toString()); WidgetShape getWidgetShape(int widgetId) => getEnumOrDefault('${SettingKeys.widgetShapePrefixKey}$widgetId', SettingsDefaults.widgetShape, WidgetShape.values); void setWidgetShape(int widgetId, WidgetShape newValue) => set('${SettingKeys.widgetShapePrefixKey}$widgetId', newValue.toString()); Set getWidgetCollectionFilters(int widgetId) => (getStringList('${SettingKeys.widgetCollectionFiltersPrefixKey}$widgetId') ?? []).map(CollectionFilter.fromJson).nonNulls.toSet(); void setWidgetCollectionFilters(int widgetId, Set newValue) => set('${SettingKeys.widgetCollectionFiltersPrefixKey}$widgetId', newValue.map((filter) => filter.toJson()).toList()); WidgetOpenPage getWidgetOpenPage(int widgetId) => getEnumOrDefault('${SettingKeys.widgetOpenPagePrefixKey}$widgetId', SettingsDefaults.widgetOpenPage, WidgetOpenPage.values); void setWidgetOpenPage(int widgetId, WidgetOpenPage newValue) => set('${SettingKeys.widgetOpenPagePrefixKey}$widgetId', newValue.toString()); WidgetDisplayedItem getWidgetDisplayedItem(int widgetId) => getEnumOrDefault('${SettingKeys.widgetDisplayedItemPrefixKey}$widgetId', SettingsDefaults.widgetDisplayedItem, WidgetDisplayedItem.values); void setWidgetDisplayedItem(int widgetId, WidgetDisplayedItem newValue) => set('${SettingKeys.widgetDisplayedItemPrefixKey}$widgetId', newValue.toString()); String? getWidgetUri(int widgetId) => getString('${SettingKeys.widgetUriPrefixKey}$widgetId'); void setWidgetUri(int widgetId, String? newValue) => set('${SettingKeys.widgetUriPrefixKey}$widgetId', newValue); // platform settings void _onPlatformSettingsChanged(Map? fields) { fields?.forEach((key, value) { switch (key) { case SettingKeys.platformAccelerometerRotationKey: if (value is num) { isRotationLocked = value == 0; } case SettingKeys.platformTransitionAnimationScaleKey: if (value is num) { areAnimationsRemoved = value == 0; } case SettingKeys.platformLongPressTimeoutMillisKey: if (value is num) { longPressTimeoutMillis = value.toInt(); } } }); } bool get isRotationLocked => getBool(SettingKeys.platformAccelerometerRotationKey) ?? SettingsDefaults.isRotationLocked; set isRotationLocked(bool newValue) => set(SettingKeys.platformAccelerometerRotationKey, newValue); bool get areAnimationsRemoved => getBool(SettingKeys.platformTransitionAnimationScaleKey) ?? SettingsDefaults.areAnimationsRemoved; set areAnimationsRemoved(bool newValue) => set(SettingKeys.platformTransitionAnimationScaleKey, newValue); Duration get longPressTimeout => Duration(milliseconds: getInt(SettingKeys.platformLongPressTimeoutMillisKey) ?? kLongPressTimeout.inMilliseconds); set longPressTimeoutMillis(int newValue) => set(SettingKeys.platformLongPressTimeoutMillisKey, newValue); // import/export Map export() => Map.fromEntries( store.getKeys().whereNot(SettingKeys.isInternalKey).map((k) => MapEntry(k, store.get(k))), ); Future import(dynamic jsonMap) async { if (jsonMap is Map) { // clear to restore defaults await reset(includeInternalKeys: false); // apply user modifications jsonMap.forEach((key, newValue) { final oldValue = store.get(key); if (newValue == null) { store.remove(key); } else if (key.startsWith(SettingKeys.tileExtentPrefixKey)) { if (newValue is double) { store.setDouble(key, newValue); } else { debugPrint('failed to import key=$key, value=$newValue is not a double'); } } else if (key.startsWith(SettingKeys.tileLayoutPrefixKey)) { if (newValue is String) { store.setString(key, newValue); } else { debugPrint('failed to import key=$key, value=$newValue is not a string'); } } else if (key.startsWith(SettingKeys.showTitleQueryPrefixKey)) { if (newValue is bool) { store.setBool(key, newValue); } else { debugPrint('failed to import key=$key, value=$newValue is not a bool'); } } else { switch (key) { case SettingKeys.convertQualityKey: case SettingKeys.screenSaverIntervalKey: case SettingKeys.slideshowIntervalKey: if (newValue is int) { store.setInt(key, newValue); } else { debugPrint('failed to import key=$key, value=$newValue is not an int'); } case SettingKeys.subtitleFontSizeKey: case SettingKeys.infoMapZoomKey: if (newValue is double) { store.setDouble(key, newValue); } else { debugPrint('failed to import key=$key, value=$newValue is not a double'); } case SettingKeys.isInstalledAppAccessAllowedKey: case SettingKeys.isErrorReportingAllowedKey: case SettingKeys.forceWesternArabicNumeralsKey: case SettingKeys.enableDynamicColorKey: case SettingKeys.enableBlurEffectKey: case SettingKeys.enableBottomNavigationBarKey: case SettingKeys.mustBackTwiceToExitKey: case SettingKeys.confirmCreateVaultKey: case SettingKeys.confirmDeleteForeverKey: case SettingKeys.confirmMoveToBinKey: case SettingKeys.confirmMoveUndatedItemsKey: case SettingKeys.confirmAfterMoveToBinKey: case SettingKeys.setMetadataDateBeforeFileOpKey: case SettingKeys.collectionSortReverseKey: case SettingKeys.showThumbnailFavouriteKey: case SettingKeys.showThumbnailHdrKey: case SettingKeys.showThumbnailMotionPhotoKey: case SettingKeys.showThumbnailRatingKey: case SettingKeys.showThumbnailRawKey: case SettingKeys.showThumbnailVideoDurationKey: case SettingKeys.albumSortReverseKey: case SettingKeys.countrySortReverseKey: case SettingKeys.stateSortReverseKey: case SettingKeys.placeSortReverseKey: case SettingKeys.tagSortReverseKey: case SettingKeys.showOverlayOnOpeningKey: case SettingKeys.showOverlayMinimapKey: case SettingKeys.showOverlayInfoKey: case SettingKeys.showOverlayDescriptionKey: case SettingKeys.showOverlayRatingTagsKey: case SettingKeys.showOverlayShootingDetailsKey: case SettingKeys.showOverlayThumbnailPreviewKey: case SettingKeys.viewerGestureSideTapNextKey: case SettingKeys.viewerUseCutoutKey: case SettingKeys.enableMotionPhotoAutoPlayKey: case SettingKeys.enableVideoHardwareAccelerationKey: case SettingKeys.videoGestureDoubleTapTogglePlayKey: case SettingKeys.videoGestureSideDoubleTapSeekKey: case SettingKeys.videoGestureVerticalDragBrightnessVolumeKey: case SettingKeys.subtitleShowOutlineKey: case SettingKeys.tagEditorCurrentFilterSectionExpandedKey: case SettingKeys.convertWriteMetadataKey: case SettingKeys.saveSearchHistoryKey: case SettingKeys.showPinchGestureAlternativesKey: case SettingKeys.filePickerShowHiddenFilesKey: case SettingKeys.screenSaverFillScreenKey: case SettingKeys.screenSaverAnimatedZoomEffectKey: case SettingKeys.slideshowRepeatKey: case SettingKeys.slideshowShuffleKey: case SettingKeys.slideshowFillScreenKey: case SettingKeys.slideshowAnimatedZoomEffectKey: if (newValue is bool) { store.setBool(key, newValue); } else { debugPrint('failed to import key=$key, value=$newValue is not a bool'); } case SettingKeys.localeKey: case SettingKeys.displayRefreshRateModeKey: case SettingKeys.themeBrightnessKey: case SettingKeys.themeColorModeKey: case SettingKeys.maxBrightnessKey: case SettingKeys.keepScreenOnKey: case SettingKeys.homePageKey: case SettingKeys.homeCustomExplorerPathKey: case SettingKeys.collectionGroupFactorKey: case SettingKeys.collectionSortFactorKey: case SettingKeys.thumbnailLocationIconKey: case SettingKeys.thumbnailTagIconKey: case SettingKeys.albumGroupFactorKey: case SettingKeys.albumSortFactorKey: case SettingKeys.countrySortFactorKey: case SettingKeys.stateSortFactorKey: case SettingKeys.placeSortFactorKey: case SettingKeys.tagSortFactorKey: case SettingKeys.imageBackgroundKey: case SettingKeys.videoAutoPlayModeKey: case SettingKeys.videoBackgroundModeKey: case SettingKeys.videoLoopModeKey: case SettingKeys.videoResumptionModeKey: case SettingKeys.subtitleTextAlignmentKey: case SettingKeys.subtitleTextPositionKey: case SettingKeys.subtitleTextColorKey: case SettingKeys.subtitleBackgroundColorKey: case SettingKeys.tagEditorExpandedSectionKey: case SettingKeys.convertMimeTypeKey: case SettingKeys.mapStyleKey: case SettingKeys.mapDefaultCenterKey: case SettingKeys.coordinateFormatKey: case SettingKeys.unitSystemKey: case SettingKeys.accessibilityAnimationsKey: case SettingKeys.timeToTakeActionKey: case SettingKeys.screenSaverTransitionKey: case SettingKeys.screenSaverVideoPlaybackKey: case SettingKeys.slideshowTransitionKey: case SettingKeys.slideshowVideoPlaybackKey: if (newValue is String) { store.setString(key, newValue); } else { debugPrint('failed to import key=$key, value=$newValue is not a string'); } case SettingKeys.homeCustomCollectionKey: case SettingKeys.drawerTypeBookmarksKey: case SettingKeys.drawerAlbumBookmarksKey: case SettingKeys.drawerPageBookmarksKey: case SettingKeys.collectionBurstPatternsKey: case SettingKeys.pinnedFiltersKey: case SettingKeys.hiddenFiltersKey: case SettingKeys.deactivatedHiddenFiltersKey: case SettingKeys.collectionBrowsingQuickActionsKey: case SettingKeys.collectionSelectionQuickActionsKey: case SettingKeys.viewerQuickActionsKey: case SettingKeys.videoControlActionsKey: case SettingKeys.screenSaverCollectionFiltersKey: if (newValue is List) { store.setStringList(key, newValue.cast()); } else { debugPrint('failed to import key=$key, value=$newValue is not a list'); } } } if (oldValue != newValue) { notifyKeyChange(key, oldValue, newValue); } }); await sanitize(); notifyListeners(); } } @override void notifyKeyChange(String key, dynamic oldValue, dynamic newValue) { _updateStreamController.add(SettingsChangedEvent(key, oldValue, newValue)); if (key.startsWith(SettingKeys.tileExtentPrefixKey)) { _updateTileExtentStreamController.add(SettingsChangedEvent(key, oldValue, newValue)); } } }