diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6236fc447..c27970268 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -31,4 +31,4 @@ jobs: run: flutter analyze - name: Unit tests. - run: flutter test + run: flutter test --no-sound-null-safety diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e5b75c55..a7bae149a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: run: flutter analyze - name: Unit tests. - run: flutter test + run: flutter test --no-sound-null-safety - name: Build signed artifacts. # `KEY_JKS` should contain the result of: diff --git a/lib/geo/countries.dart b/lib/geo/countries.dart index dfea76cb9..3c3aff4fc 100644 --- a/lib/geo/countries.dart +++ b/lib/geo/countries.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/geo/topojson.dart'; +import 'package:collection/collection.dart'; import 'package:country_code/country_code.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -13,17 +14,17 @@ class CountryTopology { CountryTopology._private(); - Topology _topology; + Topology? _topology; - Future getTopology() => _topology != null ? SynchronousFuture(_topology) : rootBundle.loadString(topoJsonAsset).then(TopoJson().parse); + Future getTopology() => _topology != null ? SynchronousFuture(_topology) : rootBundle.loadString(topoJsonAsset).then(TopoJson().parse); // returns the country containing given coordinates - Future countryCode(LatLng position) async { + Future countryCode(LatLng position) async { return _countryOfNumeric(await numericCode(position)); } // returns the ISO 3166-1 numeric code of the country containing given coordinates - Future numericCode(LatLng position) async { + Future numericCode(LatLng position) async { final topology = await getTopology(); if (topology == null) return null; @@ -34,21 +35,25 @@ class CountryTopology { // returns a map of the given positions by country Future>> countryCodeMap(Set positions) async { final numericMap = await numericCodeMap(positions); - numericMap.remove(null); - final codeMap = numericMap.map((key, value) { - final code = _countryOfNumeric(key); - return code == null ? null : MapEntry(code, value); - }); - codeMap.remove(null); - return codeMap; + if (numericMap == null) return {}; + + final codeMapEntries = numericMap.entries + .map((kv) { + final code = _countryOfNumeric(kv.key); + return MapEntry(code, kv.value); + }) + .where((kv) => kv.key != null) + .cast>>(); + + return Map.fromEntries(codeMapEntries); } // returns a map of the given positions by the ISO 3166-1 numeric code of the country containing them - Future>> numericCodeMap(Set positions) async { + Future>?> numericCodeMap(Set positions) async { final topology = await getTopology(); if (topology == null) return null; - return compute(_isoNumericCodeMap, _IsoNumericCodeMapData(topology, positions)); + return compute<_IsoNumericCodeMapData, Map>>(_isoNumericCodeMap, _IsoNumericCodeMapData(topology, positions)); } static Future>> _isoNumericCodeMap(_IsoNumericCodeMapData data) async { @@ -58,19 +63,21 @@ class CountryTopology { final byCode = >{}; for (final position in data.positions) { final code = _getNumeric(topology, countries, position); - byCode[code] = (byCode[code] ?? {})..add(position); + if (code != null) { + byCode[code] = (byCode[code] ?? {})..add(position); + } } return byCode; } catch (error, stack) { // an unhandled error in a spawn isolate would make the app crash debugPrint('failed to get country codes with error=$error\n$stack'); } - return null; + return {}; } - static int _getNumeric(Topology topology, List mruCountries, LatLng position) { + static int? _getNumeric(Topology topology, List mruCountries, LatLng position) { final point = [position.longitude, position.latitude]; - final hit = mruCountries.firstWhere((country) => country.containsPoint(topology, point), orElse: () => null); + final hit = mruCountries.firstWhereOrNull((country) => country.containsPoint(topology, point)); if (hit == null) return null; // promote hit countries, assuming given positions are likely to come from the same countries @@ -79,12 +86,12 @@ class CountryTopology { mruCountries.insert(0, hit); } - final idString = (hit.id as String); + final idString = (hit.id as String?); final code = idString == null ? null : int.tryParse(idString); return code; } - static CountryCode _countryOfNumeric(int numeric) { + static CountryCode? _countryOfNumeric(int? numeric) { if (numeric == null) return null; try { return CountryCode.ofNumeric(numeric); diff --git a/lib/geo/format.dart b/lib/geo/format.dart index 517a74c2f..a52dfd192 100644 --- a/lib/geo/format.dart +++ b/lib/geo/format.dart @@ -23,7 +23,6 @@ String _decimal2sexagesimal(final double degDecimal) { // returns coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E'] List toDMS(LatLng latLng) { - if (latLng == null) return []; final lat = latLng.latitude; final lng = latLng.longitude; return [ diff --git a/lib/geo/topojson.dart b/lib/geo/topojson.dart index 7a4f029b6..5ef57813c 100644 --- a/lib/geo/topojson.dart +++ b/lib/geo/topojson.dart @@ -5,11 +5,11 @@ import 'package:flutter/foundation.dart'; // cf https://github.com/topojson/topojson-specification class TopoJson { - Future parse(String data) async { - return compute(_isoParse, data); + Future parse(String data) async { + return compute(_isoParse, data); } - static Topology _isoParse(String jsonData) { + static Topology? _isoParse(String jsonData) { try { final data = json.decode(jsonData) as Map; return Topology.parse(data); @@ -23,7 +23,7 @@ class TopoJson { enum TopoJsonObjectType { topology, point, multipoint, linestring, multilinestring, polygon, multipolygon, geometrycollection } -TopoJsonObjectType _parseTopoJsonObjectType(String data) { +TopoJsonObjectType? _parseTopoJsonObjectType(String? data) { switch (data) { case 'Topology': return TopoJsonObjectType.topology; @@ -46,7 +46,7 @@ TopoJsonObjectType _parseTopoJsonObjectType(String data) { } class TopologyJsonObject { - final List bbox; + final List? bbox; TopologyJsonObject.parse(Map data) : bbox = data.containsKey('bbox') ? (data['bbox'] as List).cast().toList() : null; } @@ -54,10 +54,19 @@ class TopologyJsonObject { class Topology extends TopologyJsonObject { final Map objects; final List>> arcs; - final Transform transform; + final Transform? transform; Topology.parse(Map data) - : objects = (data['objects'] as Map).cast().map((name, geometry) => MapEntry(name, Geometry.build(geometry))), + : objects = Map.fromEntries((data['objects'] as Map) + .cast() + .entries + .map((kv) { + final name = kv.key; + final geometry = Geometry.build(kv.value); + return geometry != null ? MapEntry(name, geometry) : null; + }) + .where((kv) => kv != null) + .cast>()), arcs = (data['arcs'] as List).cast().map((arc) => arc.cast().map((position) => position.cast()).toList()).toList(), transform = data.containsKey('transform') ? Transform.parse((data['transform'] as Map).cast()) : null, super.parse(data); @@ -69,8 +78,8 @@ class Topology extends TopologyJsonObject { var x = 0, y = 0; arc = arc.map((quantized) { final absolute = List.of(quantized); - absolute[0] = (x += quantized[0]) * transform.scale[0] + transform.translate[0]; - absolute[1] = (y += quantized[1]) * transform.scale[1] + transform.translate[1]; + absolute[0] = (x += quantized[0] as int) * transform!.scale[0] + transform!.translate[0]; + absolute[1] = (y += quantized[1] as int) * transform!.scale[1] + transform!.translate[1]; return absolute; }).toList(); } @@ -126,17 +135,18 @@ class Transform { abstract class Geometry extends TopologyJsonObject { final dynamic id; - final Map properties; + final Map? properties; Geometry.parse(Map data) : id = data.containsKey('id') ? data['id'] : null, - properties = data.containsKey('properties') ? data['properties'] as Map : null, + properties = data.containsKey('properties') ? data['properties'] as Map? : null, super.parse(data); - static Geometry build(Map data) { - final type = _parseTopoJsonObjectType(data['type'] as String); + static Geometry? build(Map data) { + final type = _parseTopoJsonObjectType(data['type'] as String?); switch (type) { case TopoJsonObjectType.topology: + case null: return null; case TopoJsonObjectType.point: return Point.parse(data); @@ -153,7 +163,6 @@ abstract class Geometry extends TopologyJsonObject { case TopoJsonObjectType.geometrycollection: return GeometryCollection.parse(data); } - return null; } bool containsPoint(Topology topology, List point) => false; @@ -198,11 +207,11 @@ class Polygon extends Geometry { : arcs = (data['arcs'] as List).cast().map((arc) => arc.cast()).toList(), super.parse(data); - List>> _rings; + List>>? _rings; List>> rings(Topology topology) { _rings ??= topology._decodePolygonArcs(arcs); - return _rings; + return _rings!; } @override @@ -218,11 +227,11 @@ class MultiPolygon extends Geometry { : arcs = (data['arcs'] as List).cast().map((polygon) => polygon.cast().map((arc) => arc.cast()).toList()).toList(), super.parse(data); - List>>> _polygons; + List>>>? _polygons; List>>> polygons(Topology topology) { _polygons ??= topology._decodeMultiPolygonArcs(arcs); - return _polygons; + return _polygons!; } @override @@ -235,7 +244,7 @@ class GeometryCollection extends Geometry { final List geometries; GeometryCollection.parse(Map data) - : geometries = (data['geometries'] as List).cast>().map(Geometry.build).toList(), + : geometries = (data['geometries'] as List).cast>().map(Geometry.build).where((geometry) => geometry != null).cast().toList(), super.parse(data); @override diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart index cbc7490e5..a51ea806c 100644 --- a/lib/image_providers/app_icon_image_provider.dart +++ b/lib/image_providers/app_icon_image_provider.dart @@ -6,11 +6,10 @@ import 'package:flutter/widgets.dart'; class AppIconImage extends ImageProvider { const AppIconImage({ - @required this.packageName, - @required this.size, + required this.packageName, + required this.size, this.scale = 1.0, - }) : assert(packageName != null), - assert(scale != null); + }); final String packageName; final double size; @@ -39,7 +38,7 @@ class AppIconImage extends ImageProvider { Future _loadAsync(AppIconImageKey key, DecoderCallback decode) async { try { final bytes = await AndroidAppService.getAppIcon(key.packageName, key.size); - if (bytes == null) { + if (bytes.isEmpty) { throw StateError('$packageName app icon loading failed'); } return await decode(bytes); @@ -56,9 +55,9 @@ class AppIconImageKey { final double scale; const AppIconImageKey({ - @required this.packageName, - @required this.size, - this.scale, + required this.packageName, + required this.size, + this.scale = 1.0, }); @override diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index 136bcc626..1808b8e8a 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart'; class RegionProvider extends ImageProvider { final RegionProviderKey key; - RegionProvider(this.key) : assert(key != null); + RegionProvider(this.key); @override Future obtainKey(ImageConfiguration configuration) { @@ -43,7 +43,7 @@ class RegionProvider extends ImageProvider { pageId: pageId, taskKey: key, ); - if (bytes == null) { + if (bytes.isEmpty) { throw StateError('$uri ($mimeType) region loading failed'); } return await decode(bytes); @@ -66,30 +66,24 @@ class RegionProviderKey { // do not store the entry as it is, because the key should be constant // but the entry attributes may change over time final String uri, mimeType; - final int pageId, rotationDegrees, sampleSize; + final int? pageId; + final int rotationDegrees, sampleSize; final bool isFlipped; final Rectangle region; final Size imageSize; final double scale; const RegionProviderKey({ - @required this.uri, - @required this.mimeType, - @required this.pageId, - @required this.rotationDegrees, - @required this.isFlipped, - @required this.sampleSize, - @required this.region, - @required this.imageSize, + required this.uri, + required this.mimeType, + required this.pageId, + required this.rotationDegrees, + required this.isFlipped, + required this.sampleSize, + required this.region, + required this.imageSize, this.scale = 1.0, - }) : assert(uri != null), - assert(mimeType != null), - assert(rotationDegrees != null), - assert(isFlipped != null), - assert(sampleSize != null), - assert(region != null), - assert(imageSize != null), - assert(scale != null); + }); @override bool operator ==(Object other) { diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index 0578b63e3..e318db79c 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart'; class ThumbnailProvider extends ImageProvider { final ThumbnailProviderKey key; - ThumbnailProvider(this.key) : assert(key != null); + ThumbnailProvider(this.key); @override Future obtainKey(ImageConfiguration configuration) { @@ -43,7 +43,7 @@ class ThumbnailProvider extends ImageProvider { extent: key.extent, taskKey: key, ); - if (bytes == null) { + if (bytes.isEmpty) { throw StateError('$uri ($mimeType) loading failed'); } return await decode(bytes); @@ -66,25 +66,21 @@ class ThumbnailProviderKey { // do not store the entry as it is, because the key should be constant // but the entry attributes may change over time final String uri, mimeType; - final int pageId, rotationDegrees; + final int? pageId; + final int rotationDegrees; final bool isFlipped; final int dateModifiedSecs; final double extent; const ThumbnailProviderKey({ - @required this.uri, - @required this.mimeType, - @required this.pageId, - @required this.rotationDegrees, - @required this.isFlipped, - @required this.dateModifiedSecs, + required this.uri, + required this.mimeType, + required this.pageId, + required this.rotationDegrees, + required this.isFlipped, + required this.dateModifiedSecs, this.extent = 0, - }) : assert(uri != null), - assert(mimeType != null), - assert(rotationDegrees != null), - assert(isFlipped != null), - assert(dateModifiedSecs != null), - assert(extent != null); + }); @override bool operator ==(Object other) { diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index c6b1c31fa..80ea53f3d 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -8,20 +8,19 @@ import 'package:pedantic/pedantic.dart'; class UriImage extends ImageProvider { final String uri, mimeType; - final int pageId, rotationDegrees, expectedContentLength; + final int? pageId, rotationDegrees, expectedContentLength; final bool isFlipped; final double scale; const UriImage({ - @required this.uri, - @required this.mimeType, - @required this.pageId, - @required this.rotationDegrees, - @required this.isFlipped, + required this.uri, + required this.mimeType, + required this.pageId, + required this.rotationDegrees, + required this.isFlipped, this.expectedContentLength, this.scale = 1.0, - }) : assert(uri != null), - assert(scale != null); + }); @override Future obtainKey(ImageConfiguration configuration) { @@ -60,7 +59,7 @@ class UriImage extends ImageProvider { )); }, ); - if (bytes == null) { + if (bytes.isEmpty) { throw StateError('$uri ($mimeType) loading failed'); } return await decode(bytes); diff --git a/lib/image_providers/uri_picture_provider.dart b/lib/image_providers/uri_picture_provider.dart index 2f4071c8c..8a13e7125 100644 --- a/lib/image_providers/uri_picture_provider.dart +++ b/lib/image_providers/uri_picture_provider.dart @@ -2,15 +2,13 @@ import 'package:aves/services/services.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:pedantic/pedantic.dart'; class UriPicture extends PictureProvider { const UriPicture({ - @required this.uri, - @required this.mimeType, - ColorFilter colorFilter, - }) : assert(uri != null), - super(colorFilter); + required this.uri, + required this.mimeType, + ColorFilter? colorFilter, + }) : super(colorFilter); final String uri, mimeType; @@ -20,25 +18,30 @@ class UriPicture extends PictureProvider { } @override - PictureStreamCompleter load(UriPicture key, {PictureErrorListener onError}) { + PictureStreamCompleter load(UriPicture key, {PictureErrorListener? onError}) { return OneFramePictureStreamCompleter(_loadAsync(key, onError: onError), informationCollector: () sync* { yield DiagnosticsProperty('uri', uri); }); } - Future _loadAsync(UriPicture key, {PictureErrorListener onError}) async { + Future _loadAsync(UriPicture key, {PictureErrorListener? onError}) async { assert(key == this); final data = await imageFileService.getSvg(uri, mimeType); - if (data == null || data.isEmpty) { + if (data.isEmpty) { return null; } final decoder = SvgPicture.svgByteDecoder; if (onError != null) { - final future = decoder(data, colorFilter, key.toString()); - unawaited(future.catchError(onError)); - return future; + return decoder( + data, + colorFilter, + key.toString(), + ).catchError((error, stack) async { + onError(error, stack); + return Future.error(error, stack); + }); } return decoder(data, colorFilter, key.toString()); } diff --git a/lib/main.dart b/lib/main.dart index 85a98d606..650b10710 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,31 +1,9 @@ +// @dart=2.9 import 'dart:isolate'; -import 'dart:ui'; -import 'package:aves/app_mode.dart'; -import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/media_store_source.dart'; -import 'package:aves/services/services.dart'; -import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/theme/themes.dart'; -import 'package:aves/utils/debouncer.dart'; -import 'package:aves/widgets/common/behaviour/route_tracker.dart'; -import 'package:aves/widgets/common/behaviour/routes.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; -import 'package:aves/widgets/home_page.dart'; -import 'package:aves/widgets/welcome_page.dart'; -import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:firebase_analytics/observer.dart'; -import 'package:firebase_core/firebase_core.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:overlay_support/overlay_support.dart'; -import 'package:provider/provider.dart'; void main() { // HttpClient.enableTimelineLogging = true; // enable network traffic logging @@ -41,147 +19,3 @@ void main() { runApp(AvesApp()); } - -class AvesApp extends StatefulWidget { - @override - _AvesAppState createState() => _AvesAppState(); -} - -class _AvesAppState extends State { - final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); - Future _appSetup; - final _mediaStoreSource = MediaStoreSource(); - final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); - final Set changedUris = {}; - - // observers are not registered when using the same list object with different items - // the list itself needs to be reassigned - List _navigatorObservers = []; - final EventChannel _contentChangeChannel = EventChannel('deckers.thibault/aves/contentchange'); - final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); - final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); - - Widget getFirstPage({Map intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : WelcomePage(); - - @override - void initState() { - super.initState(); - initPlatformServices(); - _appSetup = _setup(); - _contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String)); - _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map)); - } - - @override - Widget build(BuildContext context) { - // place the settings provider above `MaterialApp` - // so it can be used during navigation transitions - return ChangeNotifierProvider.value( - value: settings, - child: ListenableProvider>.value( - value: appModeNotifier, - child: Provider.value( - value: _mediaStoreSource, - child: HighlightInfoProvider( - child: OverlaySupport( - child: FutureBuilder( - future: _appSetup, - builder: (context, snapshot) { - final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; - final home = initialized - ? getFirstPage() - : Scaffold( - body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(), - ); - return Selector( - selector: (context, s) => s.locale, - builder: (context, settingsLocale, child) { - return MaterialApp( - navigatorKey: _navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - onGenerateTitle: (context) => context.l10n.appName, - darkTheme: Themes.darkTheme, - themeMode: ThemeMode.dark, - locale: settingsLocale, - localizationsDelegates: [ - ...AppLocalizations.localizationsDelegates, - ], - supportedLocales: AppLocalizations.supportedLocales, - ); - }); - }, - ), - ), - ), - ), - ), - ); - } - - Widget _buildError(Object error) { - return Container( - alignment: Alignment.center, - padding: EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(AIcons.error), - SizedBox(height: 16), - Text(error.toString()), - ], - ), - ); - } - - Future _setup() async { - await Firebase.initializeApp().then((app) { - final crashlytics = FirebaseCrashlytics.instance; - FlutterError.onError = crashlytics.recordFlutterError; - crashlytics.setCustomKey('locales', window.locales.join(', ')); - final now = DateTime.now(); - crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})'); - crashlytics.setCustomKey( - 'build_mode', - kReleaseMode - ? 'release' - : kProfileMode - ? 'profile' - : 'debug'); - }); - await settings.init(); - await settings.initFirebase(); - _navigatorObservers = [ - FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()), - CrashlyticsRouteTracker(), - ]; - } - - void _onNewIntent(Map intentData) { - debugPrint('$runtimeType onNewIntent with intentData=$intentData'); - - // do not reset when relaunching the app - if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; - - FirebaseCrashlytics.instance.log('New intent'); - _navigatorKey.currentState.pushReplacement(DirectMaterialPageRoute( - settings: RouteSettings(name: HomePage.routeName), - builder: (_) => getFirstPage(intentData: intentData), - )); - } - - void _onContentChange(String uri) { - if (uri != null) changedUris.add(uri); - if (changedUris.isNotEmpty) { - _contentChangeDebouncer(() async { - final todo = changedUris.toSet(); - changedUris.clear(); - final tempUris = await _mediaStoreSource.refreshUris(todo); - if (tempUris.isNotEmpty) { - changedUris.addAll(tempUris); - _onContentChange(null); - } - }); - } - } -} diff --git a/lib/model/actions/chip_actions.dart b/lib/model/actions/chip_actions.dart index 412f745a1..7e531c168 100644 --- a/lib/model/actions/chip_actions.dart +++ b/lib/model/actions/chip_actions.dart @@ -42,7 +42,6 @@ extension ExtraChipAction on ChipAction { case ChipAction.setCover: return context.l10n.chipActionSetCover; } - return null; } IconData getIcon() { @@ -65,6 +64,5 @@ extension ExtraChipAction on ChipAction { case ChipAction.setCover: return AIcons.setCover; } - return null; } } diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 04e4865e1..f709c0e8d 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -91,10 +91,9 @@ extension ExtraEntryAction on EntryAction { case EntryAction.debug: return 'Debug'; } - return null; } - IconData getIcon() { + IconData? getIcon() { switch (this) { // in app actions case EntryAction.toggleFavourite: @@ -129,6 +128,5 @@ extension ExtraEntryAction on EntryAction { case EntryAction.debug: return AIcons.debug; } - return null; } } diff --git a/lib/model/availability.dart b/lib/model/availability.dart index 43c983f23..a56bfa9ca 100644 --- a/lib/model/availability.dart +++ b/lib/model/availability.dart @@ -11,17 +11,17 @@ import 'package:version/version.dart'; abstract class AvesAvailability { void onResume(); - Future get isConnected; + Future get isConnected; - Future get hasPlayServices; + Future get hasPlayServices; Future get canLocatePlaces; - Future get isNewVersionAvailable; + Future get isNewVersionAvailable; } class LiveAvesAvailability implements AvesAvailability { - bool _isConnected, _hasPlayServices, _isNewVersionAvailable; + bool? _isConnected, _hasPlayServices, _isNewVersionAvailable; LiveAvesAvailability() { Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult); @@ -31,11 +31,11 @@ class LiveAvesAvailability implements AvesAvailability { void onResume() => _isConnected = null; @override - Future get isConnected async { - if (_isConnected != null) return SynchronousFuture(_isConnected); + Future get isConnected async { + if (_isConnected != null) return SynchronousFuture(_isConnected!); final result = await (Connectivity().checkConnectivity()); _updateConnectivityFromResult(result); - return _isConnected; + return _isConnected!; } void _updateConnectivityFromResult(ConnectivityResult result) { @@ -47,12 +47,12 @@ class LiveAvesAvailability implements AvesAvailability { } @override - Future get hasPlayServices async { - if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices); + Future get hasPlayServices async { + if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices!); final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); _hasPlayServices = result == GooglePlayServicesAvailability.success; debugPrint('Device has Play Services=$_hasPlayServices'); - return _hasPlayServices; + return _hasPlayServices!; } // local geocoding with `geocoder` requires Play Services @@ -60,28 +60,28 @@ class LiveAvesAvailability implements AvesAvailability { Future get canLocatePlaces => Future.wait([isConnected, hasPlayServices]).then((results) => results.every((result) => result)); @override - Future get isNewVersionAvailable async { - if (_isNewVersionAvailable != null) return SynchronousFuture(_isNewVersionAvailable); + Future get isNewVersionAvailable async { + if (_isNewVersionAvailable != null) return SynchronousFuture(_isNewVersionAvailable!); final now = DateTime.now(); final dueDate = settings.lastVersionCheckDate.add(Durations.lastVersionCheckInterval); if (now.isBefore(dueDate)) { _isNewVersionAvailable = false; - return SynchronousFuture(_isNewVersionAvailable); + return SynchronousFuture(_isNewVersionAvailable!); } if (!(await isConnected)) return false; Version version(String s) => Version.parse(s.replaceFirst('v', '')); final currentTag = (await PackageInfo.fromPlatform()).version; - final latestTag = (await GitHub().repositories.getLatestRelease(RepositorySlug('deckerst', 'aves'))).tagName; + final latestTag = (await GitHub().repositories.getLatestRelease(RepositorySlug('deckerst', 'aves'))).tagName!; _isNewVersionAvailable = version(latestTag) > version(currentTag); - if (_isNewVersionAvailable) { + if (_isNewVersionAvailable!) { debugPrint('Aves $latestTag is available on github'); } else { debugPrint('Aves $currentTag is the latest version'); settings.lastVersionCheckDate = now; } - return _isNewVersionAvailable; + return _isNewVersionAvailable!; } } diff --git a/lib/model/covers.dart b/lib/model/covers.dart index 5a083438a..ffebf4b98 100644 --- a/lib/model/covers.dart +++ b/lib/model/covers.dart @@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/services/services.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -18,19 +19,19 @@ class Covers with ChangeNotifier { int get count => _rows.length; - int coverContentId(CollectionFilter filter) => _rows.firstWhere((row) => row.filter == filter, orElse: () => null)?.contentId; + int? coverContentId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.contentId; - Future set(CollectionFilter filter, int contentId) async { + Future set(CollectionFilter filter, int? contentId) async { // erase contextual properties from filters before saving them if (filter is AlbumFilter) { - filter = AlbumFilter((filter as AlbumFilter).album, null); + filter = AlbumFilter(filter.album, null); } - final row = CoverRow(filter: filter, contentId: contentId); _rows.removeWhere((row) => row.filter == filter); if (contentId == null) { - await metadataDb.removeCovers({row}); + await metadataDb.removeCovers({filter}); } else { + final row = CoverRow(filter: filter, contentId: contentId); _rows.add(row); await metadataDb.addCovers({row}); } @@ -46,11 +47,11 @@ class Covers with ChangeNotifier { final filter = oldRow.filter; _rows.remove(oldRow); if (filter.test(entry)) { - final newRow = CoverRow(filter: filter, contentId: entry.contentId); + final newRow = CoverRow(filter: filter, contentId: entry.contentId!); await metadataDb.updateCoverEntryId(oldRow.contentId, newRow); _rows.add(newRow); } else { - await metadataDb.removeCovers({oldRow}); + await metadataDb.removeCovers({filter}); } } @@ -61,7 +62,7 @@ class Covers with ChangeNotifier { final contentIds = entries.map((entry) => entry.contentId).toSet(); final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet(); - await metadataDb.removeCovers(removedRows); + await metadataDb.removeCovers(removedRows.map((row) => row.filter).toSet()); _rows.removeAll(removedRows); notifyListeners(); @@ -81,13 +82,15 @@ class CoverRow { final int contentId; const CoverRow({ - @required this.filter, - @required this.contentId, + required this.filter, + required this.contentId, }); - factory CoverRow.fromMap(Map map) { + static CoverRow? fromMap(Map map) { + final filter = CollectionFilter.fromJson(map['filter']); + if (filter == null) return null; return CoverRow( - filter: CollectionFilter.fromJson(map['filter']), + filter: filter, contentId: map['contentId'], ); } diff --git a/lib/model/entry.dart b/lib/model/entry.dart index e2aaccb58..631770a48 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -22,22 +22,22 @@ import '../ref/mime_types.dart'; class AvesEntry { String uri; - String _path, _directory, _filename, _extension; - int pageId, contentId; + String? _path, _directory, _filename, _extension; + int? pageId, contentId; final String sourceMimeType; int width; int height; int sourceRotationDegrees; - final int sizeBytes; - String _sourceTitle; + final int? sizeBytes; + String? _sourceTitle; // `dateModifiedSecs` can be missing in viewer mode - int _dateModifiedSecs; - final int sourceDateTakenMillis; - final int durationMillis; - int _catalogDateMillis; - CatalogMetadata _catalogMetadata; - AddressDetails _addressDetails; + int? _dateModifiedSecs; + final int? sourceDateTakenMillis; + final int? durationMillis; + int? _catalogDateMillis; + CatalogMetadata? _catalogMetadata; + AddressDetails? _addressDetails; final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier(); @@ -51,21 +51,20 @@ class AvesEntry { ]; AvesEntry({ - this.uri, - String path, - this.contentId, - this.pageId, - this.sourceMimeType, - @required this.width, - @required this.height, - this.sourceRotationDegrees, - this.sizeBytes, - String sourceTitle, - int dateModifiedSecs, - this.sourceDateTakenMillis, - this.durationMillis, - }) : assert(width != null), - assert(height != null) { + required this.uri, + required String? path, + required this.contentId, + required this.pageId, + required this.sourceMimeType, + required this.width, + required this.height, + required this.sourceRotationDegrees, + required this.sizeBytes, + required String? sourceTitle, + required int? dateModifiedSecs, + required this.sourceDateTakenMillis, + required this.durationMillis, + }) { this.path = path; this.sourceTitle = sourceTitle; this.dateModifiedSecs = dateModifiedSecs; @@ -76,16 +75,17 @@ class AvesEntry { bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType); AvesEntry copyWith({ - String uri, - String path, - int contentId, - int dateModifiedSecs, + String? uri, + String? path, + int? contentId, + int? dateModifiedSecs, }) { final copyContentId = contentId ?? this.contentId; final copied = AvesEntry( uri: uri ?? this.uri, path: path ?? this.path, contentId: copyContentId, + pageId: null, sourceMimeType: sourceMimeType, width: width, height: height, @@ -106,17 +106,18 @@ class AvesEntry { factory AvesEntry.fromMap(Map map) { return AvesEntry( uri: map['uri'] as String, - path: map['path'] as String, - contentId: map['contentId'] as int, + path: map['path'] as String?, + pageId: null, + contentId: map['contentId'] as int?, sourceMimeType: map['sourceMimeType'] as String, - width: map['width'] as int ?? 0, - height: map['height'] as int ?? 0, - sourceRotationDegrees: map['sourceRotationDegrees'] as int ?? 0, - sizeBytes: map['sizeBytes'] as int, - sourceTitle: map['title'] as String, - dateModifiedSecs: map['dateModifiedSecs'] as int, - sourceDateTakenMillis: map['sourceDateTakenMillis'] as int, - durationMillis: map['durationMillis'] as int, + width: map['width'] as int? ?? 0, + height: map['height'] as int? ?? 0, + sourceRotationDegrees: map['sourceRotationDegrees'] as int? ?? 0, + sizeBytes: map['sizeBytes'] as int?, + sourceTitle: map['title'] as String?, + dateModifiedSecs: map['dateModifiedSecs'] as int?, + sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?, + durationMillis: map['durationMillis'] as int?, ); } @@ -150,27 +151,27 @@ class AvesEntry { @override String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}'; - set path(String path) { + set path(String? path) { _path = path; _directory = null; _filename = null; _extension = null; } - String get path => _path; + String? get path => _path; - String get directory { - _directory ??= path != null ? pContext.dirname(path) : null; + String? get directory { + _directory ??= path != null ? pContext.dirname(path!) : null; return _directory; } - String get filenameWithoutExtension { - _filename ??= path != null ? pContext.basenameWithoutExtension(path) : null; + String? get filenameWithoutExtension { + _filename ??= path != null ? pContext.basenameWithoutExtension(path!) : null; return _filename; } - String get extension { - _extension ??= path != null ? pContext.extension(path) : null; + String? get extension { + _extension ??= path != null ? pContext.extension(path!) : null; return _extension; } @@ -258,16 +259,16 @@ class AvesEntry { static const ratioSeparator = '\u2236'; static const resolutionSeparator = ' \u00D7 '; - bool get isSized => (width ?? 0) > 0 && (height ?? 0) > 0; + bool get isSized => width > 0 && height > 0; String get resolutionText { - final ws = width ?? '?'; - final hs = height ?? '?'; + final ws = width; + final hs = height; return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs'; } String get aspectRatioText { - if (width != null && height != null && width > 0 && height > 0) { + if (width > 0 && height > 0) { final gcd = width.gcd(height); final w = width ~/ gcd; final h = height ~/ gcd; @@ -288,24 +289,24 @@ class AvesEntry { return isRotated ? Size(h, w) : Size(w, h); } - int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null; + int get megaPixels => (width * height / 1000000).round(); - DateTime _bestDate; + DateTime? _bestDate; - DateTime get bestDate { + DateTime? get bestDate { if (_bestDate == null) { if ((_catalogDateMillis ?? 0) > 0) { - _bestDate = DateTime.fromMillisecondsSinceEpoch(_catalogDateMillis); + _bestDate = DateTime.fromMillisecondsSinceEpoch(_catalogDateMillis!); } else if ((sourceDateTakenMillis ?? 0) > 0) { - _bestDate = DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis); + _bestDate = DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis!); } else if ((dateModifiedSecs ?? 0) > 0) { - _bestDate = DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000); + _bestDate = DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs! * 1000); } } return _bestDate; } - int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees ?? 0; + int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees; set rotationDegrees(int rotationDegrees) { sourceRotationDegrees = rotationDegrees; @@ -316,78 +317,78 @@ class AvesEntry { set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped; - String get sourceTitle => _sourceTitle; + String? get sourceTitle => _sourceTitle; - set sourceTitle(String sourceTitle) { + set sourceTitle(String? sourceTitle) { _sourceTitle = sourceTitle; _bestTitle = null; } - int get dateModifiedSecs => _dateModifiedSecs; + int? get dateModifiedSecs => _dateModifiedSecs; - set dateModifiedSecs(int dateModifiedSecs) { + set dateModifiedSecs(int? dateModifiedSecs) { _dateModifiedSecs = dateModifiedSecs; _bestDate = null; } - DateTime get monthTaken { + DateTime? get monthTaken { final d = bestDate; return d == null ? null : DateTime(d.year, d.month); } - DateTime get dayTaken { + DateTime? get dayTaken { final d = bestDate; return d == null ? null : DateTime(d.year, d.month, d.day); } - String _durationText; + String? _durationText; String get durationText { _durationText ??= formatFriendlyDuration(Duration(milliseconds: durationMillis ?? 0)); - return _durationText; + return _durationText!; } // returns whether this entry has GPS coordinates // (0, 0) coordinates are considered invalid, as it is likely a default value - bool get hasGps => _catalogMetadata != null && _catalogMetadata.latitude != null && _catalogMetadata.longitude != null && (_catalogMetadata.latitude != 0 || _catalogMetadata.longitude != 0); + bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0; bool get hasAddress => _addressDetails != null; // has a place, or at least the full country name // derived from Google reverse geocoding addresses - bool get hasFineAddress => _addressDetails != null && (_addressDetails.place?.isNotEmpty == true || (_addressDetails.countryName?.length ?? 0) > 3); + bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3; - LatLng get latLng => hasGps ? LatLng(_catalogMetadata.latitude, _catalogMetadata.longitude) : null; + LatLng? get latLng => hasGps ? LatLng(_catalogMetadata!.latitude!, _catalogMetadata!.longitude!) : null; - String get geoUri { + String? get geoUri { if (!hasGps) return null; - final latitude = roundToPrecision(_catalogMetadata.latitude, decimals: 6); - final longitude = roundToPrecision(_catalogMetadata.longitude, decimals: 6); + final latitude = roundToPrecision(_catalogMetadata!.latitude!, decimals: 6); + final longitude = roundToPrecision(_catalogMetadata!.longitude!, decimals: 6); return 'geo:$latitude,$longitude?q=$latitude,$longitude'; } - List _xmpSubjects; + List? _xmpSubjects; List get xmpSubjects { - _xmpSubjects ??= _catalogMetadata?.xmpSubjects?.split(';')?.where((tag) => tag.isNotEmpty)?.toList() ?? []; - return _xmpSubjects; + _xmpSubjects ??= _catalogMetadata?.xmpSubjects?.split(';').where((tag) => tag.isNotEmpty).toList() ?? []; + return _xmpSubjects!; } - String _bestTitle; + String? _bestTitle; - String get bestTitle { - _bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata.xmpTitleDescription : sourceTitle; + String? get bestTitle { + _bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata!.xmpTitleDescription : sourceTitle; return _bestTitle; } - CatalogMetadata get catalogMetadata => _catalogMetadata; + CatalogMetadata? get catalogMetadata => _catalogMetadata; - set catalogDateMillis(int dateMillis) { + set catalogDateMillis(int? dateMillis) { _catalogDateMillis = dateMillis; _bestDate = null; } - set catalogMetadata(CatalogMetadata newMetadata) { + set catalogMetadata(CatalogMetadata? newMetadata) { final oldDateModifiedSecs = dateModifiedSecs; final oldRotationDegrees = rotationDegrees; final oldIsFlipped = isFlipped; @@ -424,14 +425,14 @@ class AvesEntry { } } - AddressDetails get addressDetails => _addressDetails; + AddressDetails? get addressDetails => _addressDetails; - set addressDetails(AddressDetails newAddress) { + set addressDetails(AddressDetails? newAddress) { _addressDetails = newAddress; addressChangeNotifier.notifyListeners(); } - Future locate({@required bool background}) async { + Future locate({required bool background}) async { if (!hasGps) return; await _locateCountry(); if (await availability.canLocatePlaces) { @@ -442,11 +443,11 @@ class AvesEntry { // quick reverse geocoding to find the country, using an offline asset Future _locateCountry() async { if (!hasGps || hasAddress) return; - final countryCode = await countryTopology.countryCode(latLng); + final countryCode = await countryTopology.countryCode(latLng!); setCountry(countryCode); } - void setCountry(CountryCode countryCode) { + void setCountry(CountryCode? countryCode) { if (hasFineAddress || countryCode == null) return; addressDetails = AddressDetails( contentId: contentId, @@ -455,25 +456,25 @@ class AvesEntry { ); } - String _geocoderLocale; + String? _geocoderLocale; String get geocoderLocale { - _geocoderLocale ??= (settings.locale ?? WidgetsBinding.instance.window.locale).toString(); - return _geocoderLocale; + _geocoderLocale ??= (settings.locale ?? WidgetsBinding.instance!.window.locale).toString(); + return _geocoderLocale!; } // full reverse geocoding, requiring Play Services and some connectivity - Future locatePlace({@required bool background}) async { + Future locatePlace({required bool background}) async { if (!hasGps || hasFineAddress) return; try { - Future> call() => GeocodingService.getAddress(latLng, geocoderLocale); + Future> call() => GeocodingService.getAddress(latLng!, geocoderLocale); final addresses = await (background ? servicePolicy.call( call, priority: ServiceCallPriority.getLocation, ) : call()); - if (addresses != null && addresses.isNotEmpty) { + if (addresses.isNotEmpty) { final address = addresses.first; final cc = address.countryCode; final cn = address.countryName; @@ -493,12 +494,12 @@ class AvesEntry { } } - Future findAddressLine() async { + Future findAddressLine() async { if (!hasGps) return null; try { - final addresses = await GeocodingService.getAddress(latLng, geocoderLocale); - if (addresses != null && addresses.isNotEmpty) { + final addresses = await GeocodingService.getAddress(latLng!, geocoderLocale); + if (addresses.isNotEmpty) { final address = addresses.first; return address.addressLine; } @@ -549,12 +550,12 @@ class AvesEntry { if (isFlipped is bool) this.isFlipped = isFlipped; await metadataDb.saveEntries({this}); - await metadataDb.saveMetadata({catalogMetadata}); + if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!}); metadataChangeNotifier.notifyListeners(); } - Future rotate({@required bool clockwise}) async { + Future rotate({required bool clockwise}) async { final newFields = await imageFileService.rotate(this, clockwise: clockwise); if (newFields.isEmpty) return false; @@ -579,7 +580,7 @@ class AvesEntry { } Future delete() { - Completer completer = Completer(); + final completer = Completer(); imageFileService.delete([this]).listen( (event) => completer.complete(event.success), onError: completer.completeError, @@ -593,7 +594,7 @@ class AvesEntry { } // when the entry image itself changed (e.g. after rotation) - Future _onImageChanged(int oldDateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped) async { + Future _onImageChanged(int? oldDateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped) async { if (oldDateModifiedSecs != dateModifiedSecs || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) { await EntryCache.evict(uri, mimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); imageChangeNotifier.notifyListeners(); @@ -626,15 +627,15 @@ class AvesEntry { // 1) title ascending // 2) extension ascending static int compareByName(AvesEntry a, AvesEntry b) { - final c = compareAsciiUpperCase(a.bestTitle, b.bestTitle); - return c != 0 ? c : compareAsciiUpperCase(a.extension, b.extension); + final c = compareAsciiUpperCase(a.bestTitle ?? '', b.bestTitle ?? ''); + return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? ''); } // compare by: // 1) size descending // 2) name ascending static int compareBySize(AvesEntry a, AvesEntry b) { - final c = b.sizeBytes.compareTo(a.sizeBytes); + final c = (b.sizeBytes ?? 0).compareTo(a.sizeBytes ?? 0); return c != 0 ? c : compareByName(a, b); } diff --git a/lib/model/entry_cache.dart b/lib/model/entry_cache.dart index b535c0973..1e3de138f 100644 --- a/lib/model/entry_cache.dart +++ b/lib/model/entry_cache.dart @@ -8,12 +8,12 @@ class EntryCache { static Future evict( String uri, String mimeType, - int dateModifiedSecs, + int? dateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped, ) async { // TODO TLAD provide pageId parameter for multi page items, if someday image editing features are added for them - int pageId; + int? pageId; // evict fullscreen image await UriImage( @@ -29,7 +29,7 @@ class EntryCache { uri: uri, mimeType: mimeType, pageId: pageId, - dateModifiedSecs: dateModifiedSecs, + dateModifiedSecs: dateModifiedSecs ?? 0, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, )).evict(); @@ -42,7 +42,7 @@ class EntryCache { uri: uri, mimeType: mimeType, pageId: pageId, - dateModifiedSecs: dateModifiedSecs, + dateModifiedSecs: dateModifiedSecs ?? 0, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, extent: extent, diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart index cc8c28374..464e906c1 100644 --- a/lib/model/entry_images.dart +++ b/lib/model/entry_images.dart @@ -5,7 +5,6 @@ import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/uri_image_provider.dart'; import 'package:aves/model/entry.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; extension ExtraAvesEntry on AvesEntry { @@ -30,11 +29,11 @@ extension ExtraAvesEntry on AvesEntry { ); } - RegionProvider getRegion({@required int sampleSize, Rectangle region}) { + RegionProvider getRegion({required int sampleSize, Rectangle? region}) { return RegionProvider(_getRegionProviderKey(sampleSize, region)); } - RegionProviderKey _getRegionProviderKey(int sampleSize, Rectangle region) { + RegionProviderKey _getRegionProviderKey(int sampleSize, Rectangle? region) { return RegionProviderKey( uri: uri, mimeType: mimeType, @@ -56,7 +55,7 @@ extension ExtraAvesEntry on AvesEntry { expectedContentLength: sizeBytes, ); - bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive; + bool _isReady(Object providerKey) => imageCache!.statusForKey(providerKey).keepAlive; ImageProvider getBestThumbnail(double extent) { final sizedThumbnailKey = _getThumbnailProviderKey(extent); diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart index 1ae6d54b9..524ad4be3 100644 --- a/lib/model/favourites.dart +++ b/lib/model/favourites.dart @@ -1,5 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/services/services.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -18,7 +19,7 @@ class Favourites with ChangeNotifier { bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId); - FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId, path: entry.path); + FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!); Future add(Iterable entries) async { final newRows = entries.map(_entryToRow); @@ -40,7 +41,7 @@ class Favourites with ChangeNotifier { } Future moveEntry(int oldContentId, AvesEntry entry) async { - final oldRow = _rows.firstWhere((row) => row.contentId == oldContentId, orElse: () => null); + final oldRow = _rows.firstWhereOrNull((row) => row.contentId == oldContentId); if (oldRow == null) return; final newRow = _entryToRow(entry); @@ -66,13 +67,13 @@ class FavouriteRow { final String path; const FavouriteRow({ - this.contentId, - this.path, + required this.contentId, + required this.path, }); factory FavouriteRow.fromMap(Map map) { return FavouriteRow( - contentId: map['contentId'], + contentId: map['contentId'] ?? 0, path: map['path'] ?? '', ); } diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index edab0e9bc..758fda92f 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -14,7 +14,7 @@ class AlbumFilter extends CollectionFilter { static final Map _appColors = {}; final String album; - final String displayName; + final String? displayName; const AlbumFilter(this.album, this.displayName); @@ -41,10 +41,10 @@ class AlbumFilter extends CollectionFilter { String getTooltip(BuildContext context) => album; @override - Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { + Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { return IconUtils.getAlbumIcon( context: context, - album: album, + albumPath: album, size: size, embossed: embossed, ) ?? @@ -52,25 +52,24 @@ class AlbumFilter extends CollectionFilter { } @override - Future color(BuildContext context) { + Future color(BuildContext context) { // do not use async/await and rely on `SynchronousFuture` // to prevent rebuilding of the `FutureBuilder` listening on this future if (androidFileUtils.getAlbumType(album) == AlbumType.app) { - if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]); + if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!); - return PaletteGenerator.fromImageProvider( - AppIconImage( - packageName: androidFileUtils.getAlbumAppPackageName(album), - size: 24, - ), - ).then((palette) { - final color = palette.dominantColor?.color ?? super.color(context); - _appColors[album] = color; - return color; - }); - } else { - return super.color(context); + final packageName = androidFileUtils.getAlbumAppPackageName(album); + if (packageName != null) { + return PaletteGenerator.fromImageProvider( + AppIconImage(packageName: packageName, size: 24), + ).then((palette) async { + final color = palette.dominantColor?.color ?? (await super.color(context)); + _appColors[album] = color; + return color; + }); + } } + return super.color(context); } @override diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index fce3a889f..d0da56c81 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -24,7 +24,7 @@ abstract class CollectionFilter implements Comparable { TagFilter.type, ]; - static CollectionFilter fromJson(String jsonString) { + static CollectionFilter? fromJson(String jsonString) { final jsonMap = jsonDecode(jsonString); final type = jsonMap['type']; switch (type) { @@ -63,7 +63,7 @@ abstract class CollectionFilter implements Comparable { String getTooltip(BuildContext context) => getLabel(context); - Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}); + Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}); Future color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context))); @@ -84,7 +84,7 @@ abstract class CollectionFilter implements Comparable { class FilterGridItem { final T filter; - final AvesEntry entry; + final AvesEntry? entry; const FilterGridItem(this.filter, this.entry); diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index b92a0073b..4e78ac4d4 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -1,6 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -10,8 +11,8 @@ class LocationFilter extends CollectionFilter { final LocationLevel level; String _location; - String _countryCode; - EntryFilter _test; + String? _countryCode; + late EntryFilter _test; LocationFilter(this.level, this._location) { final split = _location.split(locationSeparator); @@ -29,7 +30,7 @@ class LocationFilter extends CollectionFilter { LocationFilter.fromMap(Map json) : this( - LocationLevel.values.firstWhere((v) => v.toString() == json['level'], orElse: () => null), + LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place, json['location'], ); @@ -42,7 +43,7 @@ class LocationFilter extends CollectionFilter { String get countryNameAndCode => '$_location$locationSeparator$_countryCode'; - String get countryCode => _countryCode; + String? get countryCode => _countryCode; @override EntryFilter get test => _test; @@ -90,8 +91,9 @@ class LocationFilter extends CollectionFilter { // U+1F1E6 🇦 REGIONAL INDICATOR SYMBOL LETTER A static const _countryCodeToFlagDiff = 0x1F1E6 - 0x0041; - static String countryCodeToFlag(String code) { - return code?.length == 2 ? String.fromCharCodes(code.codeUnits.map((letter) => letter += _countryCodeToFlagDiff)) : null; + static String? countryCodeToFlag(String? code) { + if (code == null || code.length != 2) return null; + return String.fromCharCodes(code.codeUnits.map((letter) => letter += _countryCodeToFlagDiff)); } } diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 8ed351750..0d447a62e 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -10,9 +10,9 @@ class MimeFilter extends CollectionFilter { static const type = 'mime'; final String mime; - EntryFilter _test; - String _label; - IconData _icon; + late EntryFilter _test; + late String _label; + IconData? /*late*/ _icon; static final image = MimeFilter(MimeTypes.anyImage); static final video = MimeFilter(MimeTypes.anyVideo); diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index cab4778e7..c9ffc60a2 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -12,7 +12,7 @@ class QueryFilter extends CollectionFilter { final String query; final bool colorful; - EntryFilter _test; + late EntryFilter _test; QueryFilter(this.query, {this.colorful = true}) { var upQuery = query.toUpperCase(); @@ -26,7 +26,7 @@ class QueryFilter extends CollectionFilter { // allow untrimmed queries wrapped with `"..."` final matches = exactRegex.allMatches(upQuery); if (matches.length == 1) { - upQuery = matches.first.group(1); + upQuery = matches.first.group(1)!; } _test = not ? (entry) => !entry.search(upQuery) : (entry) => entry.search(upQuery); diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index 3b39b82ac..c64fa04d2 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -8,7 +8,7 @@ class TagFilter extends CollectionFilter { static const type = 'tag'; final String tag; - EntryFilter _test; + late EntryFilter _test; TagFilter(this.tag) { if (tag.isEmpty) { @@ -42,7 +42,7 @@ class TagFilter extends CollectionFilter { String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag; @override - Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; + Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; @override String get category => type; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index 9087f1879..a808971c4 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -14,8 +14,8 @@ class TypeFilter extends CollectionFilter { static const _sphericalVideo = 'spherical_video'; // subset of videos final String itemType; - EntryFilter _test; - IconData _icon; + late EntryFilter _test; + IconData? /*late*/ _icon; static final animated = TypeFilter._private(_animated); static final geotiff = TypeFilter._private(_geotiff); diff --git a/lib/model/highlight.dart b/lib/model/highlight.dart index 589ada79e..5bd3682d4 100644 --- a/lib/model/highlight.dart +++ b/lib/model/highlight.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; class HighlightInfo extends ChangeNotifier { - Object _item; + Object? _item; void set(Object item) { if (_item == item) return; @@ -9,7 +9,7 @@ class HighlightInfo extends ChangeNotifier { notifyListeners(); } - Object clear() { + Object? clear() { if (_item == null) return null; final item = _item; _item = null; diff --git a/lib/model/metadata.dart b/lib/model/metadata.dart index 2d8157faf..2bb707e00 100644 --- a/lib/model/metadata.dart +++ b/lib/model/metadata.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; class DateMetadata { - final int contentId, dateMillis; + final int? contentId, dateMillis; DateMetadata({ this.contentId, @@ -28,13 +28,13 @@ class DateMetadata { } class CatalogMetadata { - final int contentId, dateMillis; + final int? contentId, dateMillis; final bool isAnimated, isGeotiff, is360, isMultiPage; bool isFlipped; - int rotationDegrees; - final String mimeType, xmpSubjects, xmpTitleDescription; - double latitude, longitude; - Address address; + int? rotationDegrees; + final String? mimeType, xmpSubjects, xmpTitleDescription; + double? latitude, longitude; + Address? address; static const double _precisionErrorTolerance = 1e-9; static const _isAnimatedMask = 1 << 0; @@ -55,23 +55,28 @@ class CatalogMetadata { this.rotationDegrees, this.xmpSubjects, this.xmpTitleDescription, - double latitude, - double longitude, + double? latitude, + double? longitude, }) { - // Geocoder throws an `IllegalArgumentException` when a coordinate has a funky values like `1.7056881853375E7` + // Geocoder throws an `IllegalArgumentException` when a coordinate has a funky value like `1.7056881853375E7` // We also exclude zero coordinates, taking into account precision errors (e.g. {5.952380952380953e-11,-2.7777777777777777e-10}), // but Flutter's `precisionErrorTolerance` (1e-10) is slightly too lenient for this case. if (latitude != null && longitude != null && (latitude.abs() > _precisionErrorTolerance || longitude.abs() > _precisionErrorTolerance)) { - this.latitude = latitude < -90.0 || latitude > 90.0 ? null : latitude; - this.longitude = longitude < -180.0 || longitude > 180.0 ? null : longitude; + // funny case: some files have latitude and longitude reverse + // (e.g. a Japanese location at lat~=133 and long~=34, which is a valid longitude but an invalid latitude) + // so we should check and assign both coordinates at once + if (latitude >= -90.0 && latitude <= 90.0 && longitude >= -180.0 && longitude <= 180.0) { + this.latitude = latitude; + this.longitude = longitude; + } } } CatalogMetadata copyWith({ - int contentId, - String mimeType, - bool isMultiPage, - int rotationDegrees, + int? contentId, + String? mimeType, + bool? isMultiPage, + int? rotationDegrees, }) { return CatalogMetadata( contentId: contentId ?? this.contentId, @@ -127,16 +132,16 @@ class CatalogMetadata { } class OverlayMetadata { - final String aperture, exposureTime, focalLength, iso; + final String? aperture, exposureTime, focalLength, iso; static final apertureFormat = NumberFormat('0.0', 'en_US'); static final focalLengthFormat = NumberFormat('0.#', 'en_US'); OverlayMetadata({ - double aperture, - String exposureTime, - double focalLength, - int iso, + double? aperture, + String? exposureTime, + double? focalLength, + int? iso, }) : aperture = aperture != null ? 'ƒ/${apertureFormat.format(aperture)}' : null, exposureTime = exposureTime, focalLength = focalLength != null ? '${focalLengthFormat.format(focalLength)} mm' : null, @@ -144,10 +149,10 @@ class OverlayMetadata { factory OverlayMetadata.fromMap(Map map) { return OverlayMetadata( - aperture: map['aperture'] as double, - exposureTime: map['exposureTime'] as String, - focalLength: map['focalLength'] as double, - iso: map['iso'] as int, + aperture: map['aperture'] as double?, + exposureTime: map['exposureTime'] as String?, + focalLength: map['focalLength'] as double?, + iso: map['iso'] as int?, ); } @@ -159,10 +164,10 @@ class OverlayMetadata { @immutable class AddressDetails { - final int contentId; - final String countryCode, countryName, adminArea, locality; + final int? contentId; + final String? countryCode, countryName, adminArea, locality; - String get place => locality != null && locality.isNotEmpty ? locality : adminArea; + String? get place => locality != null && locality!.isNotEmpty ? locality : adminArea; const AddressDetails({ this.contentId, @@ -173,7 +178,7 @@ class AddressDetails { }); AddressDetails copyWith({ - int contentId, + int? contentId, }) { return AddressDetails( contentId: contentId ?? this.contentId, @@ -186,11 +191,11 @@ class AddressDetails { factory AddressDetails.fromMap(Map map) { return AddressDetails( - contentId: map['contentId'], - countryCode: map['countryCode'] ?? '', - countryName: map['countryName'] ?? '', - adminArea: map['adminArea'] ?? '', - locality: map['locality'] ?? '', + contentId: map['contentId'] as int?, + countryCode: map['countryCode'] as String?, + countryName: map['countryName'] as String?, + adminArea: map['adminArea'] as String?, + locality: map['locality'] as String?, ); } diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index 1e1474325..2afdbcedf 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:aves/model/covers.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db_upgrade.dart'; import 'package:aves/services/services.dart'; @@ -16,7 +17,7 @@ abstract class MetadataDb { Future reset(); - Future removeIds(Set contentIds, {@required bool metadataOnly}); + Future removeIds(Set contentIds, {required bool metadataOnly}); // entries @@ -40,9 +41,9 @@ abstract class MetadataDb { Future> loadMetadataEntries(); - Future saveMetadata(Iterable metadataEntries); + Future saveMetadata(Set metadataEntries); - Future updateMetadataId(int oldId, CatalogMetadata metadata); + Future updateMetadataId(int oldId, CatalogMetadata? metadata); // address @@ -50,9 +51,9 @@ abstract class MetadataDb { Future> loadAddresses(); - Future saveAddresses(Iterable addresses); + Future saveAddresses(Set addresses); - Future updateAddressId(int oldId, AddressDetails address); + Future updateAddressId(int oldId, AddressDetails? address); // favourites @@ -76,11 +77,11 @@ abstract class MetadataDb { Future updateCoverEntryId(int oldId, CoverRow row); - Future removeCovers(Iterable rows); + Future removeCovers(Set filters); } class SqfliteMetadataDb implements MetadataDb { - Future _database; + late Future _database; Future get path async => pContext.join(await getDatabasesPath(), 'metadata.db'); @@ -150,8 +151,8 @@ class SqfliteMetadataDb implements MetadataDb { @override Future dbFileSize() async { - final file = File((await path)); - return await file.exists() ? file.length() : 0; + final file = File(await path); + return await file.exists() ? await file.length() : 0; } @override @@ -163,8 +164,8 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future removeIds(Set contentIds, {@required bool metadataOnly}) async { - if (contentIds == null || contentIds.isEmpty) return; + Future removeIds(Set contentIds, {required bool metadataOnly}) async { + if (contentIds.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; @@ -207,7 +208,7 @@ class SqfliteMetadataDb implements MetadataDb { @override Future saveEntries(Iterable entries) async { - if (entries == null || entries.isEmpty) return; + if (entries.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; final batch = db.batch(); @@ -226,7 +227,6 @@ class SqfliteMetadataDb implements MetadataDb { } void _batchInsertEntry(Batch batch, AvesEntry entry) { - if (entry == null) return; batch.insert( entryTable, entry.toMap(), @@ -273,13 +273,13 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future saveMetadata(Iterable metadataEntries) async { - if (metadataEntries == null || metadataEntries.isEmpty) return; + Future saveMetadata(Set metadataEntries) async { + if (metadataEntries.isEmpty) return; final stopwatch = Stopwatch()..start(); try { final db = await _database; final batch = db.batch(); - metadataEntries.where((metadata) => metadata != null).forEach((metadata) => _batchInsertMetadata(batch, metadata)); + metadataEntries.forEach((metadata) => _batchInsertMetadata(batch, metadata)); await batch.commit(noResult: true); debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); } catch (error, stack) { @@ -288,7 +288,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future updateMetadataId(int oldId, CatalogMetadata metadata) async { + Future updateMetadataId(int oldId, CatalogMetadata? metadata) async { final db = await _database; final batch = db.batch(); batch.delete(dateTakenTable, where: 'contentId = ?', whereArgs: [oldId]); @@ -297,7 +297,7 @@ class SqfliteMetadataDb implements MetadataDb { await batch.commit(noResult: true); } - void _batchInsertMetadata(Batch batch, CatalogMetadata metadata) { + void _batchInsertMetadata(Batch batch, CatalogMetadata? metadata) { if (metadata == null) return; if (metadata.dateMillis != 0) { batch.insert( @@ -333,18 +333,18 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future saveAddresses(Iterable addresses) async { - if (addresses == null || addresses.isEmpty) return; + Future saveAddresses(Set addresses) async { + if (addresses.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; final batch = db.batch(); - addresses.where((address) => address != null).forEach((address) => _batchInsertAddress(batch, address)); + addresses.forEach((address) => _batchInsertAddress(batch, address)); await batch.commit(noResult: true); debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries'); } @override - Future updateAddressId(int oldId, AddressDetails address) async { + Future updateAddressId(int oldId, AddressDetails? address) async { final db = await _database; final batch = db.batch(); batch.delete(addressTable, where: 'contentId = ?', whereArgs: [oldId]); @@ -352,7 +352,7 @@ class SqfliteMetadataDb implements MetadataDb { await batch.commit(noResult: true); } - void _batchInsertAddress(Batch batch, AddressDetails address) { + void _batchInsertAddress(Batch batch, AddressDetails? address) { if (address == null) return; batch.insert( addressTable, @@ -380,10 +380,10 @@ class SqfliteMetadataDb implements MetadataDb { @override Future addFavourites(Iterable rows) async { - if (rows == null || rows.isEmpty) return; + if (rows.isEmpty) return; final db = await _database; final batch = db.batch(); - rows.where((row) => row != null).forEach((row) => _batchInsertFavourite(batch, row)); + rows.forEach((row) => _batchInsertFavourite(batch, row)); await batch.commit(noResult: true); } @@ -397,7 +397,6 @@ class SqfliteMetadataDb implements MetadataDb { } void _batchInsertFavourite(Batch batch, FavouriteRow row) { - if (row == null) return; batch.insert( favouriteTable, row.toMap(), @@ -407,8 +406,8 @@ class SqfliteMetadataDb implements MetadataDb { @override Future removeFavourites(Iterable rows) async { - if (rows == null || rows.isEmpty) return; - final ids = rows.where((row) => row != null).map((row) => row.contentId); + if (rows.isEmpty) return; + final ids = rows.map((row) => row.contentId); if (ids.isEmpty) return; final db = await _database; @@ -431,16 +430,16 @@ class SqfliteMetadataDb implements MetadataDb { Future> loadCovers() async { final db = await _database; final maps = await db.query(coverTable); - final rows = maps.map((map) => CoverRow.fromMap(map)).toSet(); + final rows = maps.map(CoverRow.fromMap).where((v) => v != null).cast().toSet(); return rows; } @override Future addCovers(Iterable rows) async { - if (rows == null || rows.isEmpty) return; + if (rows.isEmpty) return; final db = await _database; final batch = db.batch(); - rows.where((row) => row != null).forEach((row) => _batchInsertCover(batch, row)); + rows.forEach((row) => _batchInsertCover(batch, row)); await batch.commit(noResult: true); } @@ -454,7 +453,6 @@ class SqfliteMetadataDb implements MetadataDb { } void _batchInsertCover(Batch batch, CoverRow row) { - if (row == null) return; batch.insert( coverTable, row.toMap(), @@ -463,9 +461,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future removeCovers(Iterable rows) async { - if (rows == null || rows.isEmpty) return; - final filters = rows.where((row) => row != null).map((row) => row.filter); + Future removeCovers(Set filters) async { if (filters.isEmpty) return; final db = await _database; diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index bc92ef4d7..c2478077d 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/services.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; class MultiPageInfo { @@ -11,8 +12,8 @@ class MultiPageInfo { int get pageCount => _pages.length; MultiPageInfo({ - @required this.mainEntry, - List pages, + required this.mainEntry, + required List pages, }) : _pages = pages { if (_pages.isNotEmpty) { _pages.sort(); @@ -31,15 +32,15 @@ class MultiPageInfo { ); } - SinglePageInfo get defaultPage => _pages.firstWhere((page) => page.isDefault, orElse: () => null); + SinglePageInfo? get defaultPage => _pages.firstWhereOrNull((page) => page.isDefault); - SinglePageInfo getById(int pageId) => _pages.firstWhere((page) => page.pageId == pageId, orElse: () => null); + SinglePageInfo? getById(int? pageId) => _pages.firstWhereOrNull((page) => page.pageId == pageId); - SinglePageInfo getByIndex(int pageIndex) => _pages.firstWhere((page) => page.index == pageIndex, orElse: () => null); + SinglePageInfo? getByIndex(int? pageIndex) => _pages.firstWhereOrNull((page) => page.index == pageIndex); - AvesEntry getPageEntryByIndex(int pageIndex) => _getPageEntry(getByIndex(pageIndex)); + AvesEntry getPageEntryByIndex(int? pageIndex) => _getPageEntry(getByIndex(pageIndex)); - AvesEntry _getPageEntry(SinglePageInfo pageInfo) { + AvesEntry _getPageEntry(SinglePageInfo? pageInfo) { if (pageInfo != null) { return _pageEntries.putIfAbsent(pageInfo, () => _createPageEntry(pageInfo)); } else { @@ -52,20 +53,20 @@ class MultiPageInfo { List get exportEntries => _pages.map((pageInfo) => _createPageEntry(pageInfo, eraseDefaultPageId: false)).toList(); Future extractMotionPhotoVideo() async { - final videoPage = _pages.firstWhere((page) => page.isVideo, orElse: () => null); + final videoPage = _pages.firstWhereOrNull((page) => page.isVideo); if (videoPage != null && videoPage.uri == null) { final fields = await embeddedDataService.extractMotionPhotoVideo(mainEntry); - if (fields != null) { + if (fields.containsKey('uri')) { final pageIndex = _pages.indexOf(videoPage); _pages.removeAt(pageIndex); _pages.insert( pageIndex, videoPage.copyWith( - uri: fields['uri'] as String, + uri: fields['uri'] as String?, // the initial fake page may contain inaccurate values for the following fields // so we override them with values from the extracted standalone video - rotationDegrees: fields['sourceRotationDegrees'] as int, - durationMillis: fields['durationMillis'] as int, + rotationDegrees: fields['sourceRotationDegrees'] as int?, + durationMillis: fields['durationMillis'] as int?, )); _pageEntries.remove(videoPage); } @@ -83,9 +84,9 @@ class MultiPageInfo { path: mainEntry.path, contentId: mainEntry.contentId, pageId: pageId, - sourceMimeType: pageInfo.mimeType ?? mainEntry.sourceMimeType, - width: pageInfo.width ?? mainEntry.width, - height: pageInfo.height ?? mainEntry.height, + sourceMimeType: pageInfo.mimeType, + width: pageInfo.width, + height: pageInfo.height, sourceRotationDegrees: pageInfo.rotationDegrees ?? mainEntry.sourceRotationDegrees, sizeBytes: mainEntry.sizeBytes, sourceTitle: mainEntry.sourceTitle, @@ -108,26 +109,28 @@ class MultiPageInfo { class SinglePageInfo implements Comparable { final int index, pageId; final bool isDefault; - final String uri, mimeType; - final int width, height, rotationDegrees, durationMillis; + final String? uri; + final String mimeType; + final int width, height; + final int? rotationDegrees, durationMillis; const SinglePageInfo({ - this.index, - this.pageId, - this.isDefault, + required this.index, + required this.pageId, + required this.isDefault, this.uri, - this.mimeType, - this.width, - this.height, + required this.mimeType, + required this.width, + required this.height, this.rotationDegrees, this.durationMillis, }); SinglePageInfo copyWith({ - bool isDefault, - String uri, - int rotationDegrees, - int durationMillis, + bool? isDefault, + String? uri, + int? rotationDegrees, + int? durationMillis, }) { return SinglePageInfo( index: index, @@ -147,12 +150,12 @@ class SinglePageInfo implements Comparable { return SinglePageInfo( index: index, pageId: index, - isDefault: map['isDefault'] as bool ?? false, + isDefault: map['isDefault'] as bool? ?? false, mimeType: map['mimeType'] as String, - width: map['width'] as int ?? 0, - height: map['height'] as int ?? 0, - rotationDegrees: map['rotationDegrees'] as int, - durationMillis: map['durationMillis'] as int, + width: map['width'] as int? ?? 0, + height: map['height'] as int? ?? 0, + rotationDegrees: map['rotationDegrees'] as int?, + durationMillis: map['durationMillis'] as int?, ); } diff --git a/lib/model/panorama.dart b/lib/model/panorama.dart index 99d1ff318..df67ae060 100644 --- a/lib/model/panorama.dart +++ b/lib/model/panorama.dart @@ -2,9 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class PanoramaInfo { - final Rect croppedAreaRect; - final Size fullPanoSize; - final String projectionType; + final Rect? croppedAreaRect; + final Size? fullPanoSize; + final String? projectionType; PanoramaInfo({ this.croppedAreaRect, @@ -13,13 +13,13 @@ class PanoramaInfo { }); factory PanoramaInfo.fromMap(Map map) { - var cLeft = map['croppedAreaLeft'] as int; - var cTop = map['croppedAreaTop'] as int; - final cWidth = map['croppedAreaWidth'] as int; - final cHeight = map['croppedAreaHeight'] as int; - var fWidth = map['fullPanoWidth'] as int; - var fHeight = map['fullPanoHeight'] as int; - final projectionType = map['projectionType'] as String; + var cLeft = map['croppedAreaLeft'] as int?; + var cTop = map['croppedAreaTop'] as int?; + final cWidth = map['croppedAreaWidth'] as int?; + final cHeight = map['croppedAreaHeight'] as int?; + var fWidth = map['fullPanoWidth'] as int?; + var fHeight = map['fullPanoHeight'] as int?; + final projectionType = map['projectionType'] as String?; // handle missing `fullPanoHeight` (e.g. Samsung camera app panorama mode) if (fHeight == null && cWidth != null && cHeight != null) { @@ -31,12 +31,12 @@ class PanoramaInfo { cLeft = 0; } - Rect croppedAreaRect; + Rect? croppedAreaRect; if (cLeft != null && cTop != null && cWidth != null && cHeight != null) { croppedAreaRect = Rect.fromLTWH(cLeft.toDouble(), cTop.toDouble(), cWidth.toDouble(), cHeight.toDouble()); } - Size fullPanoSize; + Size? fullPanoSize; if (fWidth != null && fHeight != null) { fullPanoSize = Size(fWidth.toDouble(), fHeight.toDouble()); } diff --git a/lib/model/settings/entry_background.dart b/lib/model/settings/entry_background.dart index 14f83f071..c9935b6d1 100644 --- a/lib/model/settings/entry_background.dart +++ b/lib/model/settings/entry_background.dart @@ -15,12 +15,11 @@ extension ExtraEntryBackground on EntryBackground { Color get color { switch (this) { - case EntryBackground.black: - return Colors.black; case EntryBackground.white: return Colors.white; + case EntryBackground.black: default: - return null; + return Colors.black; } } } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index e837d2e64..cb68e2ff6 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -1,6 +1,7 @@ import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/screen_on.dart'; +import 'package:collection/collection.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; @@ -14,7 +15,7 @@ import 'enums.dart'; final Settings settings = Settings._private(); class Settings extends ChangeNotifier { - static SharedPreferences _prefs; + static SharedPreferences? /*late final*/ _prefs; Settings._private(); @@ -93,7 +94,7 @@ class Settings extends ChangeNotifier { } Future reset() { - return _prefs.clear(); + return _prefs!.clear(); } // app @@ -111,7 +112,7 @@ class Settings extends ChangeNotifier { static const localeSeparator = '-'; - Locale get locale { + Locale? get locale { // exceptionally allow getting locale before settings are initialized final tag = _prefs?.getString(localeKey); if (tag != null) { @@ -125,11 +126,11 @@ class Settings extends ChangeNotifier { return null; } - set locale(Locale newValue) { - String tag; + set locale(Locale? newValue) { + String? tag; if (newValue != null) { tag = [ - newValue.languageCode ?? '', + newValue.languageCode, newValue.scriptCode ?? '', newValue.countryCode ?? '', ].join(localeSeparator); @@ -152,11 +153,11 @@ class Settings extends ChangeNotifier { set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); - String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? ''; + String get catalogTimeZone => _prefs!.getString(catalogTimeZoneKey) ?? ''; set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); - double getTileExtent(String routeName) => _prefs.getDouble(tileExtentPrefixKey + routeName) ?? 0; + double getTileExtent(String routeName) => _prefs!.getDouble(tileExtentPrefixKey + routeName) ?? 0; // do not notify, as tile extents are only used internally by `TileExtentController` // and should not trigger rebuilding by change notification @@ -202,11 +203,11 @@ class Settings extends ChangeNotifier { set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); - Set get pinnedFilters => (_prefs.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet(); + Set get pinnedFilters => (_prefs!.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast().toSet(); set pinnedFilters(Set newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); - Set get hiddenFilters => (_prefs.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet(); + Set get hiddenFilters => (_prefs!.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast().toSet(); set hiddenFilters(Set newValue) => setAndNotify(hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList()); @@ -248,7 +249,7 @@ class Settings extends ChangeNotifier { set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); - double get infoMapZoom => _prefs.getDouble(infoMapZoomKey) ?? 12; + double get infoMapZoom => _prefs!.getDouble(infoMapZoomKey) ?? 12; set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); @@ -272,23 +273,23 @@ class Settings extends ChangeNotifier { set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue); - List get searchHistory => (_prefs.getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).toList(); + List get searchHistory => (_prefs!.getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast().toList(); set searchHistory(List newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); // version - DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs.getInt(lastVersionCheckDateKey) ?? 0); + DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs!.getInt(lastVersionCheckDateKey) ?? 0); set lastVersionCheckDate(DateTime newValue) => setAndNotify(lastVersionCheckDateKey, newValue.millisecondsSinceEpoch); // convenience methods // ignore: avoid_positional_boolean_parameters - bool getBoolOrDefault(String key, bool defaultValue) => _prefs.getKeys().contains(key) ? _prefs.getBool(key) : defaultValue; + bool getBoolOrDefault(String key, bool defaultValue) => _prefs!.getBool(key) ?? defaultValue; T getEnumOrDefault(String key, T defaultValue, Iterable values) { - final valueString = _prefs.getString(key); + final valueString = _prefs!.getString(key); for (final v in values) { if (v.toString() == valueString) { return v; @@ -298,28 +299,28 @@ class Settings extends ChangeNotifier { } List getEnumListOrDefault(String key, List defaultValue, Iterable values) { - return _prefs.getStringList(key)?.map((s) => values.firstWhere((v) => v.toString() == s, orElse: () => null))?.where((v) => v != null)?.toList() ?? defaultValue; + return _prefs!.getStringList(key)?.map((s) => values.firstWhereOrNull((v) => v.toString() == s)).where((v) => v != null).cast().toList() ?? defaultValue; } void setAndNotify(String key, dynamic newValue, {bool notify = true}) { - var oldValue = _prefs.get(key); + var oldValue = _prefs!.get(key); if (newValue == null) { - _prefs.remove(key); + _prefs!.remove(key); } else if (newValue is String) { - oldValue = _prefs.getString(key); - _prefs.setString(key, newValue); + oldValue = _prefs!.getString(key); + _prefs!.setString(key, newValue); } else if (newValue is List) { - oldValue = _prefs.getStringList(key); - _prefs.setStringList(key, newValue); + oldValue = _prefs!.getStringList(key); + _prefs!.setStringList(key, newValue); } else if (newValue is int) { - oldValue = _prefs.getInt(key); - _prefs.setInt(key, newValue); + oldValue = _prefs!.getInt(key); + _prefs!.setInt(key, newValue); } else if (newValue is double) { - oldValue = _prefs.getDouble(key); - _prefs.setDouble(key, newValue); + oldValue = _prefs!.getDouble(key); + _prefs!.setDouble(key, newValue); } else if (newValue is bool) { - oldValue = _prefs.getBool(key); - _prefs.setBool(key, newValue); + oldValue = _prefs!.getBool(key); + _prefs!.setBool(key, newValue); } if (oldValue != newValue && notify) { notifyListeners(); diff --git a/lib/model/settings/video_loop_mode.dart b/lib/model/settings/video_loop_mode.dart index ec114540c..352b1d911 100644 --- a/lib/model/settings/video_loop_mode.dart +++ b/lib/model/settings/video_loop_mode.dart @@ -25,11 +25,10 @@ extension ExtraVideoLoopMode on VideoLoopMode { case VideoLoopMode.never: return false; case VideoLoopMode.shortOnly: - if (entry.durationMillis == null) return false; - return entry.durationMillis < shortVideoThreshold.inMilliseconds; + final durationMillis = entry.durationMillis; + return durationMillis != null ? durationMillis < shortVideoThreshold.inMilliseconds : false; case VideoLoopMode.always: return true; } - return false; } } diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 72e2930e5..359bef7be 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -9,7 +9,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; mixin AlbumMixin on SourceBase { - final Set _directories = {}; + final Set _directories = {}; List get rawAlbums => List.unmodifiable(_directories); @@ -25,7 +25,7 @@ mixin AlbumMixin on SourceBase { void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent()); - String getAlbumDisplayName(BuildContext context, String dirPath) { + String getAlbumDisplayName(BuildContext? context, String dirPath) { assert(!dirPath.endsWith(pContext.separator)); if (context != null) { @@ -41,15 +41,15 @@ mixin AlbumMixin on SourceBase { final relativeDir = dir.relativeDir; if (relativeDir.isEmpty) { - final volume = androidFileUtils.getStorageVolume(dirPath); + final volume = androidFileUtils.getStorageVolume(dirPath)!; return volume.getDescription(context); } - String unique(String dirPath, Set others) { + String unique(String dirPath, Set others) { final parts = pContext.split(dirPath); for (var i = parts.length - 1; i > 0; i--) { final testName = pContext.joinAll(['', ...parts.skip(i)]); - if (others.every((item) => !item.endsWith(testName))) return testName; + if (others.every((item) => !item!.endsWith(testName))) return testName; } return dirPath; } @@ -61,10 +61,10 @@ mixin AlbumMixin on SourceBase { } final volumePath = dir.volumePath; - String trimVolumePath(String path) => path.substring(dir.volumePath.length); - final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path.startsWith(volumePath)).map(trimVolumePath).toSet(); + String trimVolumePath(String? path) => path!.substring(dir.volumePath.length); + final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path!.startsWith(volumePath)).map(trimVolumePath).toSet(); final uniqueNameInVolume = unique(trimVolumePath(dirPath), otherAlbumsOnVolume); - final volume = androidFileUtils.getStorageVolume(dirPath); + final volume = androidFileUtils.getStorageVolume(dirPath)!; if (volume.isPrimary) { return uniqueNameInVolume; } else { @@ -72,7 +72,7 @@ mixin AlbumMixin on SourceBase { } } - Map getAlbumEntries() { + Map getAlbumEntries() { final entries = sortedEntriesByDate; final regularAlbums = [], appAlbums = [], specialAlbums = []; for (final album in rawAlbums) { @@ -90,7 +90,7 @@ mixin AlbumMixin on SourceBase { } return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) => MapEntry( album, - entries.firstWhere((entry) => entry.directory == album, orElse: () => null), + entries.firstWhereOrNull((entry) => entry.directory == album), ))); } @@ -100,14 +100,14 @@ mixin AlbumMixin on SourceBase { cleanEmptyAlbums(); } - void addDirectories(Set albums) { + void addDirectories(Set albums) { if (!_directories.containsAll(albums)) { _directories.addAll(albums); _notifyAlbumChange(); } } - void cleanEmptyAlbums([Set albums]) { + void cleanEmptyAlbums([Set? albums]) { final emptyAlbums = (albums ?? _directories).where(_isEmptyAlbum).toSet(); if (emptyAlbums.isNotEmpty) { _directories.removeAll(emptyAlbums); @@ -120,20 +120,20 @@ mixin AlbumMixin on SourceBase { } } - bool _isEmptyAlbum(String album) => !visibleEntries.any((entry) => entry.directory == album); + bool _isEmptyAlbum(String? album) => !visibleEntries.any((entry) => entry.directory == album); // filter summary // by directory final Map _filterEntryCountMap = {}; - final Map _filterRecentEntryMap = {}; + final Map _filterRecentEntryMap = {}; - void invalidateAlbumFilterSummary({Set entries, Set directories}) { + void invalidateAlbumFilterSummary({Set? entries, Set? directories}) { if (entries == null && directories == null) { _filterEntryCountMap.clear(); _filterRecentEntryMap.clear(); } else { - directories ??= entries.map((entry) => entry.directory).toSet(); + directories ??= entries!.map((entry) => entry.directory).toSet(); directories.forEach(_filterEntryCountMap.remove); directories.forEach(_filterRecentEntryMap.remove); } @@ -144,15 +144,15 @@ mixin AlbumMixin on SourceBase { return _filterEntryCountMap.putIfAbsent(filter.album, () => visibleEntries.where(filter.test).length); } - AvesEntry albumRecentEntry(AlbumFilter filter) { - return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhere(filter.test, orElse: () => null)); + AvesEntry? albumRecentEntry(AlbumFilter filter) { + return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); } } class AlbumsChangedEvent {} class AlbumSummaryInvalidatedEvent { - final Set directories; + final Set? directories; const AlbumSummaryInvalidatedEvent(this.directories); } diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 9531ed566..55bcaf60f 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -18,26 +18,26 @@ import 'enums.dart'; class CollectionLens with ChangeNotifier, CollectionActivityMixin { final CollectionSource source; - final Set filters; + final Set filters; EntryGroupFactor groupFactor; EntrySortFactor sortFactor; final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortGroupChangeNotifier = AChangeNotifier(); - int id; + final List _subscriptions = []; + int? id; bool listenToSource; - List _filteredSortedEntries; - List _subscriptions = []; + List _filteredSortedEntries = []; - Map> sections = Map.unmodifiable({}); + Map > sections = Map.unmodifiable({}); CollectionLens({ - @required this.source, - Iterable filters, - EntryGroupFactor groupFactor, - EntrySortFactor sortFactor, + required this.source, + Iterable? filters, + EntryGroupFactor? groupFactor, + EntrySortFactor? sortFactor, this.id, this.listenToSource = true, - }) : filters = {if (filters != null) ...filters.where((f) => f != null)}, + }) : filters = (filters ?? {}).where((f) => f != null).cast().toSet(), groupFactor = groupFactor ?? settings.collectionGroupFactor, sortFactor = sortFactor ?? settings.collectionSortFactor { id ??= hashCode; @@ -61,7 +61,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); - _subscriptions = null; super.dispose(); } @@ -70,11 +69,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { int get entryCount => _filteredSortedEntries.length; // sorted as displayed to the user, i.e. sorted then grouped, not an absolute order on all entries - List _sortedEntries; + List? _sortedEntries; - List get sortedEntries { - _sortedEntries ??= List.of(sections.entries.expand((e) => e.value)); - return _sortedEntries; + List get sortedEntries { + _sortedEntries ??= List.of(sections.entries.expand((kv) => kv.value)); + return _sortedEntries!; } bool get showHeaders { @@ -90,7 +89,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { } void addFilter(CollectionFilter filter) { - if (filter == null || filters.contains(filter)) return; + if (filters.contains(filter)) return; if (filter.isUnique) { filters.removeWhere((old) => old.category == filter.category); } @@ -99,7 +98,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { } void removeFilter(CollectionFilter filter) { - if (filter == null || !filters.contains(filter)) return; + if (!filters.contains(filter)) return; filters.remove(filter); onFilterChanged(); } @@ -156,19 +155,19 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { break; case EntryGroupFactor.none: sections = Map.fromEntries([ - MapEntry(null, _filteredSortedEntries), + MapEntry(SectionKey(), _filteredSortedEntries), ]); break; } break; case EntrySortFactor.size: sections = Map.fromEntries([ - MapEntry(null, _filteredSortedEntries), + MapEntry(SectionKey(), _filteredSortedEntries), ]); break; case EntrySortFactor.name: final byAlbum = groupBy(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); - sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory, b.directory)); + sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!)); break; } sections = Map.unmodifiable(sections); @@ -184,7 +183,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { _applyGroup(); } - void onEntryAdded(Set entries) { + void onEntryAdded(Set? entries) { _refresh(); } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 9339e3c78..4d3fa3498 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -15,6 +15,7 @@ import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/services.dart'; +import 'package:collection/collection.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; @@ -31,7 +32,7 @@ mixin SourceBase { Stream get progressStream => _progressStreamController.stream; - void setProgress({@required int done, @required int total}) => _progressStreamController.add(ProgressEvent(done: done, total: total)); + void setProgress({required int done, required int total}) => _progressStreamController.add(ProgressEvent(done: done, total: total)); } abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { @@ -45,24 +46,24 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM // TODO TLAD use `Set.unmodifiable()` when possible Set get allEntries => Set.of(_rawEntries); - Set _visibleEntries; + Set? _visibleEntries; @override Set get visibleEntries { // TODO TLAD use `Set.unmodifiable()` when possible _visibleEntries ??= Set.of(_applyHiddenFilters(_rawEntries)); - return _visibleEntries; + return _visibleEntries!; } - List _sortedEntriesByDate; + List? _sortedEntriesByDate; @override List get sortedEntriesByDate { _sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntry.compareByDate)); - return _sortedEntriesByDate; + return _sortedEntriesByDate!; } - List _savedDates; + late List _savedDates; Future loadDates() async { final stopwatch = Stopwatch()..start(); @@ -75,7 +76,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return hiddenFilters.isEmpty ? entries : entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry))); } - void _invalidate([Set entries]) { + void _invalidate([Set? entries]) { _visibleEntries = null; _sortedEntriesByDate = null; invalidateAlbumFilterSummary(entries: entries); @@ -91,7 +92,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } entries.forEach((entry) { final contentId = entry.contentId; - entry.catalogDateMillis = _savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis; + entry.catalogDateMillis = _savedDates.firstWhereOrNull((metadata) => metadata.contentId == contentId)?.dateMillis; }); _rawEntries.addAll(entries); _invalidate(entries); @@ -124,16 +125,16 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } Future _moveEntry(AvesEntry entry, Map newFields) async { - final oldContentId = entry.contentId; - final newContentId = newFields['contentId'] as int; + final oldContentId = entry.contentId!; + final newContentId = newFields['contentId'] as int?; entry.contentId = newContentId; // `dateModifiedSecs` changes when moving entries to another directory, // but it does not change when renaming the containing directory - if (newFields.containsKey('dateModifiedSecs')) entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int; - if (newFields.containsKey('path')) entry.path = newFields['path'] as String; + if (newFields.containsKey('dateModifiedSecs')) entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int?; + if (newFields.containsKey('path')) entry.path = newFields['path'] as String?; if (newFields.containsKey('uri')) entry.uri = newFields['uri'] as String; - if (newFields.containsKey('title') != null) entry.sourceTitle = newFields['title'] as String; + if (newFields.containsKey('title')) entry.sourceTitle = newFields['title'] as String?; entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId); entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId); @@ -159,7 +160,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM final oldFilter = AlbumFilter(sourceAlbum, null); final pinned = settings.pinnedFilters.contains(oldFilter); final oldCoverContentId = covers.coverContentId(oldFilter); - final coverEntry = oldCoverContentId != null ? todoEntries.firstWhere((entry) => entry.contentId == oldCoverContentId, orElse: () => null) : null; + final coverEntry = oldCoverContentId != null ? todoEntries.firstWhereOrNull((entry) => entry.contentId == oldCoverContentId) : null; await updateAfterMove( todoEntries: todoEntries, copy: false, @@ -177,37 +178,39 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } Future updateAfterMove({ - @required Set todoEntries, - @required bool copy, - @required String destinationAlbum, - @required Set movedOps, + required Set todoEntries, + required bool copy, + required String destinationAlbum, + required Set movedOps, }) async { if (movedOps.isEmpty) return; - final fromAlbums = {}; + final fromAlbums = {}; final movedEntries = {}; if (copy) { movedOps.forEach((movedOp) { final sourceUri = movedOp.uri; final newFields = movedOp.newFields; - final sourceEntry = todoEntries.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); - fromAlbums.add(sourceEntry.directory); - movedEntries.add(sourceEntry?.copyWith( - uri: newFields['uri'] as String, - path: newFields['path'] as String, - contentId: newFields['contentId'] as int, - dateModifiedSecs: newFields['dateModifiedSecs'] as int, - )); + final sourceEntry = todoEntries.firstWhereOrNull((entry) => entry.uri == sourceUri); + if (sourceEntry != null) { + fromAlbums.add(sourceEntry.directory); + movedEntries.add(sourceEntry.copyWith( + uri: newFields['uri'] as String?, + path: newFields['path'] as String?, + contentId: newFields['contentId'] as int?, + dateModifiedSecs: newFields['dateModifiedSecs'] as int?, + )); + } }); await metadataDb.saveEntries(movedEntries); - await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata)); - await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails)); + await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata).where((v) => v != null).cast().toSet()); + await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).where((v) => v != null).cast().toSet()); } else { await Future.forEach(movedOps, (movedOp) async { final newFields = movedOp.newFields; if (newFields.isNotEmpty) { final sourceUri = movedOp.uri; - final entry = todoEntries.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); + final entry = todoEntries.firstWhereOrNull((entry) => entry.uri == sourceUri); if (entry != null) { fromAlbums.add(entry.directory); movedEntries.add(entry); @@ -255,17 +258,17 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return 0; } - AvesEntry recentEntry(CollectionFilter filter) { + AvesEntry? recentEntry(CollectionFilter filter) { if (filter is AlbumFilter) return albumRecentEntry(filter); if (filter is LocationFilter) return countryRecentEntry(filter); if (filter is TagFilter) return tagRecentEntry(filter); return null; } - AvesEntry coverEntry(CollectionFilter filter) { + AvesEntry? coverEntry(CollectionFilter filter) { final contentId = covers.coverContentId(filter); if (contentId != null) { - final entry = visibleEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); + final entry = visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId); if (entry != null) return entry; } return recentEntry(filter); @@ -297,7 +300,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } class EntryAddedEvent { - final Set entries; + final Set? entries; const EntryAddedEvent([this.entries]); } @@ -324,5 +327,5 @@ class FilterVisibilityChangedEvent { class ProgressEvent { final int done, total; - const ProgressEvent({@required this.done, @required this.total}); + const ProgressEvent({required this.done, required this.total}); } diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index cfb178d04..509638acc 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -22,7 +22,7 @@ mixin LocationMixin on SourceBase { final saved = await metadataDb.loadAddresses(); visibleEntries.forEach((entry) { final contentId = entry.contentId; - entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null); + entry.addressDetails = saved.firstWhereOrNull((address) => address.contentId == contentId); }); debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries'); onAddressMetadataChanged(); @@ -44,19 +44,19 @@ mixin LocationMixin on SourceBase { setProgress(done: progressDone, total: progressTotal); // final stopwatch = Stopwatch()..start(); - final countryCodeMap = await countryTopology.countryCodeMap(todo.map((entry) => entry.latLng).toSet()); - final newAddresses = []; + final countryCodeMap = await countryTopology.countryCodeMap(todo.map((entry) => entry.latLng!).toSet()); + final newAddresses = []; todo.forEach((entry) { final position = entry.latLng; - final countryCode = countryCodeMap.entries.firstWhere((kv) => kv.value.contains(position), orElse: () => null)?.key; + final countryCode = countryCodeMap.entries.firstWhereOrNull((kv) => kv.value.contains(position))?.key; entry.setCountry(countryCode); if (entry.hasAddress) { - newAddresses.add(entry.addressDetails); + newAddresses.add(entry.addressDetails!); } setProgress(done: ++progressDone, total: progressTotal); }); if (newAddresses.isNotEmpty) { - await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + await metadataDb.saveAddresses(Set.of(newAddresses)); onAddressMetadataChanged(); } // debugPrint('$runtimeType _locateCountries complete in ${stopwatch.elapsed.inMilliseconds}ms'); @@ -82,21 +82,23 @@ mixin LocationMixin on SourceBase { // cf https://en.wikipedia.org/wiki/Decimal_degrees#Precision final latLngFactor = pow(10, 2); Tuple2 approximateLatLng(AvesEntry entry) { - final lat = entry.catalogMetadata?.latitude; - final lng = entry.catalogMetadata?.longitude; - if (lat == null || lng == null) return null; + // entry has coordinates + final lat = entry.catalogMetadata!.latitude!; + final lng = entry.catalogMetadata!.longitude!; return Tuple2((lat * latLngFactor).round(), (lng * latLngFactor).round()); } - final knownLocations = , AddressDetails>{}; - byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails)); + final knownLocations = , AddressDetails?>{}; + byLocated[true]?.forEach((entry) { + knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails); + }); stateNotifier.value = SourceState.locating; var progressDone = 0; final progressTotal = todo.length; setProgress(done: progressDone, total: progressTotal); - final newAddresses = []; + final newAddresses = []; await Future.forEach(todo, (entry) async { final latLng = approximateLatLng(entry); if (knownLocations.containsKey(latLng)) { @@ -108,9 +110,9 @@ mixin LocationMixin on SourceBase { knownLocations[latLng] = entry.addressDetails; } if (entry.hasFineAddress) { - newAddresses.add(entry.addressDetails); + newAddresses.add(entry.addressDetails!); if (newAddresses.length >= _commitCountThreshold) { - await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + await metadataDb.saveAddresses(Set.of(newAddresses)); onAddressMetadataChanged(); newAddresses.clear(); } @@ -118,7 +120,7 @@ mixin LocationMixin on SourceBase { setProgress(done: ++progressDone, total: progressTotal); }); if (newAddresses.isNotEmpty) { - await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + await metadataDb.saveAddresses(Set.of(newAddresses)); onAddressMetadataChanged(); } // debugPrint('$runtimeType _locatePlaces complete in ${stopwatch.elapsed.inSeconds}s'); @@ -130,8 +132,8 @@ mixin LocationMixin on SourceBase { } void updateLocations() { - final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).toList(); - final updatedPlaces = locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase); + final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).cast().toList(); + final updatedPlaces = locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase as int Function(String?, String?)?); if (!listEquals(updatedPlaces, sortedPlaces)) { sortedPlaces = List.unmodifiable(updatedPlaces); eventBus.fire(PlacesChangedEvent()); @@ -140,7 +142,7 @@ mixin LocationMixin on SourceBase { // the same country code could be found with different country names // e.g. if the locale changed between geocoding calls // so we merge countries by code, keeping only one name for each code - final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key.isNotEmpty)); + final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key!.isNotEmpty)); final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase); if (!listEquals(updatedCountries, sortedCountries)) { sortedCountries = List.unmodifiable(updatedCountries); @@ -153,27 +155,30 @@ mixin LocationMixin on SourceBase { // by country code final Map _filterEntryCountMap = {}; - final Map _filterRecentEntryMap = {}; + final Map _filterRecentEntryMap = {}; - void invalidateCountryFilterSummary([Set entries]) { - Set countryCodes; + void invalidateCountryFilterSummary([Set? entries]) { + Set? countryCodes; if (entries == null) { _filterEntryCountMap.clear(); _filterRecentEntryMap.clear(); } else { - countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails.countryCode).toSet(); - countryCodes.remove(null); + countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails!.countryCode).where((v) => v != null).cast().toSet(); countryCodes.forEach(_filterEntryCountMap.remove); } eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes)); } int countryEntryCount(LocationFilter filter) { - return _filterEntryCountMap.putIfAbsent(filter.countryCode, () => visibleEntries.where(filter.test).length); + final countryCode = filter.countryCode; + if (countryCode == null) return 0; + return _filterEntryCountMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).length); } - AvesEntry countryRecentEntry(LocationFilter filter) { - return _filterRecentEntryMap.putIfAbsent(filter.countryCode, () => sortedEntriesByDate.firstWhere(filter.test, orElse: () => null)); + AvesEntry? countryRecentEntry(LocationFilter filter) { + final countryCode = filter.countryCode; + if (countryCode == null) return null; + return _filterRecentEntryMap.putIfAbsent(countryCode, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); } } @@ -184,7 +189,7 @@ class PlacesChangedEvent {} class CountriesChangedEvent {} class CountrySummaryInvalidatedEvent { - final Set countryCodes; + final Set? countryCodes; const CountrySummaryInvalidatedEvent(this.countryCodes); } diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index b4e65fd2d..0a4919d78 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -10,6 +10,7 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/math_utils.dart'; +import 'package:collection/collection.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; @@ -27,13 +28,15 @@ class MediaStoreSource extends CollectionSource { await favourites.init(); await covers.init(); final currentTimeZone = await timeService.getDefaultTimeZone(); - final catalogTimeZone = settings.catalogTimeZone; - if (currentTimeZone != catalogTimeZone) { - // clear catalog metadata to get correct date/times when moving to a different time zone - debugPrint('$runtimeType clear catalog metadata to get correct date/times'); - await metadataDb.clearDates(); - await metadataDb.clearMetadataEntries(); - settings.catalogTimeZone = currentTimeZone; + if (currentTimeZone != null) { + final catalogTimeZone = settings.catalogTimeZone; + if (currentTimeZone != catalogTimeZone) { + // clear catalog metadata to get correct date/times when moving to a different time zone + debugPrint('$runtimeType clear catalog metadata to get correct date/times'); + await metadataDb.clearDates(); + await metadataDb.clearMetadataEntries(); + settings.catalogTimeZone = currentTimeZone; + } } await loadDates(); // 100ms for 5400 entries _initialized = true; @@ -49,7 +52,7 @@ class MediaStoreSource extends CollectionSource { clearEntries(); final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries - final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs))); + final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId!, entry.dateModifiedSecs!))); final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet(); oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId)); @@ -63,7 +66,7 @@ class MediaStoreSource extends CollectionSource { await metadataDb.removeIds(obsoleteContentIds, metadataOnly: false); // verify paths because some apps move files without updating their `last modified date` - final knownPathById = Map.fromEntries(allEntries.map((entry) => MapEntry(entry.contentId, entry.path))); + final knownPathById = Map.fromEntries(allEntries.map((entry) => MapEntry(entry.contentId!, entry.path))); final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathById)).toSet(); movedContentIds.forEach((contentId) { // make obsolete by resetting its modified date @@ -130,20 +133,22 @@ class MediaStoreSource extends CollectionSource { Future> refreshUris(Set changedUris) async { if (!_initialized || !isMonitoring) return changedUris; - final uriByContentId = Map.fromEntries(changedUris.map((uri) { - if (uri == null) return null; - final pathSegments = Uri.parse(uri).pathSegments; - // e.g. URI `content://media/` has no path segment - if (pathSegments.isEmpty) return null; - final idString = pathSegments.last; - final contentId = int.tryParse(idString); - if (contentId == null) return null; - return MapEntry(contentId, uri); - }).where((kv) => kv != null)); + final uriByContentId = Map.fromEntries(changedUris + .map((uri) { + final pathSegments = Uri.parse(uri).pathSegments; + // e.g. URI `content://media/` has no path segment + if (pathSegments.isEmpty) return null; + final idString = pathSegments.last; + final contentId = int.tryParse(idString); + if (contentId == null) return null; + return MapEntry(contentId, uri); + }) + .where((kv) => kv != null) + .cast>()); // clean up obsolete entries final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(uriByContentId.keys.toList())).toSet(); - final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).toSet(); + final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).where((v) => v != null).cast().toSet(); await removeEntries(obsoleteUris); obsoleteContentIds.forEach(uriByContentId.remove); @@ -156,14 +161,16 @@ class MediaStoreSource extends CollectionSource { final uri = kv.value; final sourceEntry = await imageFileService.getEntry(uri, null); if (sourceEntry != null) { - final existingEntry = allEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); + final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId); // compare paths because some apps move files without updating their `last modified date` - if (existingEntry == null || sourceEntry.dateModifiedSecs > existingEntry.dateModifiedSecs || sourceEntry.path != existingEntry.path) { - final volume = androidFileUtils.getStorageVolume(sourceEntry.path); + if (existingEntry == null || (sourceEntry.dateModifiedSecs ?? 0) > (existingEntry.dateModifiedSecs ?? 0) || sourceEntry.path != existingEntry.path) { + final newPath = sourceEntry.path; + final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null; if (volume != null) { newEntries.add(sourceEntry); - if (existingEntry != null) { - existingDirectories.add(existingEntry.directory); + final existingDirectory = existingEntry?.directory; + if (existingDirectory != null) { + existingDirectories.add(existingDirectory); } } else { debugPrint('$runtimeType refreshUris entry=$sourceEntry is not located on a known storage volume. Will retry soon...'); @@ -189,7 +196,7 @@ class MediaStoreSource extends CollectionSource { @override Future refreshMetadata(Set entries) { final contentIds = entries.map((entry) => entry.contentId).toSet(); - metadataDb.removeIds(contentIds, metadataOnly: true); + metadataDb.removeIds(contentIds as Set, metadataOnly: true); return refresh(); } } diff --git a/lib/model/source/section_keys.dart b/lib/model/source/section_keys.dart index 66988f27d..be43543ee 100644 --- a/lib/model/source/section_keys.dart +++ b/lib/model/source/section_keys.dart @@ -5,7 +5,7 @@ class SectionKey { } class EntryAlbumSectionKey extends SectionKey { - final String directory; + final String? directory; const EntryAlbumSectionKey(this.directory); @@ -23,7 +23,7 @@ class EntryAlbumSectionKey extends SectionKey { } class EntryDateSectionKey extends SectionKey { - final DateTime date; + final DateTime? date; const EntryDateSectionKey(this.date); diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index dec9b0df3..bea5b66eb 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -17,7 +17,7 @@ mixin TagMixin on SourceBase { final saved = await metadataDb.loadMetadataEntries(); visibleEntries.forEach((entry) { final contentId = entry.contentId; - entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null); + entry.catalogMetadata = saved.firstWhereOrNull((metadata) => metadata.contentId == contentId); }); debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries'); onCatalogMetadataChanged(); @@ -37,16 +37,16 @@ mixin TagMixin on SourceBase { await Future.forEach(todo, (entry) async { await entry.catalog(background: true); if (entry.isCatalogued) { - newMetadata.add(entry.catalogMetadata); + newMetadata.add(entry.catalogMetadata!); if (newMetadata.length >= _commitCountThreshold) { - await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); + await metadataDb.saveMetadata(Set.of(newMetadata)); onCatalogMetadataChanged(); newMetadata.clear(); } } setProgress(done: ++progressDone, total: progressTotal); }); - await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); + await metadataDb.saveMetadata(Set.of(newMetadata)); onCatalogMetadataChanged(); // debugPrint('$runtimeType catalogEntries complete in ${stopwatch.elapsed.inSeconds}s'); } @@ -69,10 +69,10 @@ mixin TagMixin on SourceBase { // by tag final Map _filterEntryCountMap = {}; - final Map _filterRecentEntryMap = {}; + final Map _filterRecentEntryMap = {}; - void invalidateTagFilterSummary([Set entries]) { - Set tags; + void invalidateTagFilterSummary([Set? entries]) { + Set? tags; if (entries == null) { _filterEntryCountMap.clear(); _filterRecentEntryMap.clear(); @@ -87,8 +87,8 @@ mixin TagMixin on SourceBase { return _filterEntryCountMap.putIfAbsent(filter.tag, () => visibleEntries.where(filter.test).length); } - AvesEntry tagRecentEntry(TagFilter filter) { - return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhere(filter.test, orElse: () => null)); + AvesEntry? tagRecentEntry(TagFilter filter) { + return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); } } @@ -97,7 +97,7 @@ class CatalogMetadataChangedEvent {} class TagsChangedEvent {} class TagSummaryInvalidatedEvent { - final Set tags; + final Set? tags; const TagSummaryInvalidatedEvent(this.tags); } diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index c1851231f..72c145de8 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -11,6 +11,8 @@ import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart'; +import 'package:collection/collection.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; @@ -54,16 +56,16 @@ class VideoMetadataFormatter { final value = kv.value; if (value != null) { try { - String key; - String keyLanguage; + String? key; + String? keyLanguage; // some keys have a language suffix, but they may be duplicates // we only keep the root key when they have the same value as the same key with no language final languageMatch = keyWithLanguagePattern.firstMatch(kv.key); if (languageMatch != null) { - final code = languageMatch.group(2); + final code = languageMatch.group(2)!; final native = _formatLanguage(code); if (native != code) { - final root = languageMatch.group(1); + final root = languageMatch.group(1)!; final rootValue = info[root]; // skip if it is a duplicate of the same entry with no language if (rootValue == value) continue; @@ -76,7 +78,7 @@ class VideoMetadataFormatter { } key = (key ?? (kv.key as String)).toLowerCase(); - void save(String key, String value) { + void save(String key, String? value) { if (value != null) { dir[keyLanguage != null ? '$key ($keyLanguage)' : key] = value; } @@ -129,21 +131,26 @@ class VideoMetadataFormatter { case Keys.codecProfileId: if (codec == 'h264') { final profile = int.tryParse(value); - if (profile != null && profile != 0) { - final level = int.tryParse(info[Keys.codecLevel]); + final levelString = info[Keys.codecLevel]; + if (profile != null && profile != 0 && levelString != null) { + final level = int.tryParse(levelString) ?? 0; save('Codec Profile', H264.formatProfile(profile, level)); } } break; case Keys.compatibleBrands: - save('Compatible Brands', RegExp(r'.{4}').allMatches(value).map((m) => _formatBrand(m.group(0))).join(', ')); + final formattedBrands = RegExp(r'.{4}').allMatches(value).map((m) { + final brand = m.group(0)!; + return _formatBrand(brand); + }).join(', '); + save('Compatible Brands', formattedBrands); break; case Keys.creationTime: save('Creation Time', _formatDate(value)); break; case Keys.date: - if (value != '0') { - final charCount = (value as String)?.length ?? 0; + if (value is String && value != '0') { + final charCount = value.length; save(charCount == 4 ? 'Year' : 'Date', value); } break; @@ -222,10 +229,10 @@ class VideoMetadataFormatter { static String _formatChannelLayout(value) => ChannelLayouts.names[value] ?? 'unknown ($value)'; - static String _formatCodecName(String value) => _codecNames[value] ?? value?.toUpperCase()?.replaceAll('_', ' '); + static String _formatCodecName(String value) => _codecNames[value] ?? value.toUpperCase().replaceAll('_', ' '); // input example: '2021-04-12T09:14:37.000000Z' - static String _formatDate(String value) { + static String? _formatDate(String value) { final date = DateTime.tryParse(value); if (date == null) return value; if (date == _epoch) return null; @@ -236,10 +243,10 @@ class VideoMetadataFormatter { static String _formatDuration(String value) { final match = _durationPattern.firstMatch(value); if (match != null) { - final h = int.tryParse(match.group(1)); - final m = int.tryParse(match.group(2)); - final s = int.tryParse(match.group(3)); - final millis = double.tryParse(match.group(4)); + final h = int.tryParse(match.group(1)!); + final m = int.tryParse(match.group(2)!); + final s = int.tryParse(match.group(3)!); + final millis = double.tryParse(match.group(4)!); if (h != null && m != null && s != null && millis != null) { return formatPreciseDuration(Duration( hours: h, @@ -258,15 +265,15 @@ class VideoMetadataFormatter { } static String _formatLanguage(String value) { - final language = Language.living639_2.firstWhere((language) => language.iso639_2 == value, orElse: () => null); + final language = Language.living639_2.firstWhereOrNull((language) => language.iso639_2 == value); return language?.native ?? value; } // format ISO 6709 input, e.g. '+37.5090+127.0243/' (Samsung), '+51.3328-000.7053+113.474/' (Apple) - static String _formatLocation(String value) { + static String? _formatLocation(String value) { final matches = _locationPattern.allMatches(value); if (matches.isNotEmpty) { - final coordinates = matches.map((m) => double.tryParse(m.group(0))).toList(); + final coordinates = matches.map((m) => double.tryParse(m.group(0)!)).toList(); if (coordinates.every((c) => c == 0)) return null; return coordinates.join(', '); } diff --git a/lib/ref/brand_colors.dart b/lib/ref/brand_colors.dart index 17db1afd0..b175781b2 100644 --- a/lib/ref/brand_colors.dart +++ b/lib/ref/brand_colors.dart @@ -7,17 +7,15 @@ class BrandColors { static const Color android = Color(0xFF3DDC84); static const Color flutter = Color(0xFF47D1FD); - static Color get(String text) { - if (text != null) { - switch (text.toLowerCase()) { - case 'after effects': - return adobeAfterEffects; - case 'illustrator': - return adobeIllustrator; - case 'photoshop': - case 'lightroom': - return adobePhotoshop; - } + static Color? get(String text) { + switch (text.toLowerCase()) { + case 'after effects': + return adobeAfterEffects; + case 'illustrator': + return adobeIllustrator; + case 'photoshop': + case 'lightroom': + return adobePhotoshop; } return null; } diff --git a/lib/ref/exif.dart b/lib/ref/exif.dart index 1c6b890cf..26fa9bb6d 100644 --- a/lib/ref/exif.dart +++ b/lib/ref/exif.dart @@ -133,7 +133,7 @@ class Exif { } static String getExifVersionDescription(String valueString) { - if (valueString?.length == 4) { + if (valueString.length == 4) { final major = int.tryParse(valueString.substring(0, 2)); final minor = int.tryParse(valueString.substring(2, 4)); if (major != null && minor != null) { diff --git a/lib/ref/languages.dart b/lib/ref/languages.dart index 5deab364a..a26cc982f 100644 --- a/lib/ref/languages.dart +++ b/lib/ref/languages.dart @@ -1,9 +1,10 @@ class Language { - final String iso639_2, name, native; + final String iso639_2, name; + final String? native; const Language({ - this.iso639_2, - this.name, + required this.iso639_2, + required this.name, this.native, }); diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 38ae05445..4b1f50d2e 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -14,7 +14,7 @@ class AndroidAppService { final result = await platform.invokeMethod('getPackages'); final packages = (result as List).cast().map((map) => Package.fromMap(map)).toSet(); // additional info for known directories - final kakaoTalk = packages.firstWhere((package) => package.packageName == 'com.kakao.talk', orElse: () => null); + final kakaoTalk = packages.firstWhereOrNull((package) => package.packageName == 'com.kakao.talk'); if (kakaoTalk != null) { kakaoTalk.ownedDirs.add('KakaoTalkDownload'); } @@ -31,19 +31,20 @@ class AndroidAppService { 'packageName': packageName, 'sizeDip': size, }); - return result as Uint8List; + if (result != null) return result as Uint8List; } on PlatformException catch (e) { debugPrint('getAppIcon failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return Uint8List(0); } static Future edit(String uri, String mimeType) async { try { - return await platform.invokeMethod('edit', { + final result = await platform.invokeMethod('edit', { 'uri': uri, 'mimeType': mimeType, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('edit failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -52,10 +53,11 @@ class AndroidAppService { static Future open(String uri, String mimeType) async { try { - return await platform.invokeMethod('open', { + final result = await platform.invokeMethod('open', { 'uri': uri, 'mimeType': mimeType, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('open failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -64,9 +66,10 @@ class AndroidAppService { static Future openMap(String geoUri) async { try { - return await platform.invokeMethod('openMap', { + final result = await platform.invokeMethod('openMap', { 'geoUri': geoUri, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('openMap failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -75,10 +78,11 @@ class AndroidAppService { static Future setAs(String uri, String mimeType) async { try { - return await platform.invokeMethod('setAs', { + final result = await platform.invokeMethod('setAs', { 'uri': uri, 'mimeType': mimeType, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('setAs failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -90,9 +94,10 @@ class AndroidAppService { // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); try { - return await platform.invokeMethod('share', { + final result = await platform.invokeMethod('share', { 'urisByMimeType': urisByMimeType, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('shareEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -101,11 +106,12 @@ class AndroidAppService { static Future shareSingle(String uri, String mimeType) async { try { - return await platform.invokeMethod('share', { + final result = await platform.invokeMethod('share', { 'urisByMimeType': { mimeType: [uri] }, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('shareSingle failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index cf89f8423..257e79be2 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.dart @@ -9,7 +9,7 @@ class AndroidDebugService { static Future getContextDirs() async { try { final result = await platform.invokeMethod('getContextDirs'); - return result as Map; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getContextDirs failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } @@ -19,7 +19,7 @@ class AndroidDebugService { static Future getEnv() async { try { final result = await platform.invokeMethod('getEnv'); - return result as Map; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getEnv failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } @@ -31,8 +31,8 @@ class AndroidDebugService { // returns map with all data available when decoding image bounds with `BitmapFactory` final result = await platform.invokeMethod('getBitmapFactoryInfo', { 'uri': entry.uri, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getBitmapFactoryInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -45,8 +45,8 @@ class AndroidDebugService { final result = await platform.invokeMethod('getContentResolverMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getContentResolverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -60,8 +60,8 @@ class AndroidDebugService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getExifInterfaceMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -73,8 +73,8 @@ class AndroidDebugService { // returns map with all data available from `MediaMetadataRetriever` final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', { 'uri': entry.uri, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getMediaMetadataRetrieverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -88,8 +88,8 @@ class AndroidDebugService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getMetadataExtractorSummary failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -102,8 +102,8 @@ class AndroidDebugService { try { final result = await platform.invokeMethod('getTiffStructure', { 'uri': entry.uri, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getTiffStructure failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart index d13daa4cb..c81d9a7f8 100644 --- a/lib/services/app_shortcut_service.dart +++ b/lib/services/app_shortcut_service.dart @@ -10,24 +10,27 @@ class AppShortcutService { static const platform = MethodChannel('deckers.thibault/aves/shortcut'); // this ability will not change over the lifetime of the app - static bool _canPin; + static bool? _canPin; - static Future canPin() async { + static Future canPin() async { if (_canPin != null) { - return SynchronousFuture(_canPin); + return SynchronousFuture(_canPin!); } try { - _canPin = await platform.invokeMethod('canPin'); - return _canPin; + final result = await platform.invokeMethod('canPin'); + if (result != null) { + _canPin = result; + return result; + } } on PlatformException catch (e) { debugPrint('canPin failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } return false; } - static Future pin(String label, AvesEntry entry, Set filters) async { - Uint8List iconBytes; + static Future pin(String label, AvesEntry? entry, Set filters) async { + Uint8List? iconBytes; if (entry != null) { final size = entry.isVideo ? 0.0 : 256.0; iconBytes = await imageFileService.getThumbnail( @@ -44,7 +47,7 @@ class AppShortcutService { await platform.invokeMethod('pin', { 'label': label, 'iconBytes': iconBytes, - 'filters': filters.where((filter) => filter != null).map((filter) => filter.toJson()).toList(), + 'filters': filters.map((filter) => filter.toJson()).toList(), }); } on PlatformException catch (e) { debugPrint('pin failed with code=${e.code}, exception=${e.message}, details=${e.details}'); diff --git a/lib/services/embedded_data_service.dart b/lib/services/embedded_data_service.dart index b92aa2410..50cd15adf 100644 --- a/lib/services/embedded_data_service.dart +++ b/lib/services/embedded_data_service.dart @@ -11,7 +11,7 @@ abstract class EmbeddedDataService { Future extractVideoEmbeddedPicture(AvesEntry entry); - Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType); + Future extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType); } class PlatformEmbeddedDataService implements EmbeddedDataService { @@ -25,7 +25,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }); - return (result as List).cast(); + if (result != null) return (result as List).cast(); } on PlatformException catch (e) { debugPrint('getExifThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -41,11 +41,11 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'sizeBytes': entry.sizeBytes, 'displayName': '${entry.bestTitle} • Video', }); - return result; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('extractMotionPhotoVideo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return {}; } @override @@ -55,15 +55,15 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'uri': entry.uri, 'displayName': '${entry.bestTitle} • Cover', }); - return result; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('extractVideoEmbeddedPicture failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return {}; } @override - Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType) async { + Future extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType) async { try { final result = await platform.invokeMethod('extractXmpDataProp', { 'mimeType': entry.mimeType, @@ -73,10 +73,10 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'propPath': propPath, 'propMimeType': propMimeType, }); - return result; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('extractXmpDataProp failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return {}; } } diff --git a/lib/services/geocoding_service.dart b/lib/services/geocoding_service.dart index 3d69a01cf..c87baddf2 100644 --- a/lib/services/geocoding_service.dart +++ b/lib/services/geocoding_service.dart @@ -29,7 +29,7 @@ class GeocodingService { @immutable class Address { - final String addressLine, adminArea, countryCode, countryName, featureName, locality, postalCode, subAdminArea, subLocality, subThoroughfare, thoroughfare; + final String? addressLine, adminArea, countryCode, countryName, featureName, locality, postalCode, subAdminArea, subLocality, subThoroughfare, thoroughfare; const Address({ this.addressLine, diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 4bf6238e8..5a9fcfb6a 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -7,28 +7,31 @@ import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/service_policy.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; + +// ignore: import_of_legacy_library_into_null_safe import 'package:streams_channel/streams_channel.dart'; abstract class ImageFileService { - Future getEntry(String uri, String mimeType); + Future getEntry(String uri, String? mimeType); Future getSvg( String uri, String mimeType, { - int expectedContentLength, - BytesReceivedCallback onBytesReceived, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, }); Future getImage( String uri, String mimeType, - int rotationDegrees, + int? rotationDegrees, bool isFlipped, { - int pageId, - int expectedContentLength, - BytesReceivedCallback onBytesReceived, + int? pageId, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, }); // `rect`: region to decode, with coordinates in reference to `imageSize` @@ -40,21 +43,21 @@ abstract class ImageFileService { int sampleSize, Rectangle regionRect, Size imageSize, { - int pageId, - Object taskKey, - int priority, + int? pageId, + Object? taskKey, + int? priority, }); Future getThumbnail({ - @required String uri, - @required String mimeType, - @required int rotationDegrees, - @required int pageId, - @required bool isFlipped, - @required int dateModifiedSecs, - @required double extent, - Object taskKey, - int priority, + required String uri, + required String mimeType, + required int rotationDegrees, + required int? pageId, + required bool isFlipped, + required int? dateModifiedSecs, + required double extent, + Object? taskKey, + int? priority, }); Future clearSizedThumbnailDiskCache(); @@ -63,27 +66,27 @@ abstract class ImageFileService { bool cancelThumbnail(Object taskKey); - Future resumeLoading(Object taskKey); + Future? resumeLoading(Object taskKey); Stream delete(Iterable entries); Stream move( Iterable entries, { - @required bool copy, - @required String destinationAlbum, + required bool copy, + required String destinationAlbum, }); Stream export( Iterable entries, { - @required String mimeType, - @required String destinationAlbum, + required String mimeType, + required String destinationAlbum, }); - Future rename(AvesEntry entry, String newName); + Future> rename(AvesEntry entry, String newName); - Future rotate(AvesEntry entry, {@required bool clockwise}); + Future> rotate(AvesEntry entry, {required bool clockwise}); - Future flip(AvesEntry entry); + Future> flip(AvesEntry entry); } class PlatformImageFileService implements ImageFileService { @@ -108,7 +111,7 @@ class PlatformImageFileService implements ImageFileService { } @override - Future getEntry(String uri, String mimeType) async { + Future getEntry(String uri, String? mimeType) async { try { final result = await platform.invokeMethod('getEntry', { 'uri': uri, @@ -125,8 +128,8 @@ class PlatformImageFileService implements ImageFileService { Future getSvg( String uri, String mimeType, { - int expectedContentLength, - BytesReceivedCallback onBytesReceived, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, }) => getImage( uri, @@ -141,11 +144,11 @@ class PlatformImageFileService implements ImageFileService { Future getImage( String uri, String mimeType, - int rotationDegrees, + int? rotationDegrees, bool isFlipped, { - int pageId, - int expectedContentLength, - BytesReceivedCallback onBytesReceived, + int? pageId, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, }) { try { final completer = Completer.sync(); @@ -155,7 +158,7 @@ class PlatformImageFileService implements ImageFileService { 'uri': uri, 'mimeType': mimeType, 'rotationDegrees': rotationDegrees ?? 0, - 'isFlipped': isFlipped ?? false, + 'isFlipped': isFlipped, 'pageId': pageId, }).listen( (data) { @@ -182,7 +185,7 @@ class PlatformImageFileService implements ImageFileService { } on PlatformException catch (e) { debugPrint('getImage failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return Future.sync(() => null); + return Future.sync(() => Uint8List(0)); } @override @@ -194,9 +197,9 @@ class PlatformImageFileService implements ImageFileService { int sampleSize, Rectangle regionRect, Size imageSize, { - int pageId, - Object taskKey, - int priority, + int? pageId, + Object? taskKey, + int? priority, }) { return servicePolicy.call( () async { @@ -213,11 +216,11 @@ class PlatformImageFileService implements ImageFileService { 'imageWidth': imageSize.width.toInt(), 'imageHeight': imageSize.height.toInt(), }); - return result as Uint8List; + if (result != null) return result as Uint8List; } on PlatformException catch (e) { debugPrint('getRegion failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return Uint8List(0); }, priority: priority ?? ServiceCallPriority.getRegion, key: taskKey, @@ -226,18 +229,18 @@ class PlatformImageFileService implements ImageFileService { @override Future getThumbnail({ - @required String uri, - @required String mimeType, - @required int rotationDegrees, - @required int pageId, - @required bool isFlipped, - @required int dateModifiedSecs, - @required double extent, - Object taskKey, - int priority, + required String uri, + required String mimeType, + required int rotationDegrees, + required int? pageId, + required bool isFlipped, + required int? dateModifiedSecs, + required double extent, + Object? taskKey, + int? priority, }) { if (mimeType == MimeTypes.svg) { - return Future.sync(() => null); + return Future.sync(() => Uint8List(0)); } return servicePolicy.call( () async { @@ -253,11 +256,11 @@ class PlatformImageFileService implements ImageFileService { 'pageId': pageId, 'defaultSizeDip': thumbnailDefaultSize, }); - return result as Uint8List; + if (result != null) return result as Uint8List; } on PlatformException catch (e) { debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return Uint8List(0); }, priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), key: taskKey, @@ -280,7 +283,7 @@ class PlatformImageFileService implements ImageFileService { bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]); @override - Future resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); + Future? resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); @override Stream delete(Iterable entries) { @@ -298,8 +301,8 @@ class PlatformImageFileService implements ImageFileService { @override Stream move( Iterable entries, { - @required bool copy, - @required String destinationAlbum, + required bool copy, + required String destinationAlbum, }) { try { return _opStreamChannel.receiveBroadcastStream({ @@ -317,8 +320,8 @@ class PlatformImageFileService implements ImageFileService { @override Stream export( Iterable entries, { - @required String mimeType, - @required String destinationAlbum, + required String mimeType, + required String destinationAlbum, }) { try { return _opStreamChannel.receiveBroadcastStream({ @@ -334,14 +337,14 @@ class PlatformImageFileService implements ImageFileService { } @override - Future rename(AvesEntry entry, String newName) async { + Future> rename(AvesEntry entry, String newName) async { try { // returns map with: 'contentId' 'path' 'title' 'uri' (all optional) final result = await platform.invokeMethod('rename', { 'entry': _toPlatformEntryMap(entry), 'newName': newName, - }) as Map; - return result; + }); + if (result != null) return (result as Map).cast(); } on PlatformException catch (e) { debugPrint('rename failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -349,14 +352,14 @@ class PlatformImageFileService implements ImageFileService { } @override - Future rotate(AvesEntry entry, {@required bool clockwise}) async { + Future> rotate(AvesEntry entry, {required bool clockwise}) async { try { // returns map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('rotate', { 'entry': _toPlatformEntryMap(entry), 'clockwise': clockwise, - }) as Map; - return result; + }); + if (result != null) return (result as Map).cast(); } on PlatformException catch (e) { debugPrint('rotate failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -364,13 +367,13 @@ class PlatformImageFileService implements ImageFileService { } @override - Future flip(AvesEntry entry) async { + Future> flip(AvesEntry entry) async { try { // returns map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('flip', { 'entry': _toPlatformEntryMap(entry), - }) as Map; - return result; + }); + if (result != null) return (result as Map).cast(); } on PlatformException catch (e) { debugPrint('flip failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -379,18 +382,18 @@ class PlatformImageFileService implements ImageFileService { } // cf flutter/foundation `consolidateHttpClientResponseBytes` -typedef BytesReceivedCallback = void Function(int cumulative, int total); +typedef BytesReceivedCallback = void Function(int cumulative, int? total); // cf flutter/foundation `consolidateHttpClientResponseBytes` class _OutputBuffer extends ByteConversionSinkBase { - List> _chunks = >[]; + List>? _chunks = >[]; int _contentLength = 0; - Uint8List _bytes; + Uint8List? _bytes; @override void add(List chunk) { assert(_bytes == null); - _chunks.add(chunk); + _chunks!.add(chunk); _contentLength += chunk.length; } @@ -402,8 +405,8 @@ class _OutputBuffer extends ByteConversionSinkBase { } _bytes = Uint8List(_contentLength); var offset = 0; - for (final chunk in _chunks) { - _bytes.setRange(offset, offset + chunk.length, chunk); + for (final chunk in _chunks!) { + _bytes!.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; } _chunks = null; @@ -411,6 +414,6 @@ class _OutputBuffer extends ByteConversionSinkBase { Uint8List get bytes { assert(_bytes != null); - return _bytes; + return _bytes!; } } diff --git a/lib/services/image_op_events.dart b/lib/services/image_op_events.dart index 2f30d8fe7..6d172f8a1 100644 --- a/lib/services/image_op_events.dart +++ b/lib/services/image_op_events.dart @@ -7,8 +7,8 @@ class ImageOpEvent { final String uri; const ImageOpEvent({ - this.success, - this.uri, + required this.success, + required this.uri, }); factory ImageOpEvent.fromMap(Map map) { @@ -34,7 +34,7 @@ class ImageOpEvent { class MoveOpEvent extends ImageOpEvent { final Map newFields; - const MoveOpEvent({bool success, String uri, this.newFields}) + const MoveOpEvent({required bool success, required String uri, required this.newFields}) : super( success: success, uri: uri, @@ -44,7 +44,7 @@ class MoveOpEvent extends ImageOpEvent { return MoveOpEvent( success: map['success'] ?? false, uri: map['uri'], - newFields: map['newFields'], + newFields: map['newFields'] ?? {}, ); } @@ -53,9 +53,9 @@ class MoveOpEvent extends ImageOpEvent { } class ExportOpEvent extends MoveOpEvent { - final int pageId; + final int? pageId; - const ExportOpEvent({bool success, String uri, this.pageId, Map newFields}) + const ExportOpEvent({required bool success, required String uri, this.pageId, required Map newFields}) : super( success: success, uri: uri, @@ -67,7 +67,7 @@ class ExportOpEvent extends MoveOpEvent { success: map['success'] ?? false, uri: map['uri'], pageId: map['pageId'], - newFields: map['newFields'], + newFields: map['newFields'] ?? {}, ); } diff --git a/lib/services/media_store_service.dart b/lib/services/media_store_service.dart index edd6e134b..0bfd42d1a 100644 --- a/lib/services/media_store_service.dart +++ b/lib/services/media_store_service.dart @@ -3,12 +3,13 @@ import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:streams_channel/streams_channel.dart'; abstract class MediaStoreService { Future> checkObsoleteContentIds(List knownContentIds); - Future> checkObsoletePaths(Map knownPathById); + Future> checkObsoletePaths(Map knownPathById); // knownEntries: map of contentId -> dateModifiedSecs Stream getEntries(Map knownEntries); @@ -32,7 +33,7 @@ class PlatformMediaStoreService implements MediaStoreService { } @override - Future> checkObsoletePaths(Map knownPathById) async { + Future> checkObsoletePaths(Map knownPathById) async { try { final result = await platform.invokeMethod('checkObsoletePaths', { 'knownPathById': knownPathById, diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index e5f2eabe6..bb77482dc 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -10,15 +10,15 @@ abstract class MetadataService { // returns Map> (map of directories, each directory being a map of metadata label and value description) Future getAllMetadata(AvesEntry entry); - Future getCatalogMetadata(AvesEntry entry, {bool background = false}); + Future getCatalogMetadata(AvesEntry entry, {bool background = false}); - Future getOverlayMetadata(AvesEntry entry); + Future getOverlayMetadata(AvesEntry entry); - Future getMultiPageInfo(AvesEntry entry); + Future getMultiPageInfo(AvesEntry entry); - Future getPanoramaInfo(AvesEntry entry); + Future getPanoramaInfo(AvesEntry entry); - Future getContentResolverProp(AvesEntry entry, String prop); + Future getContentResolverProp(AvesEntry entry, String prop); } class PlatformMetadataService implements MetadataService { @@ -26,7 +26,7 @@ class PlatformMetadataService implements MetadataService { @override Future getAllMetadata(AvesEntry entry) async { - if (entry.isSvg) return null; + if (entry.isSvg) return {}; try { final result = await platform.invokeMethod('getAllMetadata', { @@ -34,7 +34,7 @@ class PlatformMetadataService implements MetadataService { 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }); - return result as Map; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getAllMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -42,10 +42,10 @@ class PlatformMetadataService implements MetadataService { } @override - Future getCatalogMetadata(AvesEntry entry, {bool background = false}) async { + Future getCatalogMetadata(AvesEntry entry, {bool background = false}) async { if (entry.isSvg) return null; - Future call() async { + Future call() async { try { // returns map with: // 'mimeType': MIME type as reported by metadata extractors, not Media Store (string) @@ -80,7 +80,7 @@ class PlatformMetadataService implements MetadataService { } @override - Future getOverlayMetadata(AvesEntry entry) async { + Future getOverlayMetadata(AvesEntry entry) async { if (entry.isSvg) return null; try { @@ -98,7 +98,7 @@ class PlatformMetadataService implements MetadataService { } @override - Future getMultiPageInfo(AvesEntry entry) async { + Future getMultiPageInfo(AvesEntry entry) async { try { final result = await platform.invokeMethod('getMultiPageInfo', { 'mimeType': entry.mimeType, @@ -120,7 +120,7 @@ class PlatformMetadataService implements MetadataService { } @override - Future getPanoramaInfo(AvesEntry entry) async { + Future getPanoramaInfo(AvesEntry entry) async { try { // returns map with values for: // 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int), @@ -138,7 +138,7 @@ class PlatformMetadataService implements MetadataService { } @override - Future getContentResolverProp(AvesEntry entry, String prop) async { + Future getContentResolverProp(AvesEntry entry, String prop) async { try { return await platform.invokeMethod('getContentResolverProp', { 'mimeType': entry.mimeType, diff --git a/lib/services/service_policy.dart b/lib/services/service_policy.dart index c4521b22a..bc3e4321f 100644 --- a/lib/services/service_policy.dart +++ b/lib/services/service_policy.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:collection'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:tuple/tuple.dart'; @@ -22,19 +23,19 @@ class ServicePolicy { Future call( Future Function() platformCall, { int priority = ServiceCallPriority.normal, - Object key, + Object? key, }) { Completer completer; - _Task task; + _Task task; key ??= platformCall.hashCode; final toResume = _paused.remove(key); if (toResume != null) { priority = toResume.item1; - task = toResume.item2; + task = toResume.item2 as _Task; completer = task.completer; } else { completer = Completer(); - task = _Task( + task = _Task( () async { try { completer.complete(await platformCall()); @@ -52,11 +53,11 @@ class ServicePolicy { return completer.future; } - Future resume(Object key) { + Future? resume(Object key) { final toResume = _paused.remove(key); if (toResume != null) { final priority = toResume.item1; - final task = toResume.item2; + final task = toResume.item2 as _Task; _getQueue(priority)[key] = task; _pickNext(); return task.completer.future; @@ -70,10 +71,10 @@ class ServicePolicy { void _pickNext() { _notifyQueueState(); if (_runningQueue.length >= concurrentTaskMax) return; - final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value; + final queue = _queues.entries.firstWhereOrNull((kv) => kv.value.isNotEmpty)?.value; if (queue != null && queue.isNotEmpty) { final key = queue.keys.first; - final task = queue.remove(key); + final task = queue.remove(key)!; _runningQueue[key] = task; task.callback(); } @@ -109,9 +110,9 @@ class ServicePolicy { } } -class _Task { +class _Task { final VoidCallback callback; - final Completer completer; + final Completer completer; const _Task(this.callback, this.completer); } diff --git a/lib/services/services.dart b/lib/services/services.dart index c863b0a36..1ffdf90b6 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -11,16 +11,16 @@ import 'package:path/path.dart' as p; final getIt = GetIt.instance; -final pContext = getIt(); -final availability = getIt(); -final metadataDb = getIt(); +final p.Context pContext = getIt(); +final AvesAvailability availability = getIt(); +final MetadataDb metadataDb = getIt(); -final embeddedDataService = getIt(); -final imageFileService = getIt(); -final mediaStoreService = getIt(); -final metadataService = getIt(); -final storageService = getIt(); -final timeService = getIt(); +final EmbeddedDataService embeddedDataService = getIt(); +final ImageFileService imageFileService = getIt(); +final MediaStoreService mediaStoreService = getIt(); +final MetadataService metadataService = getIt(); +final StorageService storageService = getIt(); +final TimeService timeService = getIt(); void initPlatformServices() { getIt.registerLazySingleton(() => p.Context()); diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 80809a23a..b06c57b04 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -3,12 +3,13 @@ import 'dart:async'; import 'package:aves/utils/android_file_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:streams_channel/streams_channel.dart'; abstract class StorageService { Future> getStorageVolumes(); - Future getFreeSpace(StorageVolume volume); + Future getFreeSpace(StorageVolume volume); Future> getGrantedDirectories(); @@ -25,7 +26,7 @@ abstract class StorageService { Future deleteEmptyDirectories(Iterable dirPaths); // returns media URI - Future scanFile(String path, String mimeType); + Future scanFile(String path, String mimeType); } class PlatformStorageService implements StorageService { @@ -44,16 +45,16 @@ class PlatformStorageService implements StorageService { } @override - Future getFreeSpace(StorageVolume volume) async { + Future getFreeSpace(StorageVolume volume) async { try { final result = await platform.invokeMethod('getFreeSpace', { 'path': volume.path, }); - return result as int; + return result as int?; } on PlatformException catch (e) { debugPrint('getFreeSpace failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } - return 0; + return null; } @override @@ -85,22 +86,26 @@ class PlatformStorageService implements StorageService { final result = await platform.invokeMethod('getInaccessibleDirectories', { 'dirPaths': dirPaths.toList(), }); - return (result as List).cast().map((map) => VolumeRelativeDirectory.fromMap(map)).toSet(); + if (result != null) { + return (result as List).cast().map(VolumeRelativeDirectory.fromMap).toSet(); + } } on PlatformException catch (e) { debugPrint('getInaccessibleDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } - return null; + return {}; } @override Future> getRestrictedDirectories() async { try { final result = await platform.invokeMethod('getRestrictedDirectories'); - return (result as List).cast().map((map) => VolumeRelativeDirectory.fromMap(map)).toSet(); + if (result != null) { + return (result as List).cast().map(VolumeRelativeDirectory.fromMap).toSet(); + } } on PlatformException catch (e) { debugPrint('getRestrictedDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } - return null; + return {}; } // returns whether user granted access to volume root at `volumePath` @@ -111,7 +116,7 @@ class PlatformStorageService implements StorageService { storageAccessChannel.receiveBroadcastStream({ 'path': volumePath, }).listen( - (data) => completer.complete(data as bool), + (data) => completer.complete(data as bool?), onError: completer.completeError, onDone: () { if (!completer.isCompleted) completer.complete(false); @@ -129,9 +134,10 @@ class PlatformStorageService implements StorageService { @override Future deleteEmptyDirectories(Iterable dirPaths) async { try { - return await platform.invokeMethod('deleteEmptyDirectories', { + final result = await platform.invokeMethod('deleteEmptyDirectories', { 'dirPaths': dirPaths.toList(), }); + if (result != null) return result as int; } on PlatformException catch (e) { debugPrint('deleteEmptyDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } @@ -140,14 +146,14 @@ class PlatformStorageService implements StorageService { // returns media URI @override - Future scanFile(String path, String mimeType) async { + Future scanFile(String path, String mimeType) async { debugPrint('scanFile with path=$path, mimeType=$mimeType'); try { - final uriString = await platform.invokeMethod('scanFile', { + final result = await platform.invokeMethod('scanFile', { 'path': path, 'mimeType': mimeType, }); - return Uri.tryParse(uriString ?? ''); + if (result != null) return Uri.tryParse(result); } on PlatformException catch (e) { debugPrint('scanFile failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } diff --git a/lib/services/svg_metadata_service.dart b/lib/services/svg_metadata_service.dart index 94fbf36a4..3a282b883 100644 --- a/lib/services/svg_metadata_service.dart +++ b/lib/services/svg_metadata_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:aves/model/entry.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/string_utils.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:xml/xml.dart'; @@ -15,15 +16,15 @@ class SvgMetadataService { static const _textElements = ['title', 'desc']; static const _metadataElement = 'metadata'; - static Future getSize(AvesEntry entry) async { + static Future getSize(AvesEntry entry) async { try { final data = await imageFileService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; - String getAttribute(String attributeName) => root.attributes.firstWhere((a) => a.name.qualified == attributeName, orElse: () => null)?.value; - double tryParseWithoutUnit(String s) => s == null ? null : double.tryParse(s.replaceAll(RegExp(r'[a-z%]'), '')); + String? getAttribute(String attributeName) => root.attributes.firstWhereOrNull((a) => a.name.qualified == attributeName)?.value; + double? tryParseWithoutUnit(String? s) => s == null ? null : double.tryParse(s.replaceAll(RegExp(r'[a-z%]'), '')); final width = tryParseWithoutUnit(getAttribute('width')); final height = tryParseWithoutUnit(getAttribute('height')); @@ -37,7 +38,7 @@ class SvgMetadataService { if (parts.length == 4) { final vbWidth = tryParseWithoutUnit(parts[2]); final vbHeight = tryParseWithoutUnit(parts[3]); - if (vbWidth > 0 && vbHeight > 0) { + if (vbWidth != null && vbWidth > 0 && vbHeight != null && vbHeight > 0) { return Size(vbWidth, vbHeight); } } @@ -66,7 +67,7 @@ class SvgMetadataService { final docDir = Map.fromEntries([ ...root.attributes.where((a) => _attributes.contains(a.name.qualified)).map((a) => MapEntry(formatKey(a.name.qualified), a.value)), - ..._textElements.map((name) => MapEntry(formatKey(name), root.getElement(name)?.text)).where((kv) => kv.value != null), + ..._textElements.map((name) => MapEntry(formatKey(name), root.getElement(name)?.text)).where((kv) => kv.value != null).cast>(), ]); final metadata = root.getElement(_metadataElement); @@ -80,7 +81,7 @@ class SvgMetadataService { }; } catch (error, stack) { debugPrint('failed to parse XML from SVG with error=$error\n$stack'); - return null; } + return {}; } } diff --git a/lib/services/time_service.dart b/lib/services/time_service.dart index d9b284bea..af9b90613 100644 --- a/lib/services/time_service.dart +++ b/lib/services/time_service.dart @@ -2,14 +2,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; abstract class TimeService { - Future getDefaultTimeZone(); + Future getDefaultTimeZone(); } class PlatformTimeService implements TimeService { static const platform = MethodChannel('deckers.thibault/aves/time'); @override - Future getDefaultTimeZone() async { + Future getDefaultTimeZone() async { try { return await platform.invokeMethod('getDefaultTimeZone'); } on PlatformException catch (e) { diff --git a/lib/services/viewer_service.dart b/lib/services/viewer_service.dart index 76c790375..336ebd99b 100644 --- a/lib/services/viewer_service.dart +++ b/lib/services/viewer_service.dart @@ -4,10 +4,11 @@ import 'package:flutter/services.dart'; class ViewerService { static const platform = MethodChannel('deckers.thibault/aves/viewer'); - static Future getIntentData() async { + static Future> getIntentData() async { try { // returns nullable map with 'action' and possibly 'uri' 'mimeType' - return await platform.invokeMethod('getIntentData') as Map; + final result = await platform.invokeMethod('getIntentData'); + if (result != null) return (result as Map).cast(); } on PlatformException catch (e) { debugPrint('getIntentData failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 61315ca85..1e4689f03 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class AIcons { diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index 85fb14eca..6d2267d9e 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -2,13 +2,14 @@ import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { - String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath; + late String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath; Set storageVolumes = {}; Set _packages = {}; List _potentialAppDirs = []; @@ -22,7 +23,7 @@ class AndroidFileUtils { Future init() async { storageVolumes = await storageService.getStorageVolumes(); // path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files' - primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path; + primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? '/'; dcimPath = pContext.join(primaryStorage, 'DCIM'); downloadPath = pContext.join(primaryStorage, 'Download'); moviesPath = pContext.join(primaryStorage, 'Movies'); @@ -35,16 +36,17 @@ class AndroidFileUtils { appNameChangeNotifier.notifyListeners(); } - bool isCameraPath(String path) => path != null && path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO')); + bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO')); - bool isScreenshotsPath(String path) => path != null && (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('Screenshots'); + bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('Screenshots'); - bool isScreenRecordingsPath(String path) => path != null && (path.startsWith(dcimPath) || path.startsWith(moviesPath)) && (path.endsWith('Screen recordings') || path.endsWith('ScreenRecords')); + bool isScreenRecordingsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(moviesPath)) && (path.endsWith('Screen recordings') || path.endsWith('ScreenRecords')); bool isDownloadPath(String path) => path == downloadPath; - StorageVolume getStorageVolume(String path) { - final volume = storageVolumes.firstWhere((v) => path.startsWith(v.path), orElse: () => null); + StorageVolume? getStorageVolume(String? path) { + if (path == null) return null; + final volume = storageVolumes.firstWhereOrNull((v) => path.startsWith(v.path)); // storage volume path includes trailing '/', but argument path may or may not, // which is an issue when the path is at the root return volume != null || path.endsWith('/') ? volume : getStorageVolume('$path/'); @@ -53,27 +55,25 @@ class AndroidFileUtils { bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; AlbumType getAlbumType(String albumPath) { - if (albumPath != null) { - if (isCameraPath(albumPath)) return AlbumType.camera; - if (isDownloadPath(albumPath)) return AlbumType.download; - if (isScreenRecordingsPath(albumPath)) return AlbumType.screenRecordings; - if (isScreenshotsPath(albumPath)) return AlbumType.screenshots; + if (isCameraPath(albumPath)) return AlbumType.camera; + if (isDownloadPath(albumPath)) return AlbumType.download; + if (isScreenRecordingsPath(albumPath)) return AlbumType.screenRecordings; + if (isScreenshotsPath(albumPath)) return AlbumType.screenshots; + + final dir = pContext.split(albumPath).last; + if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; - final dir = pContext.split(albumPath).last; - if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; - } return AlbumType.regular; } - String getAlbumAppPackageName(String albumPath) { - if (albumPath == null) return null; + String? getAlbumAppPackageName(String albumPath) { final dir = pContext.split(albumPath).last; - final package = _launcherPackages.firstWhere((package) => package.potentialDirs.contains(dir), orElse: () => null); + final package = _launcherPackages.firstWhereOrNull((package) => package.potentialDirs.contains(dir)); return package?.packageName; } - String getCurrentAppName(String packageName) { - final package = _packages.firstWhere((package) => package.packageName == packageName, orElse: () => null); + String? getCurrentAppName(String packageName) { + final package = _packages.firstWhereOrNull((package) => package.packageName == packageName); return package?.currentLabel; } } @@ -81,25 +81,26 @@ class AndroidFileUtils { enum AlbumType { regular, app, camera, download, screenRecordings, screenshots } class Package { - final String packageName, currentLabel, englishLabel; + final String packageName; + final String? currentLabel, englishLabel; final bool categoryLauncher, isSystem; final Set ownedDirs = {}; Package({ - this.packageName, - this.currentLabel, - this.englishLabel, - this.categoryLauncher, - this.isSystem, + required this.packageName, + required this.currentLabel, + required this.englishLabel, + required this.categoryLauncher, + required this.isSystem, }); factory Package.fromMap(Map map) { return Package( - packageName: map['packageName'], + packageName: map['packageName'] ?? '', currentLabel: map['currentLabel'], englishLabel: map['englishLabel'], - categoryLauncher: map['categoryLauncher'], - isSystem: map['isSystem'], + categoryLauncher: map['categoryLauncher'] ?? false, + isSystem: map['isSystem'] ?? false, ); } @@ -107,7 +108,7 @@ class Package { currentLabel, englishLabel, ...ownedDirs, - ].where((dir) => dir != null).toSet(); + ].where((dir) => dir != null).cast().toSet(); @override String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}'; @@ -115,24 +116,25 @@ class Package { @immutable class StorageVolume { - final String _description, path, state; + final String? _description; + final String path, state; final bool isPrimary, isRemovable; const StorageVolume({ - String description, - this.isPrimary, - this.isRemovable, - this.path, - this.state, + required String? description, + required this.isPrimary, + required this.isRemovable, + required this.path, + required this.state, }) : _description = description; - String getDescription(BuildContext context) { - if (_description != null) return _description; + String getDescription(BuildContext? context) { + if (_description != null) return _description!; // ideally, the context should always be provided, but in some cases (e.g. album comparison), // this would require numerous additional methods to have the context as argument // for such a minor benefit: fallback volume description on Android < N - if (isPrimary) return context?.l10n?.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage'; - return context?.l10n?.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card'; + if (isPrimary) return context?.l10n.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage'; + return context?.l10n.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card'; } factory StorageVolume.fromMap(Map map) { @@ -152,19 +154,19 @@ class VolumeRelativeDirectory { final String volumePath, relativeDir; const VolumeRelativeDirectory({ - this.volumePath, - this.relativeDir, + required this.volumePath, + required this.relativeDir, }); - factory VolumeRelativeDirectory.fromMap(Map map) { + static VolumeRelativeDirectory fromMap(Map map) { return VolumeRelativeDirectory( - volumePath: map['volumePath'], + volumePath: map['volumePath'] ?? '', relativeDir: map['relativeDir'] ?? '', ); } // prefer static method over a null returning factory constructor - static VolumeRelativeDirectory fromPath(String dirPath) { + static VolumeRelativeDirectory? fromPath(String dirPath) { final volume = androidFileUtils.getStorageVolume(dirPath); if (volume == null) return null; @@ -177,7 +179,7 @@ class VolumeRelativeDirectory { } String getVolumeDescription(BuildContext context) { - final volume = androidFileUtils.storageVolumes.firstWhere((volume) => volume.path == volumePath, orElse: () => null); + final volume = androidFileUtils.storageVolumes.firstWhereOrNull((volume) => volume.path == volumePath); return volume?.getDescription(context) ?? volumePath; } diff --git a/lib/utils/change_notifier.dart b/lib/utils/change_notifier.dart index 727f629ea..2a1eddca8 100644 --- a/lib/utils/change_notifier.dart +++ b/lib/utils/change_notifier.dart @@ -2,22 +2,22 @@ import 'package:flutter/foundation.dart'; // reimplemented ChangeNotifier so that it can be used anywhere, not just as a mixin class AChangeNotifier implements Listenable { - ObserverList _listeners = ObserverList(); + ObserverList? _listeners = ObserverList(); @override - void addListener(VoidCallback listener) => _listeners.add(listener); + void addListener(VoidCallback listener) => _listeners!.add(listener); @override - void removeListener(VoidCallback listener) => _listeners.remove(listener); + void removeListener(VoidCallback listener) => _listeners!.remove(listener); void dispose() => _listeners = null; void notifyListeners() { if (_listeners == null) return; - final localListeners = List.from(_listeners); + final localListeners = List.from(_listeners!); for (final listener in localListeners) { try { - if (_listeners.contains(listener)) listener(); + if (_listeners!.contains(listener)) listener(); } catch (error, stack) { debugPrint('$runtimeType failed to notify listeners with error=$error\n$stack'); } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 731dcbec7..d341a3f28 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -310,9 +310,9 @@ class Dependency { final String licenseUrl; const Dependency({ - @required this.name, - @required this.license, - @required this.licenseUrl, - @required this.sourceUrl, + required this.name, + required this.license, + required this.licenseUrl, + required this.sourceUrl, }); } diff --git a/lib/utils/debouncer.dart b/lib/utils/debouncer.dart index b41cd90bc..0fc03fc88 100644 --- a/lib/utils/debouncer.dart +++ b/lib/utils/debouncer.dart @@ -5,11 +5,11 @@ import 'package:flutter/foundation.dart'; class Debouncer { final Duration delay; - Timer _timer; + Timer? _timer; - Debouncer({@required this.delay}); + Debouncer({required this.delay}); - void call(Function action) { + void call(VoidCallback action) { _timer?.cancel(); _timer = Timer(delay, action); } diff --git a/lib/utils/math_utils.dart b/lib/utils/math_utils.dart index 541ebb5c5..33c49fe99 100644 --- a/lib/utils/math_utils.dart +++ b/lib/utils/math_utils.dart @@ -1,7 +1,5 @@ import 'dart:math'; -import 'package:flutter/foundation.dart'; - final double _log2 = log(2); const double _piOver180 = pi / 180.0; @@ -9,12 +7,12 @@ double toDegrees(num radians) => radians / _piOver180; double toRadians(num degrees) => degrees * _piOver180; -int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / _log2).floor()); +int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / _log2).floor()) as int; -double roundToPrecision(final double value, {@required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals); +double roundToPrecision(final double value, {required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals); // e.g. x=12345, precision=3 should return 13000 int ceilBy(num x, int precision) { final factor = pow(10, precision); - return (x / factor).ceil() * factor; + return (x / factor).ceil() * (factor as int); } diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index d1d747aad..4b4950e41 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -3,7 +3,7 @@ extension ExtraString on String { static final _sentenceCaseStep2 = RegExp(r'([a-z])([A-Z])'); String toSentenceCase() { - var s = replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase()); + var s = replaceFirstMapped(RegExp('.'), (m) => m.group(0)!.toUpperCase()); return s.replaceAllMapped(_sentenceCaseStep1, (m) => ' ${m.group(1)}').replaceAllMapped(_sentenceCaseStep2, (m) => '${m.group(1)} ${m.group(2)}').trim(); } } diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart index 9e95fea75..bd8d9bebd 100644 --- a/lib/utils/time_utils.dart +++ b/lib/utils/time_utils.dart @@ -15,11 +15,11 @@ String formatPreciseDuration(Duration d) { } extension ExtraDateTime on DateTime { - bool isAtSameYearAs(DateTime other) => this?.year == other?.year; + bool isAtSameYearAs(DateTime? other) => year == other?.year; - bool isAtSameMonthAs(DateTime other) => isAtSameYearAs(other) && this?.month == other?.month; + bool isAtSameMonthAs(DateTime? other) => isAtSameYearAs(other) && month == other?.month; - bool isAtSameDayAs(DateTime other) => isAtSameMonthAs(other) && this?.day == other?.day; + bool isAtSameDayAs(DateTime? other) => isAtSameMonthAs(other) && day == other?.day; bool get isToday => isAtSameDayAs(DateTime.now()); diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index 0e383a192..570440212 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -13,7 +13,7 @@ class AppReference extends StatefulWidget { } class _AppReferenceState extends State { - Future _packageInfoLoader; + late Future _packageInfoLoader; @override void initState() { @@ -47,7 +47,7 @@ class _AppReferenceState extends State { builder: (context, snapshot) { return LinkChip( leading: AvesLogo( - size: style.fontSize * MediaQuery.textScaleFactorOf(context) * 1.25, + size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25, ), text: '${context.l10n.appName} ${snapshot.data?.version}', url: 'https://github.com/deckerst/aves', @@ -59,7 +59,7 @@ class _AppReferenceState extends State { Widget _buildFlutterLine() { final style = DefaultTextStyle.of(context).style; - final subColor = style.color.withOpacity(.6); + final subColor = style.color!.withOpacity(.6); return Text.rich( TextSpan( @@ -68,7 +68,7 @@ class _AppReferenceState extends State { child: Padding( padding: EdgeInsetsDirectional.only(end: 4), child: FlutterLogo( - size: style.fontSize * 1.25, + size: style.fontSize! * 1.25, ), ), ), diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index ff1f2a04f..7f029ee4f 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -12,8 +12,8 @@ class Licenses extends StatefulWidget { } class _LicensesState extends State { - final ValueNotifier _expandedNotifier = ValueNotifier(null); - List _platform, _flutterPlugins, _flutterPackages, _dartPackages; + final ValueNotifier _expandedNotifier = ValueNotifier(null); + late List _platform, _flutterPlugins, _flutterPackages, _dartPackages; @override void initState() { @@ -118,8 +118,8 @@ class LicenseRow extends StatelessWidget { @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - final bodyTextStyle = textTheme.bodyText2; - final subColor = bodyTextStyle.color.withOpacity(.6); + final bodyTextStyle = textTheme.bodyText2!; + final subColor = bodyTextStyle.color!.withOpacity(.6); return Padding( padding: EdgeInsets.symmetric(vertical: 8), diff --git a/lib/widgets/about/update.dart b/lib/widgets/about/update.dart index 02248105d..95d923d8e 100644 --- a/lib/widgets/about/update.dart +++ b/lib/widgets/about/update.dart @@ -11,7 +11,7 @@ class AboutUpdate extends StatefulWidget { } class _AboutUpdateState extends State { - Future _updateChecker; + late Future _updateChecker; @override void initState() { diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart new file mode 100644 index 000000000..c997ed0d8 --- /dev/null +++ b/lib/widgets/aves_app.dart @@ -0,0 +1,171 @@ +import 'dart:ui'; + +import 'package:aves/app_mode.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/media_store_source.dart'; +import 'package:aves/services/services.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/themes.dart'; +import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/behaviour/route_tracker.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; +import 'package:aves/widgets/home_page.dart'; +import 'package:aves/widgets/welcome_page.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:overlay_support/overlay_support.dart'; +import 'package:provider/provider.dart'; + +class AvesApp extends StatefulWidget { + @override + _AvesAppState createState() => _AvesAppState(); +} + +class _AvesAppState extends State { + final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); + late Future _appSetup; + final _mediaStoreSource = MediaStoreSource(); + final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); + final Set changedUris = {}; + + // observers are not registered when using the same list object with different items + // the list itself needs to be reassigned + List _navigatorObservers = []; + final EventChannel _contentChangeChannel = EventChannel('deckers.thibault/aves/contentchange'); + final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); + final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); + + Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : WelcomePage(); + + @override + void initState() { + super.initState(); + initPlatformServices(); + _appSetup = _setup(); + _contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String?)); + _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)); + } + + @override + Widget build(BuildContext context) { + // place the settings provider above `MaterialApp` + // so it can be used during navigation transitions + return ChangeNotifierProvider.value( + value: settings, + child: ListenableProvider>.value( + value: appModeNotifier, + child: Provider.value( + value: _mediaStoreSource, + child: HighlightInfoProvider( + child: OverlaySupport( + child: FutureBuilder( + future: _appSetup, + builder: (context, snapshot) { + final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; + final home = initialized + ? getFirstPage() + : Scaffold( + body: snapshot.hasError ? _buildError(snapshot.error!) : SizedBox(), + ); + return Selector( + selector: (context, s) => s.locale, + builder: (context, settingsLocale, child) { + return MaterialApp( + navigatorKey: _navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + onGenerateTitle: (context) => context.l10n.appName, + darkTheme: Themes.darkTheme, + themeMode: ThemeMode.dark, + locale: settingsLocale, + localizationsDelegates: [ + ...AppLocalizations.localizationsDelegates, + ], + supportedLocales: AppLocalizations.supportedLocales, + ); + }); + }, + ), + ), + ), + ), + ), + ); + } + + Widget _buildError(Object error) { + return Container( + alignment: Alignment.center, + padding: EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(AIcons.error), + SizedBox(height: 16), + Text(error.toString()), + ], + ), + ); + } + + Future _setup() async { + await Firebase.initializeApp().then((app) { + final crashlytics = FirebaseCrashlytics.instance; + FlutterError.onError = crashlytics.recordFlutterError; + crashlytics.setCustomKey('locales', window.locales.join(', ')); + final now = DateTime.now(); + crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})'); + crashlytics.setCustomKey( + 'build_mode', + kReleaseMode + ? 'release' + : kProfileMode + ? 'profile' + : 'debug'); + }); + await settings.init(); + await settings.initFirebase(); + _navigatorObservers = [ + FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()), + CrashlyticsRouteTracker(), + ]; + } + + void _onNewIntent(Map? intentData) { + debugPrint('$runtimeType onNewIntent with intentData=$intentData'); + + // do not reset when relaunching the app + if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; + + FirebaseCrashlytics.instance.log('New intent'); + _navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( + settings: RouteSettings(name: HomePage.routeName), + builder: (_) => getFirstPage(intentData: intentData), + )); + } + + void _onContentChange(String? uri) { + if (uri != null) changedUris.add(uri); + if (changedUris.isNotEmpty) { + _contentChangeDebouncer(() async { + final todo = changedUris.toSet(); + changedUris.clear(); + final tempUris = await _mediaStoreSource.refreshUris(todo); + if (tempUris.isNotEmpty) { + changedUris.addAll(tempUris); + _onContentChange(null); + } + }); + } + } +} diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 6fb8685ad..e33e6ba12 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -35,9 +35,9 @@ class CollectionAppBar extends StatefulWidget { final CollectionLens collection; const CollectionAppBar({ - Key key, - @required this.appBarHeightNotifier, - @required this.collection, + Key? key, + required this.appBarHeightNotifier, + required this.collection, }) : super(key: key); @override @@ -46,9 +46,9 @@ class CollectionAppBar extends StatefulWidget { class _CollectionAppBarState extends State with SingleTickerProviderStateMixin { final TextEditingController _searchFieldController = TextEditingController(); - EntrySetActionDelegate _actionDelegate; - AnimationController _browseToSelectAnimation; - Future _canAddShortcutsLoader; + late EntrySetActionDelegate _actionDelegate; + late AnimationController _browseToSelectAnimation; + late Future _canAddShortcutsLoader; CollectionLens get collection => widget.collection; @@ -68,7 +68,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ); _canAddShortcutsLoader = AppShortcutService.canPin(); _registerWidget(widget); - WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight()); + WidgetsBinding.instance!.addPostFrameCallback((_) => _updateHeight()); } @override @@ -127,8 +127,8 @@ class _CollectionAppBarState extends State with SingleTickerPr } Widget _buildAppBarLeading() { - VoidCallback onPressed; - String tooltip; + VoidCallback? onPressed; + String? tooltip; if (collection.isBrowsing) { onPressed = Scaffold.of(context).openDrawer; tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip; @@ -147,7 +147,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ); } - Widget _buildAppBarTitle() { + Widget? _buildAppBarTitle() { if (collection.isBrowsing) { final appMode = context.watch>().value; Widget title = Text(appMode.isPicking ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle); @@ -359,17 +359,18 @@ class _CollectionAppBarState extends State with SingleTickerPr final sortedFilters = List.from(filters)..sort(); defaultName = sortedFilters.first.getLabel(context); } - final result = await showDialog>( + final result = await showDialog>( context: context, builder: (context) => AddShortcutDialog( collection: collection, - defaultName: defaultName, + defaultName: defaultName ?? '', ), ); + if (result == null) return; + final coverEntry = result.item1; final name = result.item2; - - if (name == null || name.isEmpty) return; + if (name.isEmpty) return; unawaited(AppShortcutService.pin(name, coverEntry, filters)); } diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index fba492c3a..c32b22cfd 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -35,7 +35,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class CollectionGrid extends StatefulWidget { - final String settingsRouteKey; + final String? settingsRouteKey; const CollectionGrid({ this.settingsRouteKey, @@ -46,18 +46,18 @@ class CollectionGrid extends StatefulWidget { } class _CollectionGridState extends State { - TileExtentController _tileExtentController; + TileExtentController? _tileExtentController; @override Widget build(BuildContext context) { _tileExtentController ??= TileExtentController( - settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName, + settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName!, columnCountDefault: 4, extentMin: 46, spacing: 0, ); return TileExtentControllerProvider( - controller: _tileExtentController, + controller: _tileExtentController!, child: _CollectionGridContent(), ); } @@ -99,7 +99,7 @@ class _CollectionGridContent extends StatelessWidget { child: _CollectionSectionedContent( collection: collection, isScrollingNotifier: _isScrollingNotifier, - scrollController: PrimaryScrollController.of(context), + scrollController: PrimaryScrollController.of(context)!, ), ); }, @@ -119,9 +119,9 @@ class _CollectionSectionedContent extends StatefulWidget { final ScrollController scrollController; const _CollectionSectionedContent({ - @required this.collection, - @required this.isScrollingNotifier, - @required this.scrollController, + required this.collection, + required this.isScrollingNotifier, + required this.scrollController, }); @override @@ -177,9 +177,9 @@ class _CollectionScaler extends StatelessWidget { final Widget child; const _CollectionScaler({ - @required this.scrollableKey, - @required this.appBarHeightNotifier, - @required this.child, + required this.scrollableKey, + required this.appBarHeightNotifier, + required this.child, }); @override @@ -228,12 +228,12 @@ class _CollectionScrollView extends StatefulWidget { final ScrollController scrollController; const _CollectionScrollView({ - @required this.scrollableKey, - @required this.collection, - @required this.appBar, - @required this.appBarHeightNotifier, - @required this.isScrollingNotifier, - @required this.scrollController, + required this.scrollableKey, + required this.collection, + required this.appBar, + required this.appBarHeightNotifier, + required this.isScrollingNotifier, + required this.scrollController, }); @override @@ -241,7 +241,7 @@ class _CollectionScrollView extends StatefulWidget { } class _CollectionScrollViewState extends State<_CollectionScrollView> { - Timer _scrollMonitoringTimer; + Timer? _scrollMonitoringTimer; @override void initState() { diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart index 8706e10be..d7776d9a2 100644 --- a/lib/widgets/collection/draggable_thumb_label.dart +++ b/lib/widgets/collection/draggable_thumb_label.dart @@ -13,8 +13,8 @@ class CollectionDraggableThumbLabel extends StatelessWidget { final double offsetY; const CollectionDraggableThumbLabel({ - @required this.collection, - @required this.offsetY, + required this.collection, + required this.offsetY, }); @override @@ -28,7 +28,7 @@ class CollectionDraggableThumbLabel extends StatelessWidget { case EntryGroupFactor.album: return [ DraggableThumbLabel.formatMonthThumbLabel(context, entry.bestDate), - if (_hasMultipleSections(context)) context.read().getAlbumDisplayName(context, entry.directory), + if (_showAlbumName(context, entry)) _getAlbumName(context, entry), ]; case EntryGroupFactor.month: case EntryGroupFactor.none: @@ -40,21 +40,23 @@ class CollectionDraggableThumbLabel extends StatelessWidget { DraggableThumbLabel.formatDayThumbLabel(context, entry.bestDate), ]; } - break; case EntrySortFactor.name: return [ - if (_hasMultipleSections(context)) context.read().getAlbumDisplayName(context, entry.directory), - entry.bestTitle, + if (_showAlbumName(context, entry)) _getAlbumName(context, entry), + if (entry.bestTitle != null) entry.bestTitle!, ]; case EntrySortFactor.size: return [ - formatFilesize(entry.sizeBytes, round: 0), + if (entry.sizeBytes != null) formatFilesize(entry.sizeBytes!, round: 0), ]; } - return []; }, ); } bool _hasMultipleSections(BuildContext context) => context.read>().sections.length > 1; + + bool _showAlbumName(BuildContext context, AvesEntry entry) => _hasMultipleSections(context) && entry.directory != null; + + String _getAlbumName(BuildContext context, AvesEntry entry) => context.read().getAlbumDisplayName(context, entry.directory!); } diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 8b2e3094f..5ba7c8572 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -16,7 +16,6 @@ import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -28,7 +27,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware Set get selection => collection.selection; EntrySetActionDelegate({ - @required this.collection, + required this.collection, }); void onEntryActionSelected(BuildContext context, EntryAction action) { @@ -63,8 +62,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } } - Future _moveSelection(BuildContext context, {@required MoveType moveType}) async { - final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).toSet(); + Future _moveSelection(BuildContext context, {required MoveType moveType}) async { + final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).cast().toSet(); if (moveType == MoveType.move) { // check whether moving is possible given OS restrictions, // before asking to pick a destination album @@ -134,7 +133,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } Future _showDeleteDialog(BuildContext context) async { - final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).toSet(); + final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).cast().toSet(); final todoCount = selection.length; final confirmed = await showDialog( diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 8fdc8cddd..02cbb07fd 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -9,12 +9,12 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget { final List filters; final bool removable; - final FilterCallback onTap; + final FilterCallback? onTap; FilterBar({ - Key key, - @required Set filters, - @required this.removable, + Key? key, + required Set filters, + required this.removable, this.onTap, }) : filters = List.from(filters)..sort(), super(key: key); @@ -28,9 +28,9 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget { class _FilterBarState extends State { final GlobalKey _animatedListKey = GlobalKey(debugLabel: 'filter-bar-animated-list'); - CollectionFilter _userTappedFilter; + CollectionFilter? _userTappedFilter; - FilterCallback get onTap => widget.onTap; + FilterCallback? get onTap => widget.onTap; @override void didUpdateWidget(covariant FilterBar oldWidget) { @@ -46,7 +46,7 @@ class _FilterBarState extends State { // only animate item removal when triggered by a user interaction with the chip, // not from automatic chip replacement following chip selection final animate = _userTappedFilter == filter; - listState.removeItem( + listState!.removeItem( index, animate ? (context, animation) { @@ -69,7 +69,7 @@ class _FilterBarState extends State { }); added.forEach((filter) { final index = current.indexOf(filter); - listState.insertItem( + listState!.insertItem( index, duration: Duration.zero, ); @@ -95,7 +95,7 @@ class _FilterBarState extends State { physics: BouncingScrollPhysics(), padding: EdgeInsets.only(left: 8), itemBuilder: (context, index, animation) { - if (index >= widget.filters.length) return null; + if (index >= widget.filters.length) return SizedBox(); return _buildChip(widget.filters.toList()[index]); }, ), @@ -115,7 +115,7 @@ class _FilterBarState extends State { onTap: onTap != null ? (filter) { _userTappedFilter = filter; - onTap(filter); + onTap!(filter); } : null, ), diff --git a/lib/widgets/collection/grid/headers/album.dart b/lib/widgets/collection/grid/headers/album.dart index b2c5f3f93..a47e2bff3 100644 --- a/lib/widgets/collection/grid/headers/album.dart +++ b/lib/widgets/collection/grid/headers/album.dart @@ -2,36 +2,40 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/header.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/material.dart'; class AlbumSectionHeader extends StatelessWidget { - final String directory, albumName; + final String? directory, albumName; const AlbumSectionHeader({ - Key key, - @required this.directory, - @required this.albumName, + Key? key, + required this.directory, + required this.albumName, }) : super(key: key); @override Widget build(BuildContext context) { - var albumIcon = IconUtils.getAlbumIcon(context: context, album: directory); - if (albumIcon != null) { - albumIcon = Material( - type: MaterialType.circle, - elevation: 3, - color: Colors.transparent, - shadowColor: Colors.black, - child: albumIcon, - ); + Widget? albumIcon; + if (directory != null) { + albumIcon = IconUtils.getAlbumIcon(context: context, albumPath: directory!); + if (albumIcon != null) { + albumIcon = Material( + type: MaterialType.circle, + elevation: 3, + color: Colors.transparent, + shadowColor: Colors.black, + child: albumIcon, + ); + } } return SectionHeader( sectionKey: EntryAlbumSectionKey(directory), leading: albumIcon, - title: albumName, - trailing: androidFileUtils.isOnRemovableStorage(directory) + title: albumName ?? context.l10n.sectionUnknown, + trailing: directory != null && androidFileUtils.isOnRemovableStorage(directory!) ? Icon( AIcons.removableStorage, size: 16, @@ -42,7 +46,7 @@ class AlbumSectionHeader extends StatelessWidget { } static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, EntryAlbumSectionKey sectionKey) { - final directory = sectionKey.directory; + final directory = sectionKey.directory ?? context.l10n.sectionUnknown; return SectionHeader.getPreferredHeight( context: context, maxWidth: maxWidth, diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index 56f992c03..741481e49 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -15,10 +15,10 @@ class CollectionSectionHeader extends StatelessWidget { final double height; const CollectionSectionHeader({ - Key key, - @required this.collection, - @required this.sectionKey, - @required this.height, + Key? key, + required this.collection, + required this.sectionKey, + required this.height, }) : super(key: key); @override @@ -32,7 +32,7 @@ class CollectionSectionHeader extends StatelessWidget { : SizedBox.shrink(); } - Widget _buildHeader(BuildContext context) { + Widget? _buildHeader(BuildContext context) { switch (collection.sortFactor) { case EntrySortFactor.date: switch (collection.groupFactor) { @@ -60,7 +60,7 @@ class CollectionSectionHeader extends StatelessWidget { return AlbumSectionHeader( key: ValueKey(sectionKey), directory: directory, - albumName: source.getAlbumDisplayName(context, directory), + albumName: directory != null ? source.getAlbumDisplayName(context, directory) : null, ); } diff --git a/lib/widgets/collection/grid/headers/date.dart b/lib/widgets/collection/grid/headers/date.dart index 2e09e7874..983c84a23 100644 --- a/lib/widgets/collection/grid/headers/date.dart +++ b/lib/widgets/collection/grid/headers/date.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class DaySectionHeader extends StatelessWidget { - final DateTime date; + final DateTime? date; const DaySectionHeader({ - Key key, - @required this.date, + Key? key, + required this.date, }) : super(key: key); // Examples (en_US): @@ -33,7 +33,7 @@ class DaySectionHeader extends StatelessWidget { // `MEd`: `1. 26. (화)` // `yMEd`: `2021. 1. 26. (화)` - static String _formatDate(BuildContext context, DateTime date) { + static String _formatDate(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; if (date.isToday) return l10n.dateToday; @@ -53,14 +53,14 @@ class DaySectionHeader extends StatelessWidget { } class MonthSectionHeader extends StatelessWidget { - final DateTime date; + final DateTime? date; const MonthSectionHeader({ - Key key, - @required this.date, + Key? key, + required this.date, }) : super(key: key); - static String _formatDate(BuildContext context, DateTime date) { + static String _formatDate(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; if (date.isThisMonth) return l10n.dateThisMonth; diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart index 33597cbd5..0e9f4b948 100644 --- a/lib/widgets/collection/grid/section_layout.dart +++ b/lib/widgets/collection/grid/section_layout.dart @@ -3,20 +3,19 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider { final CollectionLens collection; const SectionedEntryListLayoutProvider({ - @required this.collection, - @required double scrollableWidth, - @required int columnCount, - @required double tileExtent, - @required Widget Function(AvesEntry entry) tileBuilder, - @required Duration tileAnimationDelay, - @required Widget child, + required this.collection, + required double scrollableWidth, + required int columnCount, + required double tileExtent, + required Widget Function(AvesEntry entry) tileBuilder, + required Duration tileAnimationDelay, + required Widget child, }) : super( scrollableWidth: scrollableWidth, columnCount: columnCount, diff --git a/lib/widgets/collection/grid/selector.dart b/lib/widgets/collection/grid/selector.dart index be1f48d66..da1ee997a 100644 --- a/lib/widgets/collection/grid/selector.dart +++ b/lib/widgets/collection/grid/selector.dart @@ -19,10 +19,10 @@ class GridSelectionGestureDetector extends StatefulWidget { const GridSelectionGestureDetector({ this.selectable = true, - @required this.collection, - @required this.scrollController, - @required this.appBarHeightNotifier, - @required this.child, + required this.collection, + required this.scrollController, + required this.appBarHeightNotifier, + required this.child, }); @override @@ -30,16 +30,16 @@ class GridSelectionGestureDetector extends StatefulWidget { } class _GridSelectionGestureDetectorState extends State { - bool _pressing = false, _selecting; - int _fromIndex, _lastToIndex; - Offset _localPosition; - EdgeInsets _scrollableInsets; - double _scrollSpeedFactor; - Timer _updateTimer; + bool _pressing = false, _selecting = false; + late int _fromIndex, _lastToIndex; + late Offset _localPosition; + late EdgeInsets _scrollableInsets; + late double _scrollSpeedFactor; + Timer? _updateTimer; CollectionLens get collection => widget.collection; - List get entries => collection.sortedEntries; + List get entries => collection.sortedEntries; ScrollController get scrollController => widget.scrollController; @@ -102,7 +102,9 @@ class _GridSelectionGestureDetectorState extends State isScrollingNotifier; + final ValueNotifier? isScrollingNotifier; const InteractiveThumbnail({ - Key key, - this.collection, - @required this.entry, - @required this.tileExtent, + Key? key, + required this.collection, + required this.entry, + required this.tileExtent, this.isScrollingNotifier, }) : super(key: key); diff --git a/lib/widgets/collection/thumbnail/decorated.dart b/lib/widgets/collection/thumbnail/decorated.dart index 5ecc75d95..e885ba1d8 100644 --- a/lib/widgets/collection/thumbnail/decorated.dart +++ b/lib/widgets/collection/thumbnail/decorated.dart @@ -8,17 +8,17 @@ import 'package:flutter/material.dart'; class DecoratedThumbnail extends StatelessWidget { final AvesEntry entry; final double extent; - final CollectionLens collection; - final ValueNotifier cancellableNotifier; + final CollectionLens? collection; + final ValueNotifier? cancellableNotifier; final bool selectable, highlightable; static final Color borderColor = Colors.grey.shade700; static const double borderWidth = .5; const DecoratedThumbnail({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.extent, this.collection, this.cancellableNotifier, this.selectable = true, diff --git a/lib/widgets/collection/thumbnail/error.dart b/lib/widgets/collection/thumbnail/error.dart index 21849a5c3..701deb93f 100644 --- a/lib/widgets/collection/thumbnail/error.dart +++ b/lib/widgets/collection/thumbnail/error.dart @@ -13,9 +13,9 @@ class ErrorThumbnail extends StatefulWidget { final String tooltip; const ErrorThumbnail({ - @required this.entry, - @required this.extent, - @required this.tooltip, + required this.entry, + required this.extent, + required this.tooltip, }); @override @@ -23,7 +23,7 @@ class ErrorThumbnail extends StatefulWidget { } class _ErrorThumbnailState extends State { - Future _exists; + late Future _exists; AvesEntry get entry => widget.entry; @@ -32,7 +32,7 @@ class _ErrorThumbnailState extends State { @override void initState() { super.initState(); - _exists = entry.path != null ? File(entry.path).exists() : SynchronousFuture(true); + _exists = entry.path != null ? File(entry.path!).exists() : SynchronousFuture(true); } @override @@ -42,7 +42,7 @@ class _ErrorThumbnailState extends State { future: _exists, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) return SizedBox(); - final exists = snapshot.data; + final exists = snapshot.data!; return Container( alignment: Alignment.center, color: Colors.black, diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index 133088e95..167df28da 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -17,9 +17,9 @@ class ThumbnailEntryOverlay extends StatelessWidget { final double extent; const ThumbnailEntryOverlay({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.extent, }) : super(key: key); @override @@ -56,9 +56,9 @@ class ThumbnailSelectionOverlay extends StatelessWidget { static const duration = Durations.thumbnailOverlayAnimation; const ThumbnailSelectionOverlay({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.extent, }) : super(key: key); @override @@ -113,9 +113,9 @@ class ThumbnailHighlightOverlay extends StatefulWidget { final double extent; const ThumbnailHighlightOverlay({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.extent, }) : super(key: key); @override diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 400fa5b8d..85fb8e8b7 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -10,13 +10,13 @@ import 'package:flutter/material.dart'; class RasterImageThumbnail extends StatefulWidget { final AvesEntry entry; final double extent; - final ValueNotifier cancellableNotifier; - final Object heroTag; + final ValueNotifier? cancellableNotifier; + final Object? heroTag; const RasterImageThumbnail({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.extent, this.cancellableNotifier, this.heroTag, }) : super(key: key); @@ -26,7 +26,7 @@ class RasterImageThumbnail extends StatefulWidget { } class _RasterImageThumbnailState extends State { - ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider; + ThumbnailProvider? _fastThumbnailProvider, _sizedThumbnailProvider; AvesEntry get entry => widget.entry; @@ -85,7 +85,7 @@ class _RasterImageThumbnailState extends State { final fastImage = Image( key: ValueKey('LQ'), - image: _fastThumbnailProvider, + image: _fastThumbnailProvider!, errorBuilder: _buildError, width: extent, height: extent, @@ -95,7 +95,7 @@ class _RasterImageThumbnailState extends State { ? fastImage : Image( key: ValueKey('HQ'), - image: _sizedThumbnailProvider, + image: _sizedThumbnailProvider!, frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { if (wasSynchronouslyLoaded) return child; return AnimatedSwitcher( @@ -123,7 +123,7 @@ class _RasterImageThumbnailState extends State { ); return widget.heroTag != null ? Hero( - tag: widget.heroTag, + tag: widget.heroTag!, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { return TransitionImage( image: entry.getBestThumbnail(extent), @@ -136,7 +136,7 @@ class _RasterImageThumbnailState extends State { : image; } - Widget _buildError(BuildContext context, Object error, StackTrace stackTrace) => ErrorThumbnail( + Widget _buildError(BuildContext context, Object error, StackTrace? stackTrace) => ErrorThumbnail( entry: entry, extent: extent, tooltip: error.toString(), diff --git a/lib/widgets/collection/thumbnail/theme.dart b/lib/widgets/collection/thumbnail/theme.dart index fa85901f4..66e774af8 100644 --- a/lib/widgets/collection/thumbnail/theme.dart +++ b/lib/widgets/collection/thumbnail/theme.dart @@ -6,13 +6,13 @@ import 'package:provider/provider.dart'; class ThumbnailTheme extends StatelessWidget { final double extent; - final bool showLocation; + final bool? showLocation; final Widget child; const ThumbnailTheme({ - @required this.extent, + required this.extent, this.showLocation, - @required this.child, + required this.child, }); @override @@ -39,10 +39,10 @@ class ThumbnailThemeData { final bool showLocation, showRaw, showVideoDuration; const ThumbnailThemeData({ - @required this.iconSize, - @required this.fontSize, - @required this.showLocation, - @required this.showRaw, - @required this.showVideoDuration, + required this.iconSize, + required this.fontSize, + required this.showLocation, + required this.showRaw, + required this.showVideoDuration, }); } diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index 099780779..2c71b3593 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -11,12 +11,12 @@ import 'package:provider/provider.dart'; class VectorImageThumbnail extends StatelessWidget { final AvesEntry entry; final double extent; - final Object heroTag; + final Object? heroTag; const VectorImageThumbnail({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.extent, this.heroTag, }) : super(key: key); @@ -31,7 +31,7 @@ class VectorImageThumbnail extends StatelessWidget { builder: (context, constraints) { final availableSize = constraints.biggest; final fitSize = applyBoxFit(fit, entry.displaySize, availableSize).destination; - final offset = fitSize / 2 - availableSize / 2; + final offset = (fitSize / 2 - availableSize / 2) as Offset; final child = CustomPaint( painter: CheckeredPainter(checkSize: extent / 8, offset: offset), child: SvgPicture( @@ -66,7 +66,7 @@ class VectorImageThumbnail extends StatelessWidget { ); return heroTag != null ? Hero( - tag: heroTag, + tag: heroTag!, transitionOnUserGestures: true, child: child, ) diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 4600de46c..044c2dbd7 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -21,12 +21,12 @@ mixin FeedbackMixin { // report overlay for multiple operations void showOpReport({ - @required BuildContext context, - @required Stream opStream, - @required int itemCount, - void Function(Set processed) onDone, + required BuildContext context, + required Stream opStream, + required int itemCount, + void Function(Set processed)? onDone, }) { - OverlayEntry _opReportOverlayEntry; + late OverlayEntry _opReportOverlayEntry; _opReportOverlayEntry = OverlayEntry( builder: (context) => ReportOverlay( opStream: opStream, @@ -37,7 +37,7 @@ mixin FeedbackMixin { }, ), ); - Overlay.of(context).insert(_opReportOverlayEntry); + Overlay.of(context)!.insert(_opReportOverlayEntry); } } @@ -47,9 +47,9 @@ class ReportOverlay extends StatefulWidget { final void Function(Set processed) onDone; const ReportOverlay({ - @required this.opStream, - @required this.itemCount, - @required this.onDone, + required this.opStream, + required this.itemCount, + required this.onDone, }); @override @@ -58,8 +58,8 @@ class ReportOverlay extends StatefulWidget { class _ReportOverlayState extends State> with SingleTickerProviderStateMixin { final processed = {}; - AnimationController _animationController; - Animation _animation; + late AnimationController _animationController; + late Animation _animation; Stream get opStream => widget.opStream; diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index 7c2d91955..335963240 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -3,21 +3,21 @@ import 'package:aves/services/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; mixin PermissionAwareMixin { Future checkStoragePermission(BuildContext context, Set entries) { - return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).toSet()); + return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).cast().toSet()); } Future checkStoragePermissionForAlbums(BuildContext context, Set albumPaths) async { final restrictedDirs = await storageService.getRestrictedDirectories(); while (true) { final dirs = await storageService.getInaccessibleDirectories(albumPaths); - if (dirs == null) return false; if (dirs.isEmpty) return true; - final restrictedInaccessibleDir = dirs.firstWhere(restrictedDirs.contains, orElse: () => null); + final restrictedInaccessibleDir = dirs.firstWhereOrNull(restrictedDirs.contains); if (restrictedInaccessibleDir != null) { await showRestrictedDirectoryDialog(context, restrictedInaccessibleDir); return false; @@ -57,7 +57,7 @@ mixin PermissionAwareMixin { } } - Future showRestrictedDirectoryDialog(BuildContext context, VolumeRelativeDirectory dir) { + Future showRestrictedDirectoryDialog(BuildContext context, VolumeRelativeDirectory dir) { return showDialog( context: context, builder: (context) { diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index 95b0e338a..d9a4891f8 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -15,14 +15,19 @@ import 'package:flutter/widgets.dart'; mixin SizeAwareMixin { Future checkFreeSpaceForMove( BuildContext context, - Set selection, + Set selection, String destinationAlbum, MoveType moveType, ) async { + // assume we have enough space if we cannot find the volume or its remaining free space final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); + if (destinationVolume == null) return true; + final free = await storageService.getFreeSpace(destinationVolume); - int needed; - int sumSize(sum, entry) => sum + entry.sizeBytes; + if (free == null) return true; + + late int needed; + int sumSize(sum, entry) => sum + entry.sizeBytes ?? 0; switch (moveType) { case MoveType.copy: case MoveType.export: @@ -30,11 +35,11 @@ mixin SizeAwareMixin { break; case MoveType.move: // when moving, we only need space for the entries that are not already on the destination volume - final byVolume = groupBy(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)); + final byVolume = Map.fromEntries(groupBy(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)).entries.where((kv) => kv.key != null).cast>>()); final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); - final fromOtherVolumes = otherVolumes.fold(0, (sum, volume) => sum + byVolume[volume].fold(0, sumSize)); + final fromOtherVolumes = otherVolumes.fold(0, (sum, volume) => sum + byVolume[volume]!.fold(0, sumSize)); // and we need at least as much space as the largest entry because individual entries are copied then deleted - final largestSingle = selection.fold(0, (largest, entry) => max(largest, entry.sizeBytes)); + final largestSingle = selection.fold(0, (largest, entry) => max(largest, entry.sizeBytes ?? 0)); needed = max(fromOtherVolumes, largestSingle); break; } diff --git a/lib/widgets/common/app_bar_subtitle.dart b/lib/widgets/common/app_bar_subtitle.dart index 575a92d95..234a3f455 100644 --- a/lib/widgets/common/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar_subtitle.dart @@ -9,9 +9,9 @@ class SourceStateAwareAppBarTitle extends StatelessWidget { final CollectionSource source; const SourceStateAwareAppBarTitle({ - Key key, - @required this.title, - @required this.source, + Key? key, + required this.title, + required this.source, }) : super(key: key); @override @@ -49,11 +49,11 @@ class SourceStateAwareAppBarTitle extends StatelessWidget { class SourceStateSubtitle extends StatelessWidget { final CollectionSource source; - const SourceStateSubtitle({@required this.source}); + const SourceStateSubtitle({required this.source}); @override Widget build(BuildContext context) { - String subtitle; + String? subtitle; switch (source.stateNotifier.value) { case SourceState.loading: subtitle = context.l10n.sourceStateLoading; @@ -79,12 +79,12 @@ class SourceStateSubtitle extends StatelessWidget { stream: source.progressStream, builder: (context, snapshot) { if (snapshot.hasError || !snapshot.hasData) return SizedBox.shrink(); - final progress = snapshot.data; + final progress = snapshot.data!; return Padding( padding: EdgeInsetsDirectional.only(start: 8), child: Text( '${progress.done}/${progress.total}', - style: subtitleStyle.copyWith(color: Colors.white30), + style: subtitleStyle!.copyWith(color: Colors.white30), ), ); }, diff --git a/lib/widgets/common/app_bar_title.dart b/lib/widgets/common/app_bar_title.dart index ed285d341..e84b84530 100644 --- a/lib/widgets/common/app_bar_title.dart +++ b/lib/widgets/common/app_bar_title.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; class InteractiveAppBarTitle extends StatelessWidget { - final GestureTapCallback onTap; + final GestureTapCallback? onTap; final Widget child; const InteractiveAppBarTitle({ this.onTap, - @required this.child, + required this.child, }); @override diff --git a/lib/widgets/common/aves_highlight.dart b/lib/widgets/common/aves_highlight.dart index 13df2ac11..876604465 100644 --- a/lib/widgets/common/aves_highlight.dart +++ b/lib/widgets/common/aves_highlight.dart @@ -14,7 +14,7 @@ class AvesHighlightView extends StatelessWidget { /// It is recommended to give it a value for performance /// /// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages) - final String language; + final String? language; /// Highlight theme /// @@ -22,12 +22,12 @@ class AvesHighlightView extends StatelessWidget { final Map theme; /// Padding - final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? padding; /// Text styles /// /// Specify text styles such as font family and font size - final TextStyle textStyle; + final TextStyle? textStyle; AvesHighlightView( String input, { @@ -45,16 +45,16 @@ class AvesHighlightView extends StatelessWidget { void _traverse(Node node) { if (node.value != null) { - currentSpans.add(node.className == null ? TextSpan(text: node.value) : TextSpan(text: node.value, style: theme[node.className])); + currentSpans.add(node.className == null ? TextSpan(text: node.value) : TextSpan(text: node.value, style: theme[node.className!])); } else if (node.children != null) { final tmp = []; - currentSpans.add(TextSpan(children: tmp, style: theme[node.className])); + currentSpans.add(TextSpan(children: tmp, style: theme[node.className!])); stack.add(currentSpans); currentSpans = tmp; - node.children.forEach((n) { + node.children!.forEach((n) { _traverse(n); - if (n == node.children.last) { + if (n == node.children!.last) { currentSpans = stack.isEmpty ? spans : stack.removeLast(); } }); @@ -93,7 +93,7 @@ class AvesHighlightView extends StatelessWidget { child: SelectableText.rich( TextSpan( style: _textStyle, - children: _convert(highlight.parse(source, language: language).nodes), + children: _convert(highlight.parse(source, language: language).nodes!), ), ), ); diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart index e6666d51f..86f60bd42 100644 --- a/lib/widgets/common/basic/draggable_scrollbar.dart +++ b/lib/widgets/common/basic/draggable_scrollbar.dart @@ -18,7 +18,7 @@ typedef ScrollThumbBuilder = Widget Function( Animation thumbAnimation, Animation labelAnimation, double height, { - Widget labelText, + Widget? labelText, }); /// Build a Text widget using the current scroll offset @@ -37,7 +37,7 @@ class DraggableScrollbar extends StatefulWidget { final ScrollThumbBuilder scrollThumbBuilder; /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry padding; + final EdgeInsets? padding; /// Determines how quickly the scrollbar will animate in and out final Duration scrollbarAnimationDuration; @@ -46,7 +46,7 @@ class DraggableScrollbar extends StatefulWidget { final Duration scrollbarTimeToFade; /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder labelTextBuilder; + final LabelTextBuilder? labelTextBuilder; /// The ScrollController for the BoxScrollView final ScrollController controller; @@ -55,30 +55,28 @@ class DraggableScrollbar extends StatefulWidget { final ScrollView child; DraggableScrollbar({ - Key key, - @required this.backgroundColor, - @required this.scrollThumbHeight, - @required this.scrollThumbBuilder, - @required this.controller, + Key? key, + required this.backgroundColor, + required this.scrollThumbHeight, + required this.scrollThumbBuilder, + required this.controller, this.padding, this.scrollbarAnimationDuration = const Duration(milliseconds: 300), this.scrollbarTimeToFade = const Duration(milliseconds: 1000), this.labelTextBuilder, - @required this.child, - }) : assert(controller != null), - assert(scrollThumbBuilder != null), - assert(child.scrollDirection == Axis.vertical), + required this.child, + }) : assert(child.scrollDirection == Axis.vertical), super(key: key); @override _DraggableScrollbarState createState() => _DraggableScrollbarState(); static Widget buildScrollThumbAndLabel({ - @required Widget scrollThumb, - @required Color backgroundColor, - @required Animation thumbAnimation, - @required Animation labelAnimation, - @required Widget labelText, + required Widget scrollThumb, + required Color backgroundColor, + required Animation thumbAnimation, + required Animation labelAnimation, + required Widget? labelText, }) { final scrollThumbAndLabel = labelText == null ? scrollThumb @@ -108,10 +106,10 @@ class ScrollLabel extends StatelessWidget { final Widget child; const ScrollLabel({ - Key key, - @required this.child, - @required this.animation, - @required this.backgroundColor, + Key? key, + required this.child, + required this.animation, + required this.backgroundColor, }) : super(key: key); @override @@ -134,13 +132,13 @@ class ScrollLabel extends StatelessWidget { class _DraggableScrollbarState extends State with TickerProviderStateMixin { final ValueNotifier _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0); bool _isDragInProcess = false; - Offset _longPressLastGlobalPosition; + late Offset _longPressLastGlobalPosition; - AnimationController _thumbAnimationController; - Animation _thumbAnimation; - AnimationController _labelAnimationController; - Animation _labelAnimation; - Timer _fadeoutTimer; + late AnimationController _thumbAnimationController; + late Animation _thumbAnimation; + late AnimationController _labelAnimationController; + late Animation _labelAnimation; + Timer? _fadeoutTimer; @override void initState() { @@ -177,7 +175,7 @@ class _DraggableScrollbarState extends State with TickerProv ScrollController get controller => widget.controller; - double get thumbMaxScrollExtent => context.size.height - widget.scrollThumbHeight - (widget.padding?.vertical ?? 0.0); + double get thumbMaxScrollExtent => context.size!.height - widget.scrollThumbHeight - (widget.padding?.vertical ?? 0.0); double get thumbMinScrollExtent => 0.0; @@ -208,20 +206,20 @@ class _DraggableScrollbarState extends State with TickerProv onVerticalDragStart: (_) => _onVerticalDragStart(), onVerticalDragUpdate: (details) => _onVerticalDragUpdate(details.delta.dy), onVerticalDragEnd: (_) => _onVerticalDragEnd(), - child: ValueListenableBuilder( + child: ValueListenableBuilder( valueListenable: _thumbOffsetNotifier, builder: (context, thumbOffset, child) => Container( alignment: AlignmentDirectional.topEnd, - padding: EdgeInsets.only(top: thumbOffset) + widget.padding, + padding: EdgeInsets.only(top: thumbOffset) + (widget.padding ?? EdgeInsets.zero), child: widget.scrollThumbBuilder( widget.backgroundColor, _thumbAnimation, _labelAnimation, widget.scrollThumbHeight, labelText: (widget.labelTextBuilder != null && _isDragInProcess) - ? ValueListenableBuilder( + ? ValueListenableBuilder( valueListenable: _viewOffsetNotifier, - builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset), + builder: (context, viewOffset, child) => widget.labelTextBuilder!.call(viewOffset + thumbOffset), ) : null, ), @@ -376,16 +374,16 @@ class SlideFadeTransition extends StatelessWidget { final Widget child; const SlideFadeTransition({ - Key key, - @required this.animation, - @required this.child, + Key? key, + required this.animation, + required this.child, }) : super(key: key); @override Widget build(BuildContext context) { return AnimatedBuilder( animation: animation, - builder: (context, child) => animation.value == 0.0 ? Container() : child, + builder: (context, child) => animation.value == 0.0 ? Container() : child!, child: SlideTransition( position: Tween( begin: Offset(0.3, 0.0), diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index 9bf5b4c35..381101d42 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -27,7 +27,7 @@ class BottomGestureAreaProtector extends StatelessWidget { class GestureAreaProtectorStack extends StatelessWidget { final Widget child; - const GestureAreaProtectorStack({@required this.child}); + const GestureAreaProtectorStack({required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/basic/labeled_checkbox.dart b/lib/widgets/common/basic/labeled_checkbox.dart index c8b2d4e87..fa8205602 100644 --- a/lib/widgets/common/basic/labeled_checkbox.dart +++ b/lib/widgets/common/basic/labeled_checkbox.dart @@ -3,14 +3,14 @@ import 'package:flutter/material.dart'; class LabeledCheckbox extends StatefulWidget { final bool value; - final ValueChanged onChanged; + final ValueChanged onChanged; final String text; const LabeledCheckbox({ - Key key, - @required this.value, - @required this.onChanged, - @required this.text, + Key? key, + required this.value, + required this.onChanged, + required this.text, }) : super(key: key); @override @@ -18,7 +18,7 @@ class LabeledCheckbox extends StatefulWidget { } class _LabeledCheckboxState extends State { - TapGestureRecognizer _tapRecognizer; + late TapGestureRecognizer _tapRecognizer; @override void initState() { diff --git a/lib/widgets/common/basic/link_chip.dart b/lib/widgets/common/basic/link_chip.dart index d64258966..833bddf94 100644 --- a/lib/widgets/common/basic/link_chip.dart +++ b/lib/widgets/common/basic/link_chip.dart @@ -3,19 +3,19 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class LinkChip extends StatelessWidget { - final Widget leading; + final Widget? leading; final String text; final String url; - final Color color; - final TextStyle textStyle; + final Color? color; + final TextStyle? textStyle; static final borderRadius = BorderRadius.circular(8); const LinkChip({ - Key key, + Key? key, this.leading, - @required this.text, - @required this.url, + required this.text, + required this.url, this.color, this.textStyle, }) : super(key: key); @@ -37,7 +37,7 @@ class LinkChip extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (leading != null) ...[ - leading, + leading!, SizedBox(width: 8), ], Flexible( diff --git a/lib/widgets/common/basic/menu_row.dart b/lib/widgets/common/basic/menu_row.dart index bcc3acf06..9279f273c 100644 --- a/lib/widgets/common/basic/menu_row.dart +++ b/lib/widgets/common/basic/menu_row.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart'; class MenuRow extends StatelessWidget { final String text; - final IconData icon; - final bool checked; + final IconData? icon; + final bool? checked; const MenuRow({ - Key key, - this.text, + Key? key, + required this.text, this.icon, this.checked, }) : super(key: key); @@ -16,12 +16,12 @@ class MenuRow extends StatelessWidget { @override Widget build(BuildContext context) { final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final iconSize = IconTheme.of(context).size * textScaleFactor; + final iconSize = IconTheme.of(context).size! * textScaleFactor; return Row( children: [ if (checked != null) ...[ Opacity( - opacity: checked ? 1 : 0, + opacity: checked! ? 1 : 0, child: Icon(AIcons.checked, size: iconSize), ), SizedBox(width: 8), diff --git a/lib/widgets/common/basic/multi_cross_fader.dart b/lib/widgets/common/basic/multi_cross_fader.dart index 22d75f822..227740b95 100644 --- a/lib/widgets/common/basic/multi_cross_fader.dart +++ b/lib/widgets/common/basic/multi_cross_fader.dart @@ -7,11 +7,11 @@ class MultiCrossFader extends StatefulWidget { final Widget child; const MultiCrossFader({ - @required this.duration, + required this.duration, this.fadeCurve = Curves.linear, this.sizeCurve = Curves.linear, this.alignment = Alignment.topCenter, - @required this.child, + required this.child, }); @override @@ -19,7 +19,7 @@ class MultiCrossFader extends StatefulWidget { } class _MultiCrossFaderState extends State { - Widget _first, _second; + late Widget _first, _second; CrossFadeState _fadeState = CrossFadeState.showFirst; @override diff --git a/lib/widgets/common/basic/outlined_text.dart b/lib/widgets/common/basic/outlined_text.dart index 088d2a229..6e9a36701 100644 --- a/lib/widgets/common/basic/outlined_text.dart +++ b/lib/widgets/common/basic/outlined_text.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; typedef OutlinedWidgetBuilder = Widget Function(BuildContext context, bool isShadow); class OutlinedText extends StatelessWidget { - final OutlinedWidgetBuilder leadingBuilder, trailingBuilder; + final OutlinedWidgetBuilder? leadingBuilder, trailingBuilder; final String text; final TextStyle style; final double outlineWidth; @@ -12,13 +12,13 @@ class OutlinedText extends StatelessWidget { static const widgetSpanAlignment = PlaceholderAlignment.middle; const OutlinedText({ - Key key, + Key? key, this.leadingBuilder, - @required this.text, + required this.text, this.trailingBuilder, - @required this.style, - double outlineWidth, - Color outlineColor, + required this.style, + double? outlineWidth, + Color? outlineColor, }) : outlineWidth = outlineWidth ?? 1, outlineColor = outlineColor ?? Colors.black, super(key: key); @@ -33,7 +33,7 @@ class OutlinedText extends StatelessWidget { if (leadingBuilder != null) WidgetSpan( alignment: widgetSpanAlignment, - child: leadingBuilder(context, true), + child: leadingBuilder!(context, true), ), TextSpan( text: text, @@ -47,7 +47,7 @@ class OutlinedText extends StatelessWidget { if (trailingBuilder != null) WidgetSpan( alignment: widgetSpanAlignment, - child: trailingBuilder(context, true), + child: trailingBuilder!(context, true), ), ], ), @@ -58,7 +58,7 @@ class OutlinedText extends StatelessWidget { if (leadingBuilder != null) WidgetSpan( alignment: widgetSpanAlignment, - child: leadingBuilder(context, false), + child: leadingBuilder!(context, false), ), TextSpan( text: text, @@ -67,7 +67,7 @@ class OutlinedText extends StatelessWidget { if (trailingBuilder != null) WidgetSpan( alignment: widgetSpanAlignment, - child: trailingBuilder(context, false), + child: trailingBuilder!(context, false), ), ], ), diff --git a/lib/widgets/common/basic/query_bar.dart b/lib/widgets/common/basic/query_bar.dart index aa3834857..8d7f5a811 100644 --- a/lib/widgets/common/basic/query_bar.dart +++ b/lib/widgets/common/basic/query_bar.dart @@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart'; class QueryBar extends StatefulWidget { final ValueNotifier filterNotifier; - const QueryBar({@required this.filterNotifier}); + const QueryBar({required this.filterNotifier}); @override _QueryBarState createState() => _QueryBarState(); @@ -17,7 +17,7 @@ class QueryBar extends StatefulWidget { class _QueryBarState extends State { final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay); - TextEditingController _controller; + late TextEditingController _controller; ValueNotifier get filterNotifier => widget.filterNotifier; diff --git a/lib/widgets/common/basic/reselectable_radio_list_tile.dart b/lib/widgets/common/basic/reselectable_radio_list_tile.dart index f90a4a159..c6c546b37 100644 --- a/lib/widgets/common/basic/reselectable_radio_list_tile.dart +++ b/lib/widgets/common/basic/reselectable_radio_list_tile.dart @@ -4,15 +4,15 @@ import 'package:flutter/material.dart'; class ReselectableRadioListTile extends StatelessWidget { final T value; final T groupValue; - final ValueChanged onChanged; + final ValueChanged? onChanged; final bool toggleable; final bool reselectable; - final Color activeColor; - final Widget title; - final Widget subtitle; - final Widget secondary; + final Color? activeColor; + final Widget? title; + final Widget? subtitle; + final Widget? secondary; final bool isThreeLine; - final bool dense; + final bool? dense; final bool selected; final ListTileControlAffinity controlAffinity; final bool autofocus; @@ -20,10 +20,10 @@ class ReselectableRadioListTile extends StatelessWidget { bool get checked => value == groupValue; const ReselectableRadioListTile({ - Key key, - @required this.value, - @required this.groupValue, - @required this.onChanged, + Key? key, + required this.value, + required this.groupValue, + required this.onChanged, this.toggleable = false, this.reselectable = false, this.activeColor, @@ -35,12 +35,7 @@ class ReselectableRadioListTile extends StatelessWidget { this.selected = false, this.controlAffinity = ListTileControlAffinity.platform, this.autofocus = false, - }) : assert(toggleable != null), - assert(isThreeLine != null), - assert(!isThreeLine || subtitle != null), - assert(selected != null), - assert(controlAffinity != null), - assert(autofocus != null), + }) : assert(!isThreeLine || subtitle != null), super(key: key); @override @@ -54,7 +49,7 @@ class ReselectableRadioListTile extends StatelessWidget { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, autofocus: autofocus, ); - Widget leading, trailing; + Widget? leading, trailing; switch (controlAffinity) { case ListTileControlAffinity.leading: case ListTileControlAffinity.platform: @@ -80,11 +75,11 @@ class ReselectableRadioListTile extends StatelessWidget { onTap: onChanged != null ? () { if (toggleable && checked) { - onChanged(null); + onChanged!(null); return; } if (reselectable || !checked) { - onChanged(value); + onChanged!(value); } } : null, diff --git a/lib/widgets/common/behaviour/double_back_pop.dart b/lib/widgets/common/behaviour/double_back_pop.dart index e3aa98d5c..8118bea01 100644 --- a/lib/widgets/common/behaviour/double_back_pop.dart +++ b/lib/widgets/common/behaviour/double_back_pop.dart @@ -12,7 +12,7 @@ class DoubleBackPopScope extends StatefulWidget { final Widget child; const DoubleBackPopScope({ - @required this.child, + required this.child, }); @override @@ -21,7 +21,7 @@ class DoubleBackPopScope extends StatefulWidget { class _DoubleBackPopScopeState extends State with FeedbackMixin { bool _backOnce = false; - Timer _backTimer; + Timer? _backTimer; @override void dispose() { diff --git a/lib/widgets/common/behaviour/route_tracker.dart b/lib/widgets/common/behaviour/route_tracker.dart index 3be10ae63..c0d577919 100644 --- a/lib/widgets/common/behaviour/route_tracker.dart +++ b/lib/widgets/common/behaviour/route_tracker.dart @@ -3,16 +3,16 @@ import 'package:flutter/material.dart'; class CrashlyticsRouteTracker extends NavigatorObserver { @override - void didPush(Route route, Route previousRoute) => FirebaseCrashlytics.instance.log('Nav didPush to ${_name(route)}'); + void didPush(Route route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPush to ${_name(route)}'); @override - void didPop(Route route, Route previousRoute) => FirebaseCrashlytics.instance.log('Nav didPop to ${_name(previousRoute)}'); + void didPop(Route route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPop to ${_name(previousRoute)}'); @override - void didRemove(Route route, Route previousRoute) => FirebaseCrashlytics.instance.log('Nav didRemove to ${_name(previousRoute)}'); + void didRemove(Route route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didRemove to ${_name(previousRoute)}'); @override - void didReplace({Route newRoute, Route oldRoute}) => FirebaseCrashlytics.instance.log('Nav didReplace to ${_name(newRoute)}'); + void didReplace({Route? newRoute, Route? oldRoute}) => FirebaseCrashlytics.instance.log('Nav didReplace to ${_name(newRoute)}'); - String _name(Route route) => route?.settings?.name ?? 'unnamed ${route?.runtimeType}'; + String _name(Route? route) => route?.settings.name ?? 'unnamed ${route?.runtimeType}'; } diff --git a/lib/widgets/common/behaviour/routes.dart b/lib/widgets/common/behaviour/routes.dart index a5ac9d079..0051022bf 100644 --- a/lib/widgets/common/behaviour/routes.dart +++ b/lib/widgets/common/behaviour/routes.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; class DirectMaterialPageRoute extends PageRouteBuilder { DirectMaterialPageRoute({ - RouteSettings settings, - @required WidgetBuilder builder, + RouteSettings? settings, + required WidgetBuilder builder, }) : super( settings: settings, transitionDuration: Duration.zero, @@ -18,8 +18,8 @@ class DirectMaterialPageRoute extends PageRouteBuilder { class TransparentMaterialPageRoute extends PageRouteBuilder { TransparentMaterialPageRoute({ - RouteSettings settings, - @required RoutePageBuilder pageBuilder, + RouteSettings? settings, + required RoutePageBuilder pageBuilder, }) : super(settings: settings, pageBuilder: pageBuilder); @override diff --git a/lib/widgets/common/behaviour/sloppy_scroll_physics.dart b/lib/widgets/common/behaviour/sloppy_scroll_physics.dart index 1fbb8f158..072c57d4d 100644 --- a/lib/widgets/common/behaviour/sloppy_scroll_physics.dart +++ b/lib/widgets/common/behaviour/sloppy_scroll_physics.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; class SloppyScrollPhysics extends ScrollPhysics { const SloppyScrollPhysics({ this.touchSlopFactor = 1, - ScrollPhysics parent, + ScrollPhysics? parent, }) : super(parent: parent); // in [0, 1] @@ -13,7 +13,7 @@ class SloppyScrollPhysics extends ScrollPhysics { final double touchSlopFactor; @override - SloppyScrollPhysics applyTo(ScrollPhysics ancestor) { + SloppyScrollPhysics applyTo(ScrollPhysics? ancestor) { return SloppyScrollPhysics( touchSlopFactor: touchSlopFactor, parent: buildParent(ancestor), diff --git a/lib/widgets/common/extensions/build_context.dart b/lib/widgets/common/extensions/build_context.dart index bd79ec90f..02a4bfb5b 100644 --- a/lib/widgets/common/extensions/build_context.dart +++ b/lib/widgets/common/extensions/build_context.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; extension ExtraContext on BuildContext { - String get currentRouteName => ModalRoute.of(this)?.settings?.name; + String? get currentRouteName => ModalRoute.of(this)?.settings.name; - AppLocalizations get l10n => AppLocalizations.of(this); + AppLocalizations get l10n => AppLocalizations.of(this)!; } diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart index d14d3e214..de0c11bf4 100644 --- a/lib/widgets/common/fx/blurred.dart +++ b/lib/widgets/common/fx/blurred.dart @@ -9,9 +9,9 @@ class BlurredRect extends StatelessWidget { final Widget child; const BlurredRect({ - Key key, + Key? key, this.enabled = true, - this.child, + required this.child, }) : super(key: key); @override @@ -31,7 +31,11 @@ class BlurredRRect extends StatelessWidget { final double borderRadius; final Widget child; - const BlurredRRect({Key key, this.borderRadius, this.child}) : super(key: key); + const BlurredRRect({ + Key? key, + required this.borderRadius, + required this.child, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -48,7 +52,10 @@ class BlurredRRect extends StatelessWidget { class BlurredOval extends StatelessWidget { final Widget child; - const BlurredOval({Key key, this.child}) : super(key: key); + const BlurredOval({ + Key? key, + required this.child, + }) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/fx/highlight_decoration.dart b/lib/widgets/common/fx/highlight_decoration.dart index 096d139cd..669fdd94d 100644 --- a/lib/widgets/common/fx/highlight_decoration.dart +++ b/lib/widgets/common/fx/highlight_decoration.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; class HighlightDecoration extends Decoration { final Color color; - const HighlightDecoration({@required this.color}); + const HighlightDecoration({required this.color}); @override - _HighlightDecorationPainter createBoxPainter([VoidCallback onChanged]) { + _HighlightDecorationPainter createBoxPainter([VoidCallback? onChanged]) { return _HighlightDecorationPainter(this, onChanged); } } @@ -14,11 +14,13 @@ class HighlightDecoration extends Decoration { class _HighlightDecorationPainter extends BoxPainter { final HighlightDecoration decoration; - const _HighlightDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged); + const _HighlightDecorationPainter(this.decoration, VoidCallback? onChanged) : super(onChanged); @override void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { final size = configuration.size; + if (size == null) return; + final confHeight = size.height; final paintHeight = confHeight * .4; final rect = Rect.fromLTWH(offset.dx, offset.dy + confHeight - paintHeight, size.width, paintHeight); diff --git a/lib/widgets/common/fx/sweeper.dart b/lib/widgets/common/fx/sweeper.dart index 01221faf5..a724f0ddb 100644 --- a/lib/widgets/common/fx/sweeper.dart +++ b/lib/widgets/common/fx/sweeper.dart @@ -12,15 +12,15 @@ class Sweeper extends StatefulWidget { final Curve curve; final ValueNotifier toggledNotifier; final bool centerSweep; - final VoidCallback onSweepEnd; + final VoidCallback? onSweepEnd; const Sweeper({ - Key key, - @required this.builder, + Key? key, + required this.builder, this.startAngle = -pi / 2, this.sweepAngle = pi / 4, this.curve = Curves.easeInOutCubic, - @required this.toggledNotifier, + required this.toggledNotifier, this.centerSweep = true, this.onSweepEnd, }) : super(key: key); @@ -30,8 +30,8 @@ class Sweeper extends StatefulWidget { } class _SweeperState extends State with SingleTickerProviderStateMixin { - AnimationController _angleAnimationController; - Animation _angle; + late AnimationController _angleAnimationController; + late Animation _angle; bool _isAppearing = false; bool get isToggled => widget.toggledNotifier.value; @@ -129,7 +129,7 @@ class _SweepClipPath extends CustomClipper { final double startAngle; final double sweepAngle; - const _SweepClipPath({@required this.startAngle, @required this.sweepAngle}); + const _SweepClipPath({required this.startAngle, required this.sweepAngle}); @override Path getClip(Size size) { diff --git a/lib/widgets/common/fx/transition_image.dart b/lib/widgets/common/fx/transition_image.dart index 2a62162b7..9c96574f1 100644 --- a/lib/widgets/common/fx/transition_image.dart +++ b/lib/widgets/common/fx/transition_image.dart @@ -10,13 +10,13 @@ import 'package:flutter/material.dart'; class TransitionImage extends StatefulWidget { final ImageProvider image; - final double width, height; + final double? width, height; final ValueListenable animation; final bool gaplessPlayback = false; const TransitionImage({ - @required this.image, - @required this.animation, + required this.image, + required this.animation, this.width, this.height, }); @@ -26,10 +26,10 @@ class TransitionImage extends StatefulWidget { } class _TransitionImageState extends State { - ImageStream _imageStream; - ImageInfo _imageInfo; + ImageStream? _imageStream; + ImageInfo? _imageInfo; bool _isListeningToStream = false; - int _frameNumber; + int? _frameNumber; @override void initState() { @@ -60,8 +60,8 @@ class _TransitionImageState extends State { void didUpdateWidget(covariant TransitionImage oldWidget) { super.didUpdateWidget(oldWidget); if (_isListeningToStream) { - _imageStream.removeListener(_getListener()); - _imageStream.addListener(_getListener()); + _imageStream!.removeListener(_getListener()); + _imageStream!.addListener(_getListener()); } if (widget.image != oldWidget.image) _resolveImage(); } @@ -76,9 +76,8 @@ class _TransitionImageState extends State { final provider = widget.image; final newStream = provider.resolve(createLocalImageConfiguration( context, - size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null, + size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null, )); - assert(newStream != null); _updateSourceStream(newStream); } @@ -92,7 +91,7 @@ class _TransitionImageState extends State { void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; - _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1; + _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1; }); } @@ -100,9 +99,9 @@ class _TransitionImageState extends State { // registration from the old stream to the new stream (if a listener was // registered). void _updateSourceStream(ImageStream newStream) { - if (_imageStream?.key == newStream?.key) return; + if (_imageStream?.key == newStream.key) return; - if (_isListeningToStream) _imageStream.removeListener(_getListener()); + if (_isListeningToStream) _imageStream!.removeListener(_getListener()); if (!widget.gaplessPlayback) { setState(() { @@ -115,18 +114,18 @@ class _TransitionImageState extends State { }); _imageStream = newStream; - if (_isListeningToStream) _imageStream.addListener(_getListener()); + if (_isListeningToStream) _imageStream!.addListener(_getListener()); } void _listenToStream() { if (_isListeningToStream) return; - _imageStream.addListener(_getListener()); + _imageStream!.addListener(_getListener()); _isListeningToStream = true; } void _stopListeningToStream() { if (!_isListeningToStream) return; - _imageStream.removeListener(_getListener()); + _imageStream!.removeListener(_getListener()); _isListeningToStream = false; } @@ -147,14 +146,14 @@ class _TransitionImageState extends State { } class _TransitionImagePainter extends CustomPainter { - final ui.Image image; + final ui.Image? image; final double scale; final double t; const _TransitionImagePainter({ - @required this.image, - @required this.scale, - @required this.t, + required this.image, + required this.scale, + required this.t, }); @override @@ -167,13 +166,13 @@ class _TransitionImagePainter extends CustomPainter { const alignment = Alignment.center; final rect = ui.Rect.fromLTWH(0, 0, size.width, size.height); - final inputSize = Size(image.width.toDouble(), image.height.toDouble()); + final inputSize = Size(image!.width.toDouble(), image!.height.toDouble()); final outputSize = rect.size; final coverSizes = applyBoxFit(BoxFit.cover, inputSize / scale, size); final containSizes = applyBoxFit(BoxFit.contain, inputSize / scale, size); - final sourceSize = Size.lerp(coverSizes.source, containSizes.source, t) * scale; - final destinationSize = Size.lerp(coverSizes.destination, containSizes.destination, t); + final sourceSize = Size.lerp(coverSizes.source, containSizes.source, t)! * scale; + final destinationSize = Size.lerp(coverSizes.destination, containSizes.destination, t)!; final halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0; final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0; @@ -185,7 +184,7 @@ class _TransitionImagePainter extends CustomPainter { sourceSize, Offset.zero & inputSize, ); - canvas.drawImageRect(image, sourceRect, destinationRect, paint); + canvas.drawImageRect(image!, sourceRect, destinationRect, paint); } @override diff --git a/lib/widgets/common/grid/draggable_thumb_label.dart b/lib/widgets/common/grid/draggable_thumb_label.dart index fa84ebb5a..f9a24d77d 100644 --- a/lib/widgets/common/grid/draggable_thumb_label.dart +++ b/lib/widgets/common/grid/draggable_thumb_label.dart @@ -6,11 +6,11 @@ import 'package:provider/provider.dart'; class DraggableThumbLabel extends StatelessWidget { final double offsetY; - final List Function(BuildContext context, T item) lineBuilder; + final List Function(BuildContext context, T item) lineBuilder; const DraggableThumbLabel({ - @required this.offsetY, - @required this.lineBuilder, + required this.offsetY, + required this.lineBuilder, }); @override @@ -19,11 +19,10 @@ class DraggableThumbLabel extends StatelessWidget { final sectionLayout = sll.getSectionAt(offsetY); if (sectionLayout == null) return SizedBox(); - final section = sll.sections[sectionLayout.sectionKey]; + final section = sll.sections[sectionLayout.sectionKey]!; final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent); final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileExtent + sll.spacing)) * sll.columnCount; final item = section[itemIndex]; - if (item == null) return SizedBox(); final lines = lineBuilder(context, item); if (lines.isEmpty) return SizedBox(); @@ -53,13 +52,13 @@ class DraggableThumbLabel extends StatelessWidget { maxLines: 1, ); - static String formatMonthThumbLabel(BuildContext context, DateTime date) { + static String formatMonthThumbLabel(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; return DateFormat.yMMM(l10n.localeName).format(date); } - static String formatDayThumbLabel(BuildContext context, DateTime date) { + static String formatDayThumbLabel(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; return DateFormat.yMMMd(l10n.localeName).format(date); diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index 2ca2500e4..de56dc950 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -11,15 +11,15 @@ import 'package:provider/provider.dart'; class SectionHeader extends StatelessWidget { final SectionKey sectionKey; - final Widget leading, trailing; + final Widget? leading, trailing; final String title; final bool selectable; const SectionHeader({ - Key key, - @required this.sectionKey, + Key? key, + required this.sectionKey, this.leading, - @required this.title, + required this.title, this.trailing, this.selectable = true, }) : super(key: key); @@ -78,7 +78,7 @@ class SectionHeader extends StatelessWidget { void _toggleSectionSelection(BuildContext context) { final collection = context.read(); - final sectionEntries = collection.sections[sectionKey]; + final sectionEntries = collection.sections[sectionKey]!; final selected = collection.isSelected(sectionEntries); if (selected) { collection.removeFromSelection(sectionEntries); @@ -89,9 +89,9 @@ class SectionHeader extends StatelessWidget { // TODO TLAD cache header extent computation? static double getPreferredHeight({ - @required BuildContext context, - @required double maxWidth, - @required String title, + required BuildContext context, + required double maxWidth, + required String title, bool hasLeading = false, bool hasTrailing = false, }) { @@ -124,15 +124,15 @@ class SectionHeader extends StatelessWidget { class _SectionSelectableLeading extends StatelessWidget { final bool selectable; final SectionKey sectionKey; - final WidgetBuilder browsingBuilder; - final VoidCallback onPressed; + final WidgetBuilder? browsingBuilder; + final VoidCallback? onPressed; const _SectionSelectableLeading({ - Key key, + Key? key, this.selectable = true, - @required this.sectionKey, - @required this.browsingBuilder, - @required this.onPressed, + required this.sectionKey, + required this.browsingBuilder, + required this.onPressed, }) : super(key: key); static const leadingDimension = SectionHeader.leadingDimension; @@ -149,7 +149,7 @@ class _SectionSelectableLeading extends StatelessWidget { ? AnimatedBuilder( animation: collection.selectionChangeNotifier, builder: (context, child) { - final sectionEntries = collection.sections[sectionKey]; + final sectionEntries = collection.sections[sectionKey]!; final selected = collection.isSelected(sectionEntries); final child = TooltipTheme( key: ValueKey(selected), diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart index 329ec9c58..668aec410 100644 --- a/lib/widgets/common/grid/section_layout.dart +++ b/lib/widgets/common/grid/section_layout.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/durations.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -16,13 +17,13 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { final Widget child; const SectionedListLayoutProvider({ - @required this.scrollableWidth, - @required this.columnCount, + required this.scrollableWidth, + required this.columnCount, this.spacing = 0, - @required this.tileExtent, - @required this.tileBuilder, - this.tileAnimationDelay, - @required this.child, + required this.tileExtent, + required this.tileBuilder, + required this.tileAnimationDelay, + required this.child, }) : assert(scrollableWidth != 0); @override @@ -45,9 +46,10 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { final animate = tileAnimationDelay > Duration.zero; final sectionLayouts = []; - var currentIndex = 0, currentOffset = 0.0; + var currentIndex = 0; + var currentOffset = 0.0; sectionKeys.forEach((sectionKey) { - final section = _sections[sectionKey]; + final section = _sections[sectionKey]!; final sectionItemCount = section.length; final rowCount = (sectionItemCount / columnCount).ceil(); final sectionChildCount = 1 + rowCount; @@ -131,7 +133,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { position: index, columnCount: columnCount, duration: Durations.staggeredAnimation, - delay: tileAnimationDelay ?? Durations.staggeredAnimationDelay, + delay: tileAnimationDelay, child: SlideAnimation( verticalOffset: 50.0, child: FadeInAnimation( @@ -143,7 +145,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { bool get showHeaders; - Map> get sections; + Map > get sections; double getHeaderExtent(BuildContext context, SectionKey sectionKey); @@ -161,27 +163,27 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { } class SectionedListLayout { - final Map> sections; + final Map > sections; final bool showHeaders; final int columnCount; final double tileExtent, spacing; final List sectionLayouts; const SectionedListLayout({ - @required this.sections, - @required this.showHeaders, - @required this.columnCount, - @required this.tileExtent, - @required this.spacing, - @required this.sectionLayouts, + required this.sections, + required this.showHeaders, + required this.columnCount, + required this.tileExtent, + required this.spacing, + required this.sectionLayouts, }); - Rect getTileRect(T item) { - final section = sections.entries.firstWhere((kv) => kv.value.contains(item), orElse: () => null); + Rect? getTileRect(T item) { + final MapEntry>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item)); if (section == null) return null; final sectionKey = section.key; - final sectionLayout = sectionLayouts.firstWhere((sl) => sl.sectionKey == sectionKey, orElse: () => null); + final sectionLayout = sectionLayouts.firstWhereOrNull((sl) => sl.sectionKey == sectionKey); if (sectionLayout == null) return null; final sectionItemIndex = section.value.indexOf(item); @@ -194,9 +196,9 @@ class SectionedListLayout { return Rect.fromLTWH(left, top, tileExtent, tileExtent); } - SectionLayout getSectionAt(double offsetY) => sectionLayouts.firstWhere((sl) => offsetY < sl.maxOffset, orElse: () => null); + SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset); - T getItemAt(Offset position) { + T? getItemAt(Offset position) { var dy = position.dy; final sectionLayout = getSectionAt(dy); if (sectionLayout == null) return null; @@ -224,15 +226,15 @@ class SectionLayout { final IndexedWidgetBuilder builder; const SectionLayout({ - @required this.sectionKey, - @required this.firstIndex, - @required this.lastIndex, - @required this.minOffset, - @required this.maxOffset, - @required this.headerExtent, - @required this.tileExtent, - @required this.spacing, - @required this.builder, + required this.sectionKey, + required this.firstIndex, + required this.lastIndex, + required this.minOffset, + required this.maxOffset, + required this.headerExtent, + required this.tileExtent, + required this.spacing, + required this.builder, }) : bodyFirstIndex = firstIndex + 1, bodyMinOffset = minOffset + headerExtent, mainAxisStride = tileExtent + spacing; diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart index 1b1ce521f..9481f8279 100644 --- a/lib/widgets/common/grid/sliver.dart +++ b/lib/widgets/common/grid/sliver.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -11,6 +12,7 @@ import 'package:provider/provider.dart'; // With the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen // because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0. // cf https://github.com/flutter/flutter/issues/49027 +// adapted from `RenderSliverFixedExtentBoxAdaptor` class SectionedListSliver extends StatelessWidget { const SectionedListSliver(); @@ -23,7 +25,7 @@ class SectionedListSliver extends StatelessWidget { delegate: SliverChildBuilderDelegate( (context, index) { if (index >= childCount) return null; - final sectionLayout = sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null); + final sectionLayout = sectionLayouts.firstWhereOrNull((section) => section.hasChild(index)); return sectionLayout?.builder(context, index) ?? SizedBox.shrink(); }, childCount: childCount, @@ -37,9 +39,9 @@ class _SliverKnownExtentList extends SliverMultiBoxAdaptorWidget { final List sectionLayouts; const _SliverKnownExtentList({ - Key key, - @required SliverChildDelegate delegate, - @required this.sectionLayouts, + Key? key, + required SliverChildDelegate delegate, + required this.sectionLayouts, }) : super(key: key, delegate: delegate); @override @@ -60,19 +62,18 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { List get sectionLayouts => _sectionLayouts; set sectionLayouts(List value) { - assert(value != null); if (_sectionLayouts == value) return; _sectionLayouts = value; markNeedsLayout(); } _RenderSliverKnownExtentBoxAdaptor({ - @required RenderSliverBoxChildManager childManager, - @required List sectionLayouts, + required RenderSliverBoxChildManager childManager, + required List sectionLayouts, }) : _sectionLayouts = sectionLayouts, super(childManager: childManager); - SectionLayout sectionAtIndex(int index) => sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null); + SectionLayout? sectionAtIndex(int index) => sectionLayouts.firstWhereOrNull((section) => section.hasChild(index)); SectionLayout sectionAtOffset(double scrollOffset) => sectionLayouts.firstWhere((section) => section.hasChildAtOffset(scrollOffset), orElse: () => sectionLayouts.last); @@ -90,10 +91,10 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { double estimateMaxScrollOffset( SliverConstraints constraints, { - int firstIndex, - int lastIndex, - double leadingScrollOffset, - double trailingScrollOffset, + int? firstIndex, + int? lastIndex, + double? leadingScrollOffset, + double? trailingScrollOffset, }) { return childManager.estimateMaxScrollOffset( constraints, @@ -156,23 +157,11 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { if (firstChild == null) { if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(firstIndex))) { // There are either no children, or we are past the end of all our children. - // If it is the latter, we will need to find the first available child. double max; - if (childManager.childCount != null) { - max = computeMaxScrollOffset(constraints); - } else if (firstIndex <= 0) { + if (firstIndex <= 0) { max = 0.0; } else { - // We will have to find it manually. - var possibleFirstIndex = firstIndex - 1; - while (possibleFirstIndex > 0 && - !addInitialChild( - index: possibleFirstIndex, - layoutOffset: indexToLayoutOffset(possibleFirstIndex), - )) { - possibleFirstIndex -= 1; - } - max = sectionAtIndex(possibleFirstIndex).indexToLayoutOffset(possibleFirstIndex); + max = computeMaxScrollOffset(constraints); } geometry = SliverGeometry( scrollExtent: max, @@ -183,9 +172,9 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { } } - RenderBox trailingChildWithLayout; + RenderBox? trailingChildWithLayout; - for (var index = indexOf(firstChild) - 1; index >= firstIndex; --index) { + for (var index = indexOf(firstChild!) - 1; index >= firstIndex; --index) { final child = insertAndLayoutLeadingChild(childConstraints); if (child == null) { // Items before the previously first child are no longer present. @@ -202,15 +191,15 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { } if (trailingChildWithLayout == null) { - firstChild.layout(childConstraints); - final childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData; + firstChild!.layout(childConstraints); + final childParentData = firstChild!.parentData as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = indexToLayoutOffset(firstIndex); trailingChildWithLayout = firstChild; } var estimatedMaxScrollOffset = double.infinity; - for (var index = indexOf(trailingChildWithLayout) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { - var child = childAfter(trailingChildWithLayout); + for (var index = indexOf(trailingChildWithLayout!) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { + var child = childAfter(trailingChildWithLayout!); if (child == null || indexOf(child) != index) { child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout); if (child == null) { @@ -223,19 +212,18 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { child.layout(childConstraints); } trailingChildWithLayout = child; - assert(child != null); final childParentData = child.parentData as SliverMultiBoxAdaptorParentData; assert(childParentData.index == index); - childParentData.layoutOffset = indexToLayoutOffset(childParentData.index); + childParentData.layoutOffset = indexToLayoutOffset(childParentData.index!); } - final lastIndex = indexOf(lastChild); + final lastIndex = indexOf(lastChild!); final leadingScrollOffset = indexToLayoutOffset(firstIndex); final trailingScrollOffset = indexToLayoutOffset(lastIndex + 1); - assert(firstIndex == 0 || childScrollOffset(firstChild) - scrollOffset <= precisionErrorTolerance); + assert(firstIndex == 0 || childScrollOffset(firstChild!)! - scrollOffset <= precisionErrorTolerance); assert(debugAssertChildListIsNonEmptyAndContiguous()); - assert(indexOf(firstChild) == firstIndex); + assert(indexOf(firstChild!) == firstIndex); assert(targetLastIndex == null || lastIndex <= targetLastIndex); estimatedMaxScrollOffset = math.min( diff --git a/lib/widgets/common/identity/aves_expansion_tile.dart b/lib/widgets/common/identity/aves_expansion_tile.dart index c82ef9c2c..b27f810ab 100644 --- a/lib/widgets/common/identity/aves_expansion_tile.dart +++ b/lib/widgets/common/identity/aves_expansion_tile.dart @@ -4,27 +4,27 @@ import 'package:flutter/material.dart'; class AvesExpansionTile extends StatelessWidget { final String value; - final Widget leading; + final Widget? leading; final String title; - final Color color; - final ValueNotifier expandedNotifier; + final Color? color; + final ValueNotifier? expandedNotifier; final bool initiallyExpanded, showHighlight; final List children; const AvesExpansionTile({ - String value, + String? value, this.leading, - @required this.title, + required this.title, this.color, this.expandedNotifier, this.initiallyExpanded = false, this.showHighlight = true, - @required this.children, + required this.children, }) : value = value ?? title; @override Widget build(BuildContext context) { - final enabled = children?.isNotEmpty == true; + final enabled = children.isNotEmpty == true; Widget titleChild = HighlightTitle( title, color: color, @@ -34,7 +34,7 @@ class AvesExpansionTile extends StatelessWidget { if (leading != null) { titleChild = Row( children: [ - leading, + leading!, SizedBox(width: 8), Expanded(child: titleChild), ], diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 9fe245ac7..5eb06e363 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -22,13 +22,13 @@ class AvesFilterChip extends StatefulWidget { final CollectionFilter filter; final bool removable; final bool showGenericIcon; - final Widget background; - final Widget details; - final BorderRadius borderRadius; + final Widget? background; + final Widget? details; + final BorderRadius? borderRadius; final double padding; final HeroType heroType; - final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final FilterCallback? onTap; + final OffsetFilterCallback? onLongPress; static const Color defaultOutlineColor = Colors.white; static const double defaultRadius = 32; @@ -38,8 +38,8 @@ class AvesFilterChip extends StatefulWidget { static const double maxChipWidth = 160; const AvesFilterChip({ - Key key, - @required this.filter, + Key? key, + required this.filter, this.removable = false, this.showGenericIcon = true, this.background, @@ -49,8 +49,7 @@ class AvesFilterChip extends StatefulWidget { this.heroType = HeroType.onTap, this.onTap, this.onLongPress = showDefaultLongPressMenu, - }) : assert(filter != null), - super(key: key); + }) : super(key: key); static Future showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { if (context.read>().value == AppMode.main) { @@ -65,7 +64,7 @@ class AvesFilterChip extends StatefulWidget { // after the user is done with the popup menu FocusManager.instance.primaryFocus?.unfocus(); - final RenderBox overlay = Overlay.of(context).context.findRenderObject(); + final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox; final touchArea = Size(40, 40); final selectedAction = await showMenu( context: context, @@ -89,18 +88,18 @@ class AvesFilterChip extends StatefulWidget { } class _AvesFilterChipState extends State { - Future _colorFuture; - Color _outlineColor; - bool _tapped; - Offset _tapPosition; + late Future _colorFuture; + late Color _outlineColor; + late bool _tapped; + Offset? _tapPosition; CollectionFilter get filter => widget.filter; double get padding => widget.padding; - FilterCallback get onTap => widget.onTap; + FilterCallback? get onTap => widget.onTap; - OffsetFilterCallback get onLongPress => widget.onLongPress; + OffsetFilterCallback? get onLongPress => widget.onLongPress; @override void initState() { @@ -171,7 +170,7 @@ class _AvesFilterChipState extends State { mainAxisSize: MainAxisSize.min, children: [ content, - Flexible(child: widget.details), + Flexible(child: widget.details!), ], ); } @@ -186,7 +185,7 @@ class _AvesFilterChipState extends State { child: ColoredBox( color: Colors.black54, child: DefaultTextStyle( - style: Theme.of(context).textTheme.bodyText2.copyWith( + style: Theme.of(context).textTheme.bodyText2!.copyWith( shadows: [Constants.embossShadow], ), child: content, @@ -224,17 +223,17 @@ class _AvesFilterChipState extends State { onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null, onTap: onTap != null ? () { - WidgetsBinding.instance.addPostFrameCallback((_) => onTap(filter)); + WidgetsBinding.instance!.addPostFrameCallback((_) => onTap!(filter)); setState(() => _tapped = true); } : null, - onLongPress: onLongPress != null ? () => onLongPress(context, filter, _tapPosition) : null, + onLongPress: onLongPress != null ? () => onLongPress!(context, filter, _tapPosition!) : null, borderRadius: borderRadius, child: FutureBuilder( future: _colorFuture, builder: (context, snapshot) { if (snapshot.hasData) { - _outlineColor = snapshot.data; + _outlineColor = snapshot.data!; } return DecoratedBox( decoration: BoxDecoration( diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index bec2db615..f82fe2708 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -14,8 +14,8 @@ class VideoIcon extends StatelessWidget { final AvesEntry entry; const VideoIcon({ - Key key, - this.entry, + Key? key, + required this.entry, }) : super(key: key); @override @@ -42,7 +42,7 @@ class VideoIcon extends StatelessWidget { } class AnimatedImageIcon extends StatelessWidget { - const AnimatedImageIcon({Key key}) : super(key: key); + const AnimatedImageIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -55,7 +55,7 @@ class AnimatedImageIcon extends StatelessWidget { } class GeotiffIcon extends StatelessWidget { - const GeotiffIcon({Key key}) : super(key: key); + const GeotiffIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -67,7 +67,7 @@ class GeotiffIcon extends StatelessWidget { } class SphericalImageIcon extends StatelessWidget { - const SphericalImageIcon({Key key}) : super(key: key); + const SphericalImageIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -79,7 +79,7 @@ class SphericalImageIcon extends StatelessWidget { } class GpsIcon extends StatelessWidget { - const GpsIcon({Key key}) : super(key: key); + const GpsIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -91,7 +91,7 @@ class GpsIcon extends StatelessWidget { } class RawIcon extends StatelessWidget { - const RawIcon({Key key}) : super(key: key); + const RawIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -106,8 +106,8 @@ class MultiPageIcon extends StatelessWidget { final AvesEntry entry; const MultiPageIcon({ - Key key, - this.entry, + Key? key, + required this.entry, }) : super(key: key); @override @@ -123,13 +123,13 @@ class MultiPageIcon extends StatelessWidget { class OverlayIcon extends StatelessWidget { final IconData icon; final double size; - final String text; + final String? text; final double iconScale; const OverlayIcon({ - Key key, - @required this.icon, - @required this.size, + Key? key, + required this.icon, + required this.size, this.iconScale = 1, this.text, }) : super(key: key); @@ -164,7 +164,7 @@ class OverlayIcon extends StatelessWidget { children: [ iconBox, SizedBox(width: 2), - Text(text), + Text(text!), ], ), ); @@ -172,10 +172,10 @@ class OverlayIcon extends StatelessWidget { } class IconUtils { - static Widget getAlbumIcon({ - @required BuildContext context, - @required String album, - double size, + static Widget? getAlbumIcon({ + required BuildContext context, + required String albumPath, + double? size, bool embossed = false, }) { size ??= IconTheme.of(context).size; @@ -195,7 +195,7 @@ class IconUtils { icon, size: size, ); - switch (androidFileUtils.getAlbumType(album)) { + switch (androidFileUtils.getAlbumType(albumPath)) { case AlbumType.camera: return buildIcon(AIcons.cameraAlbum); case AlbumType.screenshots: @@ -206,8 +206,8 @@ class IconUtils { case AlbumType.app: return Image( image: AppIconImage( - packageName: androidFileUtils.getAlbumAppPackageName(album), - size: size, + packageName: androidFileUtils.getAlbumAppPackageName(albumPath)!, + size: size!, ), width: size, height: size, diff --git a/lib/widgets/common/identity/aves_logo.dart b/lib/widgets/common/identity/aves_logo.dart index a458013e9..89ded5caa 100644 --- a/lib/widgets/common/identity/aves_logo.dart +++ b/lib/widgets/common/identity/aves_logo.dart @@ -4,7 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart'; class AvesLogo extends StatelessWidget { final double size; - const AvesLogo({@required this.size}); + const AvesLogo({required this.size}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/identity/empty.dart b/lib/widgets/common/identity/empty.dart index 6bc02eea0..a93f18ede 100644 --- a/lib/widgets/common/identity/empty.dart +++ b/lib/widgets/common/identity/empty.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; class EmptyContent extends StatelessWidget { - final IconData icon; + final IconData? icon; final String text; final AlignmentGeometry alignment; const EmptyContent({ this.icon, - @required this.text, + required this.text, this.alignment = const FractionalOffset(.5, .35), }); diff --git a/lib/widgets/common/identity/highlight_title.dart b/lib/widgets/common/identity/highlight_title.dart index c36ff5ffc..0a21390c0 100644 --- a/lib/widgets/common/identity/highlight_title.dart +++ b/lib/widgets/common/identity/highlight_title.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; class HighlightTitle extends StatelessWidget { final String title; - final Color color; + final Color? color; final double fontSize; final bool enabled, selectable; final bool showHighlight; @@ -18,7 +18,7 @@ class HighlightTitle extends StatelessWidget { this.enabled = true, this.selectable = false, this.showHighlight = true, - }) : assert(title != null); + }); static const disabledColor = Colors.grey; diff --git a/lib/widgets/common/identity/scroll_thumb.dart b/lib/widgets/common/identity/scroll_thumb.dart index ed0ee3ec1..ef937c022 100644 --- a/lib/widgets/common/identity/scroll_thumb.dart +++ b/lib/widgets/common/identity/scroll_thumb.dart @@ -6,8 +6,8 @@ const double avesScrollThumbHeight = 48; // height and background color do not change // so we do not rely on the builder props ScrollThumbBuilder avesScrollThumbBuilder({ - @required double height, - @required Color backgroundColor, + required double height, + required Color backgroundColor, }) { final scrollThumb = Container( decoration: BoxDecoration( diff --git a/lib/widgets/common/magnifier/controller/controller.dart b/lib/widgets/common/magnifier/controller/controller.dart index d4c5b37f9..84483e43c 100644 --- a/lib/widgets/common/magnifier/controller/controller.dart +++ b/lib/widgets/common/magnifier/controller/controller.dart @@ -12,9 +12,9 @@ class MagnifierController { final StreamController _scaleBoundariesStreamController = StreamController.broadcast(); final StreamController _scaleStateChangeStreamController = StreamController.broadcast(); - MagnifierState _currentState, initial, previousState; - ScaleBoundaries _scaleBoundaries; - ScaleStateChange _currentScaleState, previousScaleState; + late MagnifierState _currentState, initial, previousState; + ScaleBoundaries? _scaleBoundaries; + late ScaleStateChange _currentScaleState, previousScaleState; MagnifierController({ Offset initialPosition = Offset.zero, @@ -25,10 +25,12 @@ class MagnifierController { source: ChangeSource.internal, ); previousState = initial; + _currentState = initial; _setState(initial); final _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: ChangeSource.internal); previousScaleState = _initialScaleState; + _currentScaleState = _initialScaleState; _setScaleState(_initialScaleState); } @@ -42,9 +44,9 @@ class MagnifierController { Offset get position => currentState.position; - double get scale => currentState.scale; + double? get scale => currentState.scale; - ScaleBoundaries get scaleBoundaries => _scaleBoundaries; + ScaleBoundaries get scaleBoundaries => _scaleBoundaries!; ScaleStateChange get scaleState => _currentScaleState; @@ -60,9 +62,9 @@ class MagnifierController { } void update({ - Offset position, - double scale, - @required ChangeSource source, + Offset? position, + double? scale, + required ChangeSource source, }) { position = position ?? this.position; scale = scale ?? this.scale; @@ -76,7 +78,7 @@ class MagnifierController { )); } - void setScaleState(ScaleState newValue, ChangeSource source, {Offset childFocalPoint}) { + void setScaleState(ScaleState newValue, ChangeSource source, {Offset? childFocalPoint}) { if (_currentScaleState.state == newValue) return; previousScaleState = _currentScaleState; @@ -109,18 +111,18 @@ class MagnifierController { _scaleStateChangeStreamController.sink.add(_currentScaleState); } - double getScaleForScaleState(ScaleState scaleState) { - double _clamp(double scale, ScaleBoundaries boundaries) => scale.clamp(boundaries.minScale, boundaries.maxScale); + double? getScaleForScaleState(ScaleState scaleState) { + double _clamp(double scale) => scale.clamp(scaleBoundaries.minScale, scaleBoundaries.maxScale); switch (scaleState) { case ScaleState.initial: case ScaleState.zoomedIn: case ScaleState.zoomedOut: - return _clamp(scaleBoundaries.initialScale, scaleBoundaries); + return _clamp(scaleBoundaries.initialScale); case ScaleState.covering: - return _clamp(ScaleLevel.scaleForCovering(scaleBoundaries.viewportSize, scaleBoundaries.childSize), scaleBoundaries); + return _clamp(ScaleLevel.scaleForCovering(scaleBoundaries.viewportSize, scaleBoundaries.childSize)); case ScaleState.originalSize: - return _clamp(1.0, scaleBoundaries); + return _clamp(1.0); default: return null; } diff --git a/lib/widgets/common/magnifier/controller/controller_delegate.dart b/lib/widgets/common/magnifier/controller/controller_delegate.dart index 4169dced5..a6a79b3d7 100644 --- a/lib/widgets/common/magnifier/controller/controller_delegate.dart +++ b/lib/widgets/common/magnifier/controller/controller_delegate.dart @@ -16,11 +16,15 @@ mixin MagnifierControllerDelegate on State { ScaleBoundaries get scaleBoundaries => controller.scaleBoundaries; + Size get childSize => scaleBoundaries.childSize; + + Size get viewportSize => scaleBoundaries.viewportSize; + ScaleStateCycle get scaleStateCycle => widget.scaleStateCycle; Alignment get basePosition => Alignment.center; - Function(double prevScale, double nextScale, Offset nextPosition) _animateScale; + Function(double? prevScale, double? nextScale, Offset nextPosition)? _animateScale; /// Mark if scale need recalculation, useful for scale boundaries changes. bool markNeedsScaleRecalc = true; @@ -47,15 +51,15 @@ mixin MagnifierControllerDelegate on State { if (nextScaleState == ScaleState.covering || nextScaleState == ScaleState.originalSize) { final childFocalPoint = scaleStateChange.childFocalPoint; if (childFocalPoint != null) { - nextPosition = scaleBoundaries.childToStatePosition(nextScale, childFocalPoint); + nextPosition = scaleBoundaries.childToStatePosition(nextScale!, childFocalPoint); } } final prevScale = controller.scale ?? controller.getScaleForScaleState(controller.previousScaleState.state); - _animateScale(prevScale, nextScale, nextPosition); + _animateScale!(prevScale, nextScale, nextPosition); } - void setScaleStateUpdateAnimation(void Function(double prevScale, double nextScale, Offset nextPosition) animateScale) { + void setScaleStateUpdateAnimation(void Function(double? prevScale, double? nextScale, Offset nextPosition) animateScale) { _animateScale = animateScale; } @@ -64,13 +68,13 @@ mixin MagnifierControllerDelegate on State { if (controller.scale == controller.previousState.scale) return; if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) return; - final newScaleState = (scale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; + final newScaleState = (scale! > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; controller.setScaleState(newScaleState, state.source); } Offset get position => controller.position; - double get scale { + double? get scale { final scaleState = controller.scaleState.state; final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut); final scaleExistsOnController = controller.scale != null; @@ -83,12 +87,12 @@ mixin MagnifierControllerDelegate on State { return controller.scale; } - void setScale(double scale, ChangeSource source) => controller.update(scale: scale, source: source); + void setScale(double? scale, ChangeSource source) => controller.update(scale: scale, source: source); void updateMultiple({ - @required Offset position, - @required double scale, - @required ChangeSource source, + required Offset position, + required double scale, + required ChangeSource source, }) { controller.update(position: position, scale: scale, source: source); } @@ -101,7 +105,7 @@ mixin MagnifierControllerDelegate on State { controller.setScaleState(newScaleState, source); } - void nextScaleState(ChangeSource source, {Offset childFocalPoint}) { + void nextScaleState(ChangeSource source, {Offset? childFocalPoint}) { final scaleState = controller.scaleState.state; if (scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut) { controller.setScaleState(scaleStateCycle(scaleState), source, childFocalPoint: childFocalPoint); @@ -125,11 +129,11 @@ mixin MagnifierControllerDelegate on State { controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint); } - CornersRange cornersX({double scale}) { - final _scale = scale ?? this.scale; + CornersRange cornersX({double? scale}) { + final _scale = scale ?? this.scale!; - final computedWidth = scaleBoundaries.childSize.width * _scale; - final screenWidth = scaleBoundaries.viewportSize.width; + final computedWidth = childSize.width * _scale; + final screenWidth = viewportSize.width; final positionX = basePosition.x; final widthDiff = computedWidth - screenWidth; @@ -139,11 +143,11 @@ mixin MagnifierControllerDelegate on State { return CornersRange(minX, maxX); } - CornersRange cornersY({double scale}) { - final _scale = scale ?? this.scale; + CornersRange cornersY({double? scale}) { + final _scale = scale ?? this.scale!; - final computedHeight = scaleBoundaries.childSize.height * _scale; - final screenHeight = scaleBoundaries.viewportSize.height; + final computedHeight = childSize.height * _scale; + final screenHeight = viewportSize.height; final positionY = basePosition.y; final heightDiff = computedHeight - screenHeight; @@ -153,15 +157,15 @@ mixin MagnifierControllerDelegate on State { return CornersRange(minY, maxY); } - Offset clampPosition({Offset position, double scale}) { - final _scale = scale ?? this.scale; + Offset clampPosition({Offset? position, double? scale}) { + final _scale = scale ?? this.scale!; final _position = position ?? this.position; - final computedWidth = scaleBoundaries.childSize.width * _scale; - final computedHeight = scaleBoundaries.childSize.height * _scale; + final computedWidth = childSize.width * _scale; + final computedHeight = childSize.height * _scale; - final screenWidth = scaleBoundaries.viewportSize.width; - final screenHeight = scaleBoundaries.viewportSize.height; + final screenWidth = viewportSize.width; + final screenHeight = viewportSize.height; var finalX = 0.0; if (screenWidth < computedWidth) { diff --git a/lib/widgets/common/magnifier/controller/state.dart b/lib/widgets/common/magnifier/controller/state.dart index 6185a1707..949c692c0 100644 --- a/lib/widgets/common/magnifier/controller/state.dart +++ b/lib/widgets/common/magnifier/controller/state.dart @@ -6,13 +6,13 @@ import 'package:flutter/widgets.dart'; @immutable class MagnifierState { const MagnifierState({ - @required this.position, - @required this.scale, - @required this.source, + required this.position, + required this.scale, + required this.source, }); final Offset position; - final double scale; + final double? scale; final ChangeSource source; @override diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index 1f81097e5..f71cd572e 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -12,12 +12,12 @@ import 'package:flutter/widgets.dart'; /// to user gestures, updates to the controller state and mounts the entire Layout class MagnifierCore extends StatefulWidget { const MagnifierCore({ - Key key, - @required this.child, - @required this.onTap, - @required this.controller, - @required this.scaleStateCycle, - @required this.applyScale, + Key? key, + required this.child, + required this.onTap, + required this.controller, + required this.scaleStateCycle, + required this.applyScale, this.panInertia = .2, }) : super(key: key); @@ -26,7 +26,7 @@ class MagnifierCore extends StatefulWidget { final MagnifierController controller; final ScaleStateCycle scaleStateCycle; - final MagnifierTapCallback onTap; + final MagnifierTapCallback? onTap; final bool applyScale; final double panInertia; @@ -38,18 +38,18 @@ class MagnifierCore extends StatefulWidget { } class _MagnifierCoreState extends State with TickerProviderStateMixin, MagnifierControllerDelegate, CornerHitDetector { - Offset _startFocalPoint, _lastViewportFocalPosition; - double _startScale, _quickScaleLastY, _quickScaleLastDistance; - bool _doubleTap, _quickScaleMoved; + Offset? _startFocalPoint, _lastViewportFocalPosition; + double? _startScale, _quickScaleLastY, _quickScaleLastDistance; + late bool _doubleTap, _quickScaleMoved; DateTime _lastScaleGestureDate = DateTime.now(); - AnimationController _scaleAnimationController; - Animation _scaleAnimation; + late AnimationController _scaleAnimationController; + late Animation _scaleAnimation; - AnimationController _positionAnimationController; - Animation _positionAnimation; + late AnimationController _positionAnimationController; + late Animation _positionAnimation; - ScaleBoundaries cachedScaleBoundaries; + ScaleBoundaries? cachedScaleBoundaries; void handleScaleAnimation() { setScale(_scaleAnimation.value, ChangeSource.animation); @@ -65,7 +65,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM _lastViewportFocalPosition = _startFocalPoint; _doubleTap = doubleTap; _quickScaleLastDistance = null; - _quickScaleLastY = _startFocalPoint.dy; + _quickScaleLastY = _startFocalPoint!.dy; _quickScaleMoved = false; _scaleAnimationController.stop(); @@ -78,21 +78,21 @@ class _MagnifierCoreState extends State with TickerProviderStateM // quick scale, aka one finger zoom // magic numbers from `davemorrissey/subsampling-scale-image-view` final focalPointY = details.focalPoint.dy; - final distance = (focalPointY - _startFocalPoint.dy).abs() * 2 + 20; + final distance = (focalPointY - _startFocalPoint!.dy).abs() * 2 + 20; _quickScaleLastDistance ??= distance; - final spanDiff = (1 - (distance / _quickScaleLastDistance)).abs() * .5; + final spanDiff = (1 - (distance / _quickScaleLastDistance!)).abs() * .5; _quickScaleMoved |= spanDiff > .03; - final factor = _quickScaleMoved ? (focalPointY > _quickScaleLastY ? (1 + spanDiff) : (1 - spanDiff)) : 1; + final factor = _quickScaleMoved ? (focalPointY > _quickScaleLastY! ? (1 + spanDiff) : (1 - spanDiff)) : 1; _quickScaleLastDistance = distance; _quickScaleLastY = focalPointY; - newScale = scale * factor; + newScale = scale! * factor; } else { - newScale = _startScale * details.scale; + newScale = _startScale! * details.scale; } - final scaleFocalPoint = _doubleTap ? _startFocalPoint : details.focalPoint; + final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.focalPoint; - final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition; - final scalePositionDelta = scaleBoundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale / newScale - 1); + final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!; + final scalePositionDelta = scaleBoundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale! / newScale - 1); final newPosition = position + panPositionDelta + scalePositionDelta; updateScaleStateFromNewScale(newScale, ChangeSource.gesture); @@ -107,7 +107,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM void onScaleEnd(ScaleEndDetails details) { final _position = controller.position; - final _scale = controller.scale; + final _scale = controller.scale!; final maxScale = scaleBoundaries.maxScale; final minScale = scaleBoundaries.minScale; @@ -145,14 +145,14 @@ class _MagnifierCoreState extends State with TickerProviderStateM } Duration _getAnimationDurationForVelocity({ - Cubic curve, - Tween tween, - Offset targetPixelPerSecond, + required Cubic curve, + required Tween tween, + required Offset targetPixelPerSecond, }) { assert(targetPixelPerSecond != Offset.zero); // find initial animation velocity over the first 20% of the specified curve const t = 0.2; - final animationVelocity = (tween.end - tween.begin).distance * curve.transform(t) / t; + final animationVelocity = (tween.end! - tween.begin!).distance * curve.transform(t) / t; final gestureVelocity = targetPixelPerSecond.distance; return Duration(milliseconds: (animationVelocity / gestureVelocity * 1000).round()); } @@ -162,16 +162,16 @@ class _MagnifierCoreState extends State with TickerProviderStateM final viewportTapPosition = details.localPosition; final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition); - widget.onTap.call(context, details, controller.currentState, childTapPosition); + widget.onTap!.call(context, details, controller.currentState, childTapPosition); } void onDoubleTap(TapDownDetails details) { - final viewportTapPosition = details?.localPosition; + final viewportTapPosition = details.localPosition; final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition); nextScaleState(ChangeSource.gesture, childFocalPoint: childTapPosition); } - void animateScale(double from, double to) { + void animateScale(double? from, double? to) { _scaleAnimation = Tween( begin: from, end: to, @@ -215,7 +215,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM cachedScaleBoundaries = widget.controller.scaleBoundaries; } - void animateOnScaleStateUpdate(double prevScale, double nextScale, Offset nextPosition) { + void animateOnScaleStateUpdate(double? prevScale, double? nextScale, Offset nextPosition) { animateScale(prevScale, nextScale); animatePosition(controller.position, nextPosition); } @@ -242,7 +242,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM builder: (context, snapshot) { if (!snapshot.hasData) return Container(); - final magnifierState = snapshot.data; + final magnifierState = snapshot.data!; final position = magnifierState.position; final applyScale = widget.applyScale; diff --git a/lib/widgets/common/magnifier/core/gesture_detector.dart b/lib/widgets/common/magnifier/core/gesture_detector.dart index 84ccf9c33..ca1e20720 100644 --- a/lib/widgets/common/magnifier/core/gesture_detector.dart +++ b/lib/widgets/common/magnifier/core/gesture_detector.dart @@ -7,8 +7,8 @@ import '../pan/corner_hit_detector.dart'; class MagnifierGestureDetector extends StatefulWidget { const MagnifierGestureDetector({ - Key key, - this.hitDetector, + Key? key, + required this.hitDetector, this.onScaleStart, this.onScaleUpdate, this.onScaleEnd, @@ -20,31 +20,26 @@ class MagnifierGestureDetector extends StatefulWidget { }) : super(key: key); final CornerHitDetector hitDetector; - final void Function(ScaleStartDetails details, bool doubleTap) onScaleStart; - final GestureScaleUpdateCallback onScaleUpdate; - final GestureScaleEndCallback onScaleEnd; + final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart; + final GestureScaleUpdateCallback? onScaleUpdate; + final GestureScaleEndCallback? onScaleEnd; - final GestureTapDownCallback onTapDown; - final GestureTapUpCallback onTapUp; - final GestureTapDownCallback onDoubleTap; + final GestureTapDownCallback? onTapDown; + final GestureTapUpCallback? onTapUp; + final GestureTapDownCallback? onDoubleTap; - final HitTestBehavior behavior; - final Widget child; + final HitTestBehavior? behavior; + final Widget? child; @override _MagnifierGestureDetectorState createState() => _MagnifierGestureDetectorState(); } class _MagnifierGestureDetectorState extends State { - final ValueNotifier doubleTapDetails = ValueNotifier(null); + final ValueNotifier doubleTapDetails = ValueNotifier(null); @override Widget build(BuildContext context) { - final scope = MagnifierGestureDetectorScope.of(context); - - final axis = scope?.axis; - final touchSlopFactor = scope?.touchSlopFactor; - final gestures = {}; if (widget.onTapDown != null || widget.onTapUp != null) { @@ -58,30 +53,35 @@ class _MagnifierGestureDetectorState extends State { ); } - gestures[MagnifierGestureRecognizer] = GestureRecognizerFactoryWithHandlers( - () => MagnifierGestureRecognizer( - hitDetector: widget.hitDetector, - debugOwner: this, - validateAxis: axis, - touchSlopFactor: touchSlopFactor, - doubleTapDetails: doubleTapDetails, - ), - (instance) { - instance.onStart = (details) => widget.onScaleStart(details, doubleTapDetails.value != null); - instance.onUpdate = widget.onScaleUpdate; - instance.onEnd = widget.onScaleEnd; - }, - ); + final scope = MagnifierGestureDetectorScope.of(context); + if (scope != null) { + gestures[MagnifierGestureRecognizer] = GestureRecognizerFactoryWithHandlers( + () => MagnifierGestureRecognizer( + hitDetector: widget.hitDetector, + debugOwner: this, + validateAxis: scope.axis, + touchSlopFactor: scope.touchSlopFactor, + doubleTapDetails: doubleTapDetails, + ), + (instance) { + instance.onStart = widget.onScaleStart != null ? (details) => widget.onScaleStart!(details, doubleTapDetails.value != null) : null; + instance.onUpdate = widget.onScaleUpdate; + instance.onEnd = widget.onScaleEnd; + }, + ); + } gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => DoubleTapGestureRecognizer(debugOwner: this), (instance) { instance.onDoubleTapCancel = () => doubleTapDetails.value = null; instance.onDoubleTapDown = (details) => doubleTapDetails.value = details; - instance.onDoubleTap = () { - widget.onDoubleTap(doubleTapDetails.value); - doubleTapDetails.value = null; - }; + instance.onDoubleTap = widget.onDoubleTap != null + ? () { + widget.onDoubleTap!(doubleTapDetails.value!); + doubleTapDetails.value = null; + } + : null; }, ); diff --git a/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart b/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart index 37db8bbc6..b95d73f72 100644 --- a/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart +++ b/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart @@ -9,23 +9,23 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { final CornerHitDetector hitDetector; final List validateAxis; final double touchSlopFactor; - final ValueNotifier doubleTapDetails; + final ValueNotifier doubleTapDetails; MagnifierGestureRecognizer({ - Object debugOwner, - PointerDeviceKind kind, - this.hitDetector, - this.validateAxis, + Object? debugOwner, + PointerDeviceKind? kind, + required this.hitDetector, + required this.validateAxis, this.touchSlopFactor = 2, - this.doubleTapDetails, + required this.doubleTapDetails, }) : super(debugOwner: debugOwner, kind: kind); Map _pointerLocations = {}; - Offset _initialFocalPoint; - Offset _currentFocalPoint; - double _initialSpan; - double _currentSpan; + Offset? _initialFocalPoint; + Offset? _currentFocalPoint; + double? _initialSpan; + double? _currentSpan; bool ready = true; @@ -48,7 +48,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { @override void handleEvent(PointerEvent event) { - if (validateAxis != null && validateAxis.isNotEmpty) { + if (validateAxis.isNotEmpty) { var didChangeConfiguration = false; if (event is PointerMoveEvent) { if (!event.synthesized) { @@ -82,7 +82,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { // Compute the focal point var focalPoint = Offset.zero; for (final pointer in _pointerLocations.keys) { - focalPoint += _pointerLocations[pointer]; + focalPoint += _pointerLocations[pointer]!; } _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero; @@ -91,7 +91,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { // vertical coordinates, respectively. var totalDeviation = 0.0; for (final pointer in _pointerLocations.keys) { - totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]).distance; + totalDeviation += (_currentFocalPoint! - _pointerLocations[pointer]!).distance; } _currentSpan = count > 0 ? totalDeviation / count : 0.0; } @@ -106,7 +106,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { return; } - final move = _initialFocalPoint - _currentFocalPoint; + final move = _initialFocalPoint! - _currentFocalPoint!; var shouldMove = false; if (validateAxis.length == 2) { // the image is the descendant of gesture detector(s) handling drag in both directions @@ -128,10 +128,10 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move); } - final doubleTap = doubleTapDetails?.value != null; + final doubleTap = doubleTapDetails.value != null; if (shouldMove || doubleTap) { - final spanDelta = (_currentSpan - _initialSpan).abs(); - final focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance; + final spanDelta = (_currentSpan! - _initialSpan!).abs(); + final focalPointDelta = (_currentFocalPoint! - _initialFocalPoint!).distance; // warning: do not compare `focalPointDelta` to `kPanSlop` // `ScaleGestureRecognizer` uses `kPanSlop`, but `HorizontalDragGestureRecognizer` uses `kTouchSlop` // and the magnifier recognizer may compete with the `HorizontalDragGestureRecognizer` from a containing `PageView` diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index 44fa6f590..17091e4d5 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -20,24 +20,17 @@ import 'package:flutter/material.dart'; */ class Magnifier extends StatelessWidget { const Magnifier({ - Key key, - @required this.controller, - @required this.childSize, + Key? key, + required this.controller, + required this.childSize, this.minScale = const ScaleLevel(factor: .0), this.maxScale = const ScaleLevel(factor: double.infinity), this.initialScale = const ScaleLevel(ref: ScaleReference.contained), this.scaleStateCycle = defaultScaleStateCycle, this.applyScale = true, this.onTap, - @required this.child, - }) : assert(controller != null), - assert(childSize != null), - assert(minScale != null), - assert(maxScale != null), - assert(initialScale != null), - assert(scaleStateCycle != null), - assert(applyScale != null), - super(key: key); + required this.child, + }) : super(key: key); final MagnifierController controller; @@ -55,7 +48,7 @@ class Magnifier extends StatelessWidget { final ScaleStateCycle scaleStateCycle; final bool applyScale; - final MagnifierTapCallback onTap; + final MagnifierTapCallback? onTap; final Widget child; @override @@ -67,7 +60,7 @@ class Magnifier extends StatelessWidget { maxScale: maxScale, initialScale: initialScale, viewportSize: constraints.biggest, - childSize: childSize?.isEmpty == false ? childSize : constraints.biggest, + childSize: childSize.isEmpty == false ? childSize : constraints.biggest, )); return MagnifierCore( diff --git a/lib/widgets/common/magnifier/pan/corner_hit_detector.dart b/lib/widgets/common/magnifier/pan/corner_hit_detector.dart index 482b39f5b..5e7a15a06 100644 --- a/lib/widgets/common/magnifier/pan/corner_hit_detector.dart +++ b/lib/widgets/common/magnifier/pan/corner_hit_detector.dart @@ -11,7 +11,7 @@ mixin CornerHitDetector on MagnifierControllerDelegate { // so be sure to compare with `precisionErrorTolerance` _CornerHit _hitCornersX() { - final childWidth = scaleBoundaries.childSize.width * scale; + final childWidth = scaleBoundaries.childSize.width * scale!; final viewportWidth = scaleBoundaries.viewportSize.width; if (viewportWidth + precisionErrorTolerance >= childWidth) { return _CornerHit(true, true); @@ -22,7 +22,7 @@ mixin CornerHitDetector on MagnifierControllerDelegate { } _CornerHit _hitCornersY() { - final childHeight = scaleBoundaries.childSize.height * scale; + final childHeight = scaleBoundaries.childSize.height * scale!; final viewportHeight = scaleBoundaries.viewportSize.height; if (viewportHeight + precisionErrorTolerance >= childHeight) { return _CornerHit(true, true); diff --git a/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart b/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart index 8eaee4f69..e135cf181 100644 --- a/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart +++ b/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart @@ -8,12 +8,12 @@ import 'package:flutter/widgets.dart'; /// such as [PageView], [Dismissible], [BottomSheet]. class MagnifierGestureDetectorScope extends InheritedWidget { const MagnifierGestureDetectorScope({ - this.axis, + required this.axis, this.touchSlopFactor = .8, - @required Widget child, + required Widget child, }) : super(child: child); - static MagnifierGestureDetectorScope of(BuildContext context) { + static MagnifierGestureDetectorScope? of(BuildContext context) { final scope = context.dependOnInheritedWidgetOfExactType(); return scope; } diff --git a/lib/widgets/common/magnifier/pan/scroll_physics.dart b/lib/widgets/common/magnifier/pan/scroll_physics.dart index 9f8e14d13..61d3492ec 100644 --- a/lib/widgets/common/magnifier/pan/scroll_physics.dart +++ b/lib/widgets/common/magnifier/pan/scroll_physics.dart @@ -8,7 +8,7 @@ import 'package:flutter/widgets.dart'; class MagnifierScrollerPhysics extends ScrollPhysics { const MagnifierScrollerPhysics({ this.touchSlopFactor = 1, - ScrollPhysics parent, + ScrollPhysics? parent, }) : super(parent: parent); // in [0, 1] @@ -17,7 +17,7 @@ class MagnifierScrollerPhysics extends ScrollPhysics { final double touchSlopFactor; @override - MagnifierScrollerPhysics applyTo(ScrollPhysics ancestor) { + MagnifierScrollerPhysics applyTo(ScrollPhysics? ancestor) { return MagnifierScrollerPhysics( touchSlopFactor: touchSlopFactor, parent: buildParent(ancestor), diff --git a/lib/widgets/common/magnifier/scale/scale_boundaries.dart b/lib/widgets/common/magnifier/scale/scale_boundaries.dart index 30615777a..3c9db4994 100644 --- a/lib/widgets/common/magnifier/scale/scale_boundaries.dart +++ b/lib/widgets/common/magnifier/scale/scale_boundaries.dart @@ -14,11 +14,11 @@ class ScaleBoundaries { final Size childSize; const ScaleBoundaries({ - @required ScaleLevel minScale, - @required ScaleLevel maxScale, - @required ScaleLevel initialScale, - @required this.viewportSize, - @required this.childSize, + required ScaleLevel minScale, + required ScaleLevel maxScale, + required ScaleLevel initialScale, + required this.viewportSize, + required this.childSize, }) : _minScale = minScale, _maxScale = maxScale, _initialScale = initialScale; @@ -51,7 +51,7 @@ class ScaleBoundaries { } Offset viewportToChildPosition(MagnifierController controller, Offset viewportPosition) { - return viewportToStatePosition(controller, viewportPosition) / controller.scale + _childCenter; + return viewportToStatePosition(controller, viewportPosition) / controller.scale! + _childCenter; } Offset childToStatePosition(double scale, Offset childPosition) { diff --git a/lib/widgets/common/magnifier/scale/state.dart b/lib/widgets/common/magnifier/scale/state.dart index 81595109e..1a587bb50 100644 --- a/lib/widgets/common/magnifier/scale/state.dart +++ b/lib/widgets/common/magnifier/scale/state.dart @@ -7,14 +7,14 @@ import 'package:flutter/widgets.dart'; @immutable class ScaleStateChange { const ScaleStateChange({ - @required this.state, - @required this.source, + required this.state, + required this.source, this.childFocalPoint, }); final ScaleState state; final ChangeSource source; - final Offset childFocalPoint; + final Offset? childFocalPoint; @override bool operator ==(Object other) => identical(this, other) || other is ScaleStateChange && runtimeType == other.runtimeType && state == other.state && childFocalPoint == other.childFocalPoint; diff --git a/lib/widgets/common/providers/highlight_info_provider.dart b/lib/widgets/common/providers/highlight_info_provider.dart index 8b09c1695..a1de6717a 100644 --- a/lib/widgets/common/providers/highlight_info_provider.dart +++ b/lib/widgets/common/providers/highlight_info_provider.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; class HighlightInfoProvider extends StatelessWidget { final Widget child; - const HighlightInfoProvider({@required this.child}); + const HighlightInfoProvider({required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/providers/media_query_data_provider.dart b/lib/widgets/common/providers/media_query_data_provider.dart index 25ba8c4c7..dc44699f9 100644 --- a/lib/widgets/common/providers/media_query_data_provider.dart +++ b/lib/widgets/common/providers/media_query_data_provider.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; class MediaQueryDataProvider extends StatelessWidget { final Widget child; - const MediaQueryDataProvider({@required this.child}); + const MediaQueryDataProvider({required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/providers/tile_extent_controller_provider.dart b/lib/widgets/common/providers/tile_extent_controller_provider.dart index 0b39401c7..19701e48b 100644 --- a/lib/widgets/common/providers/tile_extent_controller_provider.dart +++ b/lib/widgets/common/providers/tile_extent_controller_provider.dart @@ -7,8 +7,8 @@ class TileExtentControllerProvider extends StatelessWidget { final Widget child; const TileExtentControllerProvider({ - @required this.controller, - @required this.child, + required this.controller, + required this.child, }); @override diff --git a/lib/widgets/common/scaling.dart b/lib/widgets/common/scaling.dart index 702f5dbc8..1f24cfa2d 100644 --- a/lib/widgets/common/scaling.dart +++ b/lib/widgets/common/scaling.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'dart:ui' as ui; import 'package:aves/theme/durations.dart'; @@ -17,20 +18,20 @@ class ScalerMetadata { class GridScaleGestureDetector extends StatefulWidget { final GlobalKey scrollableKey; final ValueNotifier appBarHeightNotifier; - final Widget Function(Offset center, double extent, Widget child) gridBuilder; + final Widget Function(Offset center, double extent, Widget child)? gridBuilder; final Widget Function(T item, double extent) scaledBuilder; final Rect Function(BuildContext context, T item) getScaledItemTileRect; final void Function(T item) onScaled; final Widget child; const GridScaleGestureDetector({ - @required this.scrollableKey, - @required this.appBarHeightNotifier, + required this.scrollableKey, + required this.appBarHeightNotifier, this.gridBuilder, - @required this.scaledBuilder, - @required this.getScaledItemTileRect, - @required this.onScaled, - @required this.child, + required this.scaledBuilder, + required this.getScaledItemTileRect, + required this.onScaled, + required this.child, }); @override @@ -38,11 +39,11 @@ class GridScaleGestureDetector extends StatefulWidget { } class _GridScaleGestureDetectorState extends State> { - double _startExtent, _extentMin, _extentMax; + double? _startExtent, _extentMin, _extentMax; bool _applyingScale = false; - ValueNotifier _scaledExtentNotifier; - OverlayEntry _overlayEntry; - ScalerMetadata _metadata; + ValueNotifier? _scaledExtentNotifier; + OverlayEntry? _overlayEntry; + ScalerMetadata? _metadata; @override Widget build(BuildContext context) { @@ -52,19 +53,19 @@ class _GridScaleGestureDetectorState extends State(BoxHitTestResult result) => result.path.firstWhere((el) => el.target is U, orElse: () => null)?.target as U; + U? firstOf(BoxHitTestResult result) => result.path.firstWhereOrNull((el) => el.target is U)?.target as U?; final renderMetaData = firstOf(result); // abort if we cannot find an image to show on overlay if (renderMetaData == null) return; _metadata = renderMetaData.metaData; _startExtent = renderMetaData.size.width; - _scaledExtentNotifier = ValueNotifier(_startExtent); + _scaledExtentNotifier = ValueNotifier(_startExtent!); // not the same as `MediaQuery.size.width`, because of screen insets/padding final gridWidth = scrollableBox.size.width; @@ -73,28 +74,28 @@ class _GridScaleGestureDetectorState extends State ScaleOverlay( - builder: (extent) => widget.scaledBuilder(_metadata.item, extent), + builder: (extent) => widget.scaledBuilder(_metadata!.item, extent), center: thumbnailCenter, viewportWidth: gridWidth, gridBuilder: widget.gridBuilder, - scaledExtentNotifier: _scaledExtentNotifier, + scaledExtentNotifier: _scaledExtentNotifier!, ), ); - Overlay.of(scrollableContext).insert(_overlayEntry); + Overlay.of(scrollableContext)!.insert(_overlayEntry!); }, onScaleUpdate: (details) { if (_scaledExtentNotifier == null) return; final s = details.scale; - _scaledExtentNotifier.value = (_startExtent * s).clamp(_extentMin, _extentMax); + _scaledExtentNotifier!.value = (_startExtent! * s).clamp(_extentMin!, _extentMax!); }, onScaleEnd: (details) { if (_scaledExtentNotifier == null) return; if (_overlayEntry != null) { - _overlayEntry.remove(); + _overlayEntry!.remove(); _overlayEntry = null; } @@ -102,18 +103,18 @@ class _GridScaleGestureDetectorState extends State(); final oldExtent = tileExtentController.extentNotifier.value; // sanitize and update grid layout if necessary - final newExtent = tileExtentController.setUserPreferredExtent(_scaledExtentNotifier.value); + final newExtent = tileExtentController.setUserPreferredExtent(_scaledExtentNotifier!.value); _scaledExtentNotifier = null; if (newExtent == oldExtent) { _applyingScale = false; } else { // scroll to show the focal point thumbnail at its new position - WidgetsBinding.instance.addPostFrameCallback((_) { - final entry = _metadata.item; + WidgetsBinding.instance!.addPostFrameCallback((_) { + final entry = _metadata!.item; _scrollToItem(entry); // warning: posting `onScaled` in the next frame with `addPostFrameCallback` // would trigger only when the scrollable offset actually changes - Future.delayed(Durations.collectionScalingCompleteNotificationDelay).then((_) => widget.onScaled?.call(entry)); + Future.delayed(Durations.collectionScalingCompleteNotificationDelay).then((_) => widget.onScaled(entry)); _applyingScale = false; }); } @@ -136,7 +137,7 @@ class _GridScaleGestureDetectorState extends State scaledExtentNotifier; - final Widget Function(Offset center, double extent, Widget child) gridBuilder; + final Widget Function(Offset center, double extent, Widget child)? gridBuilder; const ScaleOverlay({ - @required this.builder, - @required this.center, - @required this.viewportWidth, - @required this.scaledExtentNotifier, + required this.builder, + required this.center, + required this.viewportWidth, + required this.scaledExtentNotifier, this.gridBuilder, }); @@ -181,7 +182,7 @@ class _ScaleOverlayState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _init = true)); + WidgetsBinding.instance!.addPostFrameCallback((_) => setState(() => _init = true)); } @override @@ -256,11 +257,11 @@ class GridPainter extends CustomPainter { final Color color; const GridPainter({ - @required this.center, - @required this.extent, + required this.center, + required this.extent, this.spacing = 0.0, this.strokeWidth = 1.0, - @required this.color, + required this.color, }); @override diff --git a/lib/widgets/common/tile_extent_controller.dart b/lib/widgets/common/tile_extent_controller.dart index 9500d1641..5ee44678b 100644 --- a/lib/widgets/common/tile_extent_controller.dart +++ b/lib/widgets/common/tile_extent_controller.dart @@ -10,17 +10,17 @@ class TileExtentController { final double spacing, extentMin, extentMax; final ValueNotifier extentNotifier = ValueNotifier(0); - Size _viewportSize; + Size _viewportSize = Size.zero; Size get viewportSize => _viewportSize; TileExtentController({ - @required this.settingsRouteKey, + required this.settingsRouteKey, this.columnCountMin = 2, - @required this.columnCountDefault, - @required this.extentMin, + required this.columnCountDefault, + required this.extentMin, this.extentMax = 300, - @required this.spacing, + required this.spacing, }); void setViewportSize(Size viewportSize) { diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart index 9854088c1..01ef8f608 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/android_apps.dart @@ -12,7 +12,7 @@ class DebugAndroidAppSection extends StatefulWidget { } class _DebugAndroidAppSectionState extends State with AutomaticKeepAliveClientMixin { - Future> _loader; + late Future> _loader; static const iconSize = 20.0; @@ -36,7 +36,7 @@ class _DebugAndroidAppSectionState extends State with Au builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final packages = snapshot.data.toList()..sort((a, b) => compareAsciiUpperCase(a.packageName, b.packageName)); + final packages = snapshot.data!.toList()..sort((a, b) => compareAsciiUpperCase(a.packageName, b.packageName)); final enabledTheme = IconTheme.of(context); final disabledTheme = enabledTheme.merge(IconThemeData(opacity: .2)); return Column( diff --git a/lib/widgets/debug/android_dirs.dart b/lib/widgets/debug/android_dirs.dart index 92e71bdf7..c5a1f9eb1 100644 --- a/lib/widgets/debug/android_dirs.dart +++ b/lib/widgets/debug/android_dirs.dart @@ -11,7 +11,7 @@ class DebugAndroidDirSection extends StatefulWidget { } class _DebugAndroidDirSectionState extends State with AutomaticKeepAliveClientMixin { - Future _loader; + late Future _loader; @override void initState() { @@ -33,7 +33,7 @@ class _DebugAndroidDirSectionState extends State with Au builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final data = SplayTreeMap.of(snapshot.data.map((k, v) => MapEntry(k.toString(), v?.toString() ?? 'null'))); + final data = SplayTreeMap.of(snapshot.data!.map((k, v) => MapEntry(k.toString(), v?.toString() ?? 'null'))); return InfoRowGroup(data); }, ), diff --git a/lib/widgets/debug/android_env.dart b/lib/widgets/debug/android_env.dart index 0600393dd..164cbe3a6 100644 --- a/lib/widgets/debug/android_env.dart +++ b/lib/widgets/debug/android_env.dart @@ -11,7 +11,7 @@ class DebugAndroidEnvironmentSection extends StatefulWidget { } class _DebugAndroidEnvironmentSectionState extends State with AutomaticKeepAliveClientMixin { - Future _loader; + late Future _loader; @override void initState() { @@ -33,7 +33,7 @@ class _DebugAndroidEnvironmentSectionState extends State MapEntry(k.toString(), v?.toString() ?? 'null'))); + final data = SplayTreeMap.of(snapshot.data!.map((k, v) => MapEntry(k.toString(), v?.toString() ?? 'null'))); return InfoRowGroup(data); }, ), diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index d4698e79e..29c198a70 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -28,7 +28,7 @@ class _AppDebugPageState extends State { Set get visibleEntries => source.visibleEntries; - static OverlayEntry _taskQueueOverlayEntry; + static OverlayEntry? _taskQueueOverlayEntry; @override Widget build(BuildContext context) { @@ -85,7 +85,7 @@ class _AppDebugPageState extends State { _taskQueueOverlayEntry = OverlayEntry( builder: (context) => DebugTaskQueueOverlay(), ); - Overlay.of(context).insert(_taskQueueOverlayEntry); + Overlay.of(context)!.insert(_taskQueueOverlayEntry!); } else { _taskQueueOverlayEntry = null; } diff --git a/lib/widgets/debug/cache.dart b/lib/widgets/debug/cache.dart index cc9842fe4..bad822e0a 100644 --- a/lib/widgets/debug/cache.dart +++ b/lib/widgets/debug/cache.dart @@ -24,12 +24,12 @@ class _DebugCacheSectionState extends State with AutomaticKee Row( children: [ Expanded( - child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t${formatFilesize(imageCache.currentSizeBytes)}/${formatFilesize(imageCache.maximumSizeBytes)}'), + child: Text('Image cache:\n\t${imageCache!.currentSize}/${imageCache!.maximumSize} items\n\t${formatFilesize(imageCache!.currentSizeBytes)}/${formatFilesize(imageCache!.maximumSizeBytes)}'), ), SizedBox(width: 8), ElevatedButton( onPressed: () { - imageCache.clear(); + imageCache!.clear(); setState(() {}); }, diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 9dd19894c..119502afb 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -13,13 +13,13 @@ class DebugAppDatabaseSection extends StatefulWidget { } class _DebugAppDatabaseSectionState extends State with AutomaticKeepAliveClientMixin { - Future _dbFileSizeLoader; - Future> _dbEntryLoader; - Future> _dbDateLoader; - Future> _dbMetadataLoader; - Future> _dbAddressLoader; - Future> _dbFavouritesLoader; - Future> _dbCoversLoader; + late Future _dbFileSizeLoader; + late Future> _dbEntryLoader; + late Future> _dbDateLoader; + late Future> _dbMetadataLoader; + late Future> _dbAddressLoader; + late Future> _dbFavouritesLoader; + late Future> _dbCoversLoader; @override void initState() { @@ -48,7 +48,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('DB file size: ${formatFilesize(snapshot.data)}'), + child: Text('DB file size: ${formatFilesize(snapshot.data!)}'), ), SizedBox(width: 8), ElevatedButton( @@ -69,7 +69,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('entry rows: ${snapshot.data.length}'), + child: Text('entry rows: ${snapshot.data!.length}'), ), SizedBox(width: 8), ElevatedButton( @@ -90,7 +90,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('date rows: ${snapshot.data.length}'), + child: Text('date rows: ${snapshot.data!.length}'), ), SizedBox(width: 8), ElevatedButton( @@ -111,7 +111,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('metadata rows: ${snapshot.data.length}'), + child: Text('metadata rows: ${snapshot.data!.length}'), ), SizedBox(width: 8), ElevatedButton( @@ -132,7 +132,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('address rows: ${snapshot.data.length}'), + child: Text('address rows: ${snapshot.data!.length}'), ), SizedBox(width: 8), ElevatedButton( @@ -153,7 +153,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('favourite rows: ${snapshot.data.length} (${favourites.count} in memory)'), + child: Text('favourite rows: ${snapshot.data!.length} (${favourites.count} in memory)'), ), SizedBox(width: 8), ElevatedButton( @@ -174,7 +174,7 @@ class _DebugAppDatabaseSectionState extends State with return Row( children: [ Expanded( - child: Text('cover rows: ${snapshot.data.length} (${covers.count} in memory)'), + child: Text('cover rows: ${snapshot.data!.length} (${covers.count} in memory)'), ), SizedBox(width: 8), ElevatedButton( diff --git a/lib/widgets/debug/overlay.dart b/lib/widgets/debug/overlay.dart index 9fb6b0f06..5da36f76d 100644 --- a/lib/widgets/debug/overlay.dart +++ b/lib/widgets/debug/overlay.dart @@ -11,7 +11,7 @@ class DebugTaskQueueOverlay extends StatelessWidget { alignment: AlignmentDirectional.bottomStart, child: SafeArea( child: Container( - color: Colors.indigo[900].withAlpha(0xCC), + color: Colors.indigo[900]!.withAlpha(0xCC), padding: EdgeInsets.all(8), child: StreamBuilder( stream: servicePolicy.queueStream, @@ -19,7 +19,7 @@ class DebugTaskQueueOverlay extends StatelessWidget { if (snapshot.hasError) return SizedBox.shrink(); final queuedEntries = >[]; if (snapshot.hasData) { - final state = snapshot.data; + final state = snapshot.data!; queuedEntries.add(MapEntry('run', state.runningCount)); queuedEntries.add(MapEntry('paused', state.pausedCount)); queuedEntries.addAll(state.queueByPriority.entries.map((kv) => MapEntry(kv.key.toString(), kv.value))); diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index e83bba3a5..aae0e036b 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -43,7 +43,7 @@ class DebugSettingsSection extends StatelessWidget { 'searchHistory': toMultiline(settings.searchHistory), 'lastVersionCheckDate': '${settings.lastVersionCheckDate}', 'locale': '${settings.locale}', - 'systemLocale': '${WidgetsBinding.instance.window.locale}', + 'systemLocale': '${WidgetsBinding.instance!.window.locale}', }), ), ], diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index 9272ecfaa..e7227b8fd 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -11,7 +11,7 @@ class DebugStorageSection extends StatefulWidget { } class _DebugStorageSectionState extends State with AutomaticKeepAliveClientMixin { - final Map _freeSpaceByVolume = {}; + final Map _freeSpaceByVolume = {}; @override void initState() { diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index 61b7a7e2f..9092b695d 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/item_pick_dialog.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -18,8 +19,8 @@ class AddShortcutDialog extends StatefulWidget { final String defaultName; const AddShortcutDialog({ - @required this.collection, - @required this.defaultName, + required this.collection, + required this.defaultName, }); @override @@ -29,7 +30,7 @@ class AddShortcutDialog extends StatefulWidget { class _AddShortcutDialogState extends State { final TextEditingController _nameController = TextEditingController(); final ValueNotifier _isValidNotifier = ValueNotifier(false); - AvesEntry _coverEntry; + AvesEntry? _coverEntry; CollectionLens get collection => widget.collection; @@ -40,7 +41,7 @@ class _AddShortcutDialogState extends State { super.initState(); final entries = collection.sortedEntries; if (entries.isNotEmpty) { - final coverEntries = filters.map(covers.coverContentId).where((id) => id != null).map((id) => entries.firstWhere((entry) => entry.contentId == id, orElse: () => null)).where((entry) => entry != null); + final coverEntries = filters.map(covers.coverContentId).where((id) => id != null).map((id) => entries.firstWhereOrNull((entry) => entry.contentId == id)).where((entry) => entry != null); _coverEntry = coverEntries.isNotEmpty ? coverEntries.first : entries.first; } _nameController.text = widget.defaultName; @@ -67,7 +68,7 @@ class _AddShortcutDialogState extends State { Container( alignment: Alignment.center, padding: EdgeInsets.only(top: 16), - child: _buildCover(_coverEntry, extent), + child: _buildCover(_coverEntry!, extent), ), Padding( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 24), @@ -147,9 +148,9 @@ class _AddShortcutDialogState extends State { } Future _validate() async { - final name = _nameController.text ?? ''; + final name = _nameController.text; _isValidNotifier.value = name.isNotEmpty; } - void _submit(BuildContext context) => Navigator.pop(context, Tuple2(_coverEntry, _nameController.text)); + void _submit(BuildContext context) => Navigator.pop(context, Tuple2(_coverEntry, _nameController.text)); } diff --git a/lib/widgets/dialogs/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart index 1542726bd..0626887ea 100644 --- a/lib/widgets/dialogs/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -9,12 +9,12 @@ class AvesDialog extends AlertDialog { static const borderWidth = 1.0; AvesDialog({ - @required BuildContext context, - String title, - ScrollController scrollController, - List scrollableContent, - Widget content, - @required List actions, + required BuildContext context, + String? title, + ScrollController? scrollController, + List? scrollableContent, + Widget? content, + required List actions, }) : assert((scrollableContent != null) ^ (content != null)), super( title: title != null @@ -64,7 +64,7 @@ class AvesDialog extends AlertDialog { class DialogTitle extends StatelessWidget { final String title; - const DialogTitle({@required this.title}); + const DialogTitle({required this.title}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index 99af9d174..c1323b2e0 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -9,14 +9,14 @@ typedef TextBuilder = String Function(T value); class AvesSelectionDialog extends StatefulWidget { final T initialValue; final Map options; - final TextBuilder optionSubtitleBuilder; + final TextBuilder? optionSubtitleBuilder; final String title; const AvesSelectionDialog({ - @required this.initialValue, - @required this.options, + required this.initialValue, + required this.options, this.optionSubtitleBuilder, - @required this.title, + required this.title, }); @override @@ -24,7 +24,7 @@ class AvesSelectionDialog extends StatefulWidget { } class _AvesSelectionDialogState extends State> { - T _selectedValue; + late T _selectedValue; @override void initState() { diff --git a/lib/widgets/dialogs/cover_selection_dialog.dart b/lib/widgets/dialogs/cover_selection_dialog.dart index e939810f9..102011dac 100644 --- a/lib/widgets/dialogs/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/cover_selection_dialog.dart @@ -14,11 +14,11 @@ import 'package:tuple/tuple.dart'; class CoverSelectionDialog extends StatefulWidget { final CollectionFilter filter; - final AvesEntry customEntry; + final AvesEntry? customEntry; const CoverSelectionDialog({ - @required this.filter, - @required this.customEntry, + required this.filter, + required this.customEntry, }); @override @@ -26,8 +26,8 @@ class CoverSelectionDialog extends StatefulWidget { } class _CoverSelectionDialogState extends State { - bool _isCustom; - AvesEntry _customEntry, _recentEntry; + late bool _isCustom; + AvesEntry? _customEntry, _recentEntry; CollectionFilter get filter => widget.filter; @@ -59,10 +59,11 @@ class _CoverSelectionDialogState extends State { overflow: TextOverflow.fade, maxLines: 1, ); - return RadioListTile( + return RadioListTile( value: isCustom, groupValue: _isCustom, onChanged: (v) { + if (v == null) return; if (v && _customEntry == null) { _pickEntry(); return; @@ -101,7 +102,7 @@ class _CoverSelectionDialogState extends State { child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), TextButton( - onPressed: () => Navigator.pop(context, Tuple2(_isCustom, _customEntry)), + onPressed: () => Navigator.pop(context, Tuple2(_isCustom, _customEntry)), child: Text(l10n.applyButtonLabel), ), ], diff --git a/lib/widgets/dialogs/create_album_dialog.dart b/lib/widgets/dialogs/create_album_dialog.dart index 8b1852247..71077fb5c 100644 --- a/lib/widgets/dialogs/create_album_dialog.dart +++ b/lib/widgets/dialogs/create_album_dialog.dart @@ -21,8 +21,8 @@ class _CreateAlbumDialogState extends State { final FocusNode _nameFieldFocusNode = FocusNode(); final ValueNotifier _existsNotifier = ValueNotifier(false); final ValueNotifier _isValidNotifier = ValueNotifier(false); - Set _allVolumes; - StorageVolume _primaryVolume, _selectedVolume; + late Set _allVolumes; + late StorageVolume _primaryVolume, _selectedVolume; @override void initState() { @@ -46,8 +46,8 @@ class _CreateAlbumDialogState extends State { if (_allVolumes.length > 1) { final byPrimary = groupBy(_allVolumes, (volume) => volume.isPrimary); int compare(StorageVolume a, StorageVolume b) => compareAsciiUpperCase(a.path, b.path); - final primaryVolumes = byPrimary[true]..sort(compare); - final otherVolumes = byPrimary[false]..sort(compare); + final primaryVolumes = (byPrimary[true] ?? [])..sort(compare); + final otherVolumes = (byPrimary[false] ?? [])..sort(compare); volumeTiles.addAll([ Padding( padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(top: 20), @@ -106,7 +106,7 @@ class _CreateAlbumDialogState extends State { value: volume, groupValue: _selectedVolume, onChanged: (volume) { - _selectedVolume = volume; + _selectedVolume = volume!; _validate(); setState(() {}); }, @@ -142,12 +142,12 @@ class _CreateAlbumDialogState extends State { } String _buildAlbumPath(String name) { - if (name == null || name.isEmpty) return ''; + if (name.isEmpty) return ''; return pContext.join(_selectedVolume.path, 'Pictures', name); } Future _validate() async { - final newName = _nameController.text ?? ''; + final newName = _nameController.text; final path = _buildAlbumPath(newName); final exists = newName.isNotEmpty && await Directory(path).exists(); _existsNotifier.value = exists; diff --git a/lib/widgets/dialogs/rename_album_dialog.dart b/lib/widgets/dialogs/rename_album_dialog.dart index 07ee1a9e2..2c9a09726 100644 --- a/lib/widgets/dialogs/rename_album_dialog.dart +++ b/lib/widgets/dialogs/rename_album_dialog.dart @@ -74,12 +74,12 @@ class _RenameAlbumDialogState extends State { } String _buildAlbumPath(String name) { - if (name == null || name.isEmpty) return ''; + if (name.isEmpty) return ''; return pContext.join(pContext.dirname(album), name); } Future _validate() async { - final newName = _nameController.text ?? ''; + final newName = _nameController.text; final path = _buildAlbumPath(newName); final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound; _existsNotifier.value = exists && newName != initialValue; diff --git a/lib/widgets/dialogs/rename_entry_dialog.dart b/lib/widgets/dialogs/rename_entry_dialog.dart index a7c25692c..39c343e84 100644 --- a/lib/widgets/dialogs/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/rename_entry_dialog.dart @@ -25,7 +25,7 @@ class _RenameEntryDialogState extends State { @override void initState() { super.initState(); - _nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle; + _nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle ?? ''; _validate(); } @@ -68,12 +68,12 @@ class _RenameEntryDialogState extends State { } String _buildEntryPath(String name) { - if (name == null || name.isEmpty) return ''; - return pContext.join(entry.directory, name + entry.extension); + if (name.isEmpty) return ''; + return pContext.join(entry.directory!, name + entry.extension!); } Future _validate() async { - final newName = _nameController.text ?? ''; + final newName = _nameController.text; final path = _buildEntryPath(newName); final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound; _isValidNotifier.value = newName.isNotEmpty && !exists; diff --git a/lib/widgets/drawer/album_tile.dart b/lib/widgets/drawer/album_tile.dart index 0c2706846..e47e0b1b7 100644 --- a/lib/widgets/drawer/album_tile.dart +++ b/lib/widgets/drawer/album_tile.dart @@ -19,7 +19,7 @@ class AlbumTile extends StatelessWidget { return CollectionNavTile( leading: IconUtils.getAlbumIcon( context: context, - album: album, + albumPath: album, ), title: displayName, trailing: androidFileUtils.isOnRemovableStorage(album) diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index 6a5b88e29..e2141091c 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -33,7 +33,7 @@ class AppDrawer extends StatefulWidget { } class _AppDrawerState extends State { - Future _newVersionLoader; + late Future _newVersionLoader; CollectionSource get source => context.read(); @@ -75,7 +75,7 @@ class _AppDrawerState extends State { padding: EdgeInsets.only(bottom: mqPaddingBottom), child: IconTheme( data: iconTheme.copyWith( - size: iconTheme.size * MediaQuery.textScaleFactorOf(context), + size: iconTheme.size! * MediaQuery.textScaleFactorOf(context), ), child: Column( children: drawerItems, diff --git a/lib/widgets/drawer/collection_tile.dart b/lib/widgets/drawer/collection_tile.dart index 5001baa48..221c280cd 100644 --- a/lib/widgets/drawer/collection_tile.dart +++ b/lib/widgets/drawer/collection_tile.dart @@ -2,23 +2,22 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class CollectionNavTile extends StatelessWidget { - final Widget leading; + final Widget? leading; final String title; - final Widget trailing; + final Widget? trailing; final bool dense; - final CollectionFilter filter; + final CollectionFilter? filter; const CollectionNavTile({ - @required this.leading, - @required this.title, + required this.leading, + required this.title, this.trailing, - bool dense, - @required this.filter, + bool? dense, + required this.filter, }) : dense = dense ?? false; @override diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index 47602c4fd..5fce6f524 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -5,18 +5,18 @@ import 'package:flutter/material.dart'; class NavTile extends StatelessWidget { final IconData icon; final String title; - final Widget trailing; + final Widget? trailing; final bool topLevel; final String routeName; final WidgetBuilder pageBuilder; const NavTile({ - @required this.icon, - @required this.title, + required this.icon, + required this.title, this.trailing, this.topLevel = true, - @required this.routeName, - @required this.pageBuilder, + required this.routeName, + required this.pageBuilder, }); @override @@ -32,9 +32,9 @@ class NavTile extends StatelessWidget { ? Builder( builder: (context) => DefaultTextStyle.merge( style: TextStyle( - color: IconTheme.of(context).color.withOpacity(.6), + color: IconTheme.of(context).color!.withOpacity(.6), ), - child: trailing, + child: trailing!, ), ) : null, diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 4c44049d6..1bc743cbe 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -30,8 +30,8 @@ class AlbumPickPage extends StatefulWidget { final MoveType moveType; const AlbumPickPage({ - @required this.source, - @required this.moveType, + required this.source, + required this.moveType, }); @override @@ -66,15 +66,15 @@ class _AlbumPickPageState extends State { showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, queryNotifier: _queryNotifier, applyQuery: (filters, query) { - if (query == null || query.isEmpty) return filters; + if (query.isEmpty) return filters; query = query.toUpperCase(); - return filters.where((item) => item.filter.displayName.toUpperCase().contains(query)).toList(); + return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList(); }, emptyBuilder: () => EmptyContent( icon: AIcons.album, text: context.l10n.albumEmpty, ), - onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), + onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter).album), ), ); }, @@ -91,10 +91,10 @@ class AlbumPickAppBar extends StatelessWidget { static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight; const AlbumPickAppBar({ - @required this.source, - @required this.moveType, - @required this.actionDelegate, - @required this.queryNotifier, + required this.source, + required this.moveType, + required this.actionDelegate, + required this.queryNotifier, }); @override @@ -108,7 +108,7 @@ class AlbumPickAppBar extends StatelessWidget { case MoveType.move: return context.l10n.albumPickPageTitleMove; default: - return null; + return moveType.toString(); } } @@ -169,7 +169,7 @@ class AlbumFilterBar extends StatelessWidget implements PreferredSizeWidget { static const preferredHeight = kToolbarHeight; const AlbumFilterBar({ - @required this.filterNotifier, + required this.filterNotifier, }); @override diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 9e5049b99..b006cdfa6 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -75,8 +75,8 @@ class AlbumListPage extends StatelessWidget { static Map>> _group(BuildContext context, Iterable> sortedMapEntries) { final pinned = settings.pinnedFilters.whereType(); final byPin = groupBy, bool>(sortedMapEntries, (e) => pinned.contains(e.filter)); - final pinnedMapEntries = (byPin[true] ?? []); - final unpinnedMapEntries = (byPin[false] ?? []); + final pinnedMapEntries = byPin[true] ?? []; + final unpinnedMapEntries = byPin[false] ?? []; var sections = >>{}; switch (settings.albumGroupFactor) { @@ -94,11 +94,12 @@ class AlbumListPage extends StatelessWidget { return specialKey; } }); - sections = { + sections = Map.fromEntries({ + // group ordering specialKey: sections[specialKey], appsKey: sections[appsKey], regularKey: sections[regularKey], - }..removeWhere((key, value) => value == null); + }.entries.where((kv) => kv.value != null).cast>>>()); break; case AlbumChipGroupFactor.volume: sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 69ebb2de8..5b0cf1143 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -22,6 +22,7 @@ import 'package:aves/widgets/dialogs/rename_album_dialog.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/tags_page.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -83,8 +84,8 @@ class ChipActionDelegate { void _showCoverSelectionDialog(BuildContext context, CollectionFilter filter) async { final contentId = covers.coverContentId(filter); - final customEntry = context.read().visibleEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); - final coverSelection = await showDialog>( + final customEntry = context.read().visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId); + final coverSelection = await showDialog>( context: context, builder: (context) => CoverSelectionDialog( filter: filter, diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 841745d44..5b0a4ecdd 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -25,15 +25,15 @@ import 'package:provider/provider.dart'; class DecoratedFilterChip extends StatelessWidget { final CollectionFilter filter; final double extent; - final AvesEntry coverEntry; + final AvesEntry? coverEntry; final bool pinned, highlightable; - final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final FilterCallback? onTap; + final OffsetFilterCallback? onLongPress; const DecoratedFilterChip({ - Key key, - @required this.filter, - @required this.extent, + Key? key, + required this.filter, + required this.extent, this.coverEntry, this.pinned = false, this.highlightable = true, @@ -50,7 +50,7 @@ class DecoratedFilterChip extends StatelessWidget { { final album = (filter as AlbumFilter).album; return StreamBuilder( - stream: source.eventBus.on().where((event) => event.directories == null || event.directories.contains(album)), + stream: source.eventBus.on().where((event) => event.directories == null || event.directories!.contains(album)), builder: (context, snapshot) => _buildChip(source), ); } @@ -58,7 +58,7 @@ class DecoratedFilterChip extends StatelessWidget { { final countryCode = (filter as LocationFilter).countryCode; return StreamBuilder( - stream: source.eventBus.on().where((event) => event.countryCodes == null || event.countryCodes.contains(countryCode)), + stream: source.eventBus.on().where((event) => event.countryCodes == null || event.countryCodes!.contains(countryCode)), builder: (context, snapshot) => _buildChip(source), ); } @@ -66,7 +66,7 @@ class DecoratedFilterChip extends StatelessWidget { { final tag = (filter as TagFilter).tag; return StreamBuilder( - stream: source.eventBus.on().where((event) => event.tags == null || event.tags.contains(tag)), + stream: source.eventBus.on().where((event) => event.tags == null || event.tags!.contains(tag)), builder: (context, snapshot) => _buildChip(source), ); } diff --git a/lib/widgets/filter_grids/common/draggable_thumb_label.dart b/lib/widgets/filter_grids/common/draggable_thumb_label.dart index ba1dab337..0967c385c 100644 --- a/lib/widgets/filter_grids/common/draggable_thumb_label.dart +++ b/lib/widgets/filter_grids/common/draggable_thumb_label.dart @@ -11,8 +11,8 @@ class FilterDraggableThumbLabel extends StatelessWid final double offsetY; const FilterDraggableThumbLabel({ - @required this.sortFactor, - @required this.offsetY, + required this.sortFactor, + required this.offsetY, }); @override @@ -25,17 +25,15 @@ class FilterDraggableThumbLabel extends StatelessWid return [ context.l10n.itemCount(context.read().count(filterGridItem.filter)), ]; - break; case ChipSortFactor.date: return [ - DraggableThumbLabel.formatMonthThumbLabel(context, filterGridItem.entry.bestDate), + DraggableThumbLabel.formatMonthThumbLabel(context, filterGridItem.entry?.bestDate), ]; case ChipSortFactor.name: return [ filterGridItem.filter.getLabel(context), ]; } - return []; }, ); } diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index af4f98381..6bb6c0da5 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -24,6 +24,7 @@ import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:aves/widgets/filter_grids/common/section_layout.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -33,30 +34,30 @@ import 'package:tuple/tuple.dart'; typedef QueryTest = Iterable> Function(Iterable> filters, String query); class FilterGridPage extends StatelessWidget { - final String settingsRouteKey; + final String? settingsRouteKey; final Widget appBar; final double appBarHeight; final Map>> filterSections; final ChipSortFactor sortFactor; final bool showHeaders; final ValueNotifier queryNotifier; - final QueryTest applyQuery; + final QueryTest? applyQuery; final Widget Function() emptyBuilder; final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final OffsetFilterCallback? onLongPress; const FilterGridPage({ - Key key, + Key? key, this.settingsRouteKey, - @required this.appBar, + required this.appBar, this.appBarHeight = kToolbarHeight, - @required this.filterSections, - @required this.sortFactor, - @required this.showHeaders, - @required this.queryNotifier, + required this.filterSections, + required this.sortFactor, + required this.showHeaders, + required this.queryNotifier, this.applyQuery, - @required this.emptyBuilder, - @required this.onTap, + required this.emptyBuilder, + required this.onTap, this.onLongPress, }) : super(key: key); @@ -97,31 +98,31 @@ class FilterGridPage extends StatelessWidget { } class FilterGrid extends StatefulWidget { - final String settingsRouteKey; + final String? settingsRouteKey; final Widget appBar; final double appBarHeight; final Map>> filterSections; final ChipSortFactor sortFactor; final bool showHeaders; final ValueNotifier queryNotifier; - final QueryTest applyQuery; + final QueryTest? applyQuery; final Widget Function() emptyBuilder; final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final OffsetFilterCallback? onLongPress; const FilterGrid({ - Key key, - @required this.settingsRouteKey, - @required this.appBar, - @required this.appBarHeight, - @required this.filterSections, - @required this.sortFactor, - @required this.showHeaders, - @required this.queryNotifier, - @required this.applyQuery, - @required this.emptyBuilder, - @required this.onTap, - @required this.onLongPress, + Key? key, + required this.settingsRouteKey, + required this.appBar, + required this.appBarHeight, + required this.filterSections, + required this.sortFactor, + required this.showHeaders, + required this.queryNotifier, + required this.applyQuery, + required this.emptyBuilder, + required this.onTap, + required this.onLongPress, }) : super(key: key); @override @@ -129,18 +130,18 @@ class FilterGrid extends StatefulWidget { } class _FilterGridState extends State> { - TileExtentController _tileExtentController; + TileExtentController? _tileExtentController; @override Widget build(BuildContext context) { _tileExtentController ??= TileExtentController( - settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName, + settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName!, columnCountDefault: 2, extentMin: 60, spacing: 8, ); return TileExtentControllerProvider( - controller: _tileExtentController, + controller: _tileExtentController!, child: _FilterGridContent( appBar: widget.appBar, appBarHeight: widget.appBarHeight, @@ -164,24 +165,24 @@ class _FilterGridContent extends StatelessWidget { final bool showHeaders; final ValueNotifier queryNotifier; final Widget Function() emptyBuilder; - final QueryTest applyQuery; + final QueryTest? applyQuery; final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final OffsetFilterCallback? onLongPress; final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); _FilterGridContent({ - Key key, - @required this.appBar, - @required double appBarHeight, - @required this.filterSections, - @required this.sortFactor, - @required this.showHeaders, - @required this.queryNotifier, - @required this.applyQuery, - @required this.emptyBuilder, - @required this.onTap, - @required this.onLongPress, + Key? key, + required this.appBar, + required double appBarHeight, + required this.filterSections, + required this.sortFactor, + required this.showHeaders, + required this.queryNotifier, + required this.applyQuery, + required this.emptyBuilder, + required this.onTap, + required this.onLongPress, }) : super(key: key) { _appBarHeightNotifier.value = appBarHeight; } @@ -197,7 +198,7 @@ class _FilterGridContent extends StatelessWidget { } else { visibleFilterSections = {}; filterSections.forEach((sectionKey, sectionFilters) { - final visibleFilters = applyQuery(sectionFilters, query); + final visibleFilters = applyQuery!(sectionFilters, query); if (visibleFilters.isNotEmpty) { visibleFilterSections[sectionKey] = visibleFilters.toList(); } @@ -246,7 +247,7 @@ class _FilterGridContent extends StatelessWidget { visibleFilterSections: visibleFilterSections, sortFactor: sortFactor, emptyBuilder: emptyBuilder, - scrollController: PrimaryScrollController.of(context), + scrollController: PrimaryScrollController.of(context)!, ), ); }); @@ -267,12 +268,12 @@ class _FilterSectionedContent extends StatefulWidget final ScrollController scrollController; const _FilterSectionedContent({ - @required this.appBar, - @required this.appBarHeightNotifier, - @required this.visibleFilterSections, - @required this.sortFactor, - @required this.emptyBuilder, - @required this.scrollController, + required this.appBar, + required this.appBarHeightNotifier, + required this.visibleFilterSections, + required this.sortFactor, + required this.emptyBuilder, + required this.scrollController, }); @override @@ -295,7 +296,7 @@ class _FilterSectionedContentState extends State<_Fi @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _checkInitHighlight()); + WidgetsBinding.instance!.addPostFrameCallback((_) => _checkInitHighlight()); } @override @@ -324,19 +325,21 @@ class _FilterSectionedContentState extends State<_Fi final highlightInfo = context.read(); final filter = highlightInfo.clear(); if (filter is T) { - final gridItem = visibleFilterSections.values.expand((list) => list).firstWhere((gridItem) => gridItem.filter == filter, orElse: () => null); + final gridItem = visibleFilterSections.values.expand((list) => list).firstWhereOrNull((gridItem) => gridItem.filter == filter); if (gridItem != null) { await Future.delayed(Durations.highlightScrollInitDelay); final sectionedListLayout = context.read>>(); final tileRect = sectionedListLayout.getTileRect(gridItem); - await _scrollToItem(tileRect); - highlightInfo.set(filter); + if (tileRect != null) { + await _scrollToItem(tileRect); + highlightInfo.set(filter); + } } } } Future _scrollToItem(Rect tileRect) async { - final scrollableContext = _scrollableKey.currentContext; + final scrollableContext = _scrollableKey.currentContext!; final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height; // most of the time the app bar will be scrolled away after scaling, @@ -360,9 +363,9 @@ class _FilterScaler extends StatelessWidget { final Widget child; const _FilterScaler({ - @required this.scrollableKey, - @required this.appBarHeightNotifier, - @required this.child, + required this.scrollableKey, + required this.appBarHeightNotifier, + required this.child, }); @override @@ -409,12 +412,12 @@ class _FilterScrollView extends StatelessWidget { final ScrollController scrollController; const _FilterScrollView({ - @required this.scrollableKey, - @required this.appBar, - @required this.appBarHeightNotifier, - @required this.sortFactor, - @required this.emptyBuilder, - @required this.scrollController, + required this.scrollableKey, + required this.appBar, + required this.appBarHeightNotifier, + required this.sortFactor, + required this.emptyBuilder, + required this.scrollController, }); @override diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index 4c8a29fe6..2215d696a 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -13,6 +13,7 @@ import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; @@ -36,16 +37,16 @@ class FilterNavigationPage extends StatelessWidget { final Widget Function() emptyBuilder; const FilterNavigationPage({ - @required this.source, - @required this.title, - @required this.sortFactor, + required this.source, + required this.title, + required this.sortFactor, this.groupable = false, this.showHeaders = false, - @required this.chipSetActionDelegate, - @required this.chipActionDelegate, - @required this.chipActionsBuilder, - @required this.filterSections, - @required this.emptyBuilder, + required this.chipSetActionDelegate, + required this.chipActionDelegate, + required this.chipActionsBuilder, + required this.filterSections, + required this.emptyBuilder, }); @override @@ -72,7 +73,7 @@ class FilterNavigationPage extends StatelessWidget { emptyBuilder: () => ValueListenableBuilder( valueListenable: source.stateNotifier, builder: (context, sourceState, child) { - return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); + return sourceState != SourceState.loading ? emptyBuilder() : SizedBox.shrink(); }, ), onTap: (filter) => Navigator.push( @@ -85,16 +86,16 @@ class FilterNavigationPage extends StatelessWidget { )), ), ), - onLongPress: isMainMode ? _showMenu : null, + onLongPress: isMainMode ? _showMenu as OffsetFilterCallback : null, ); } - void _showMenu(BuildContext context, T filter, Offset tapPosition) async { - final RenderBox overlay = Overlay.of(context).context.findRenderObject(); + void _showMenu(BuildContext context, T filter, Offset? tapPosition) async { + final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox; final touchArea = Size(40, 40); final selectedAction = await showMenu( context: context, - position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), + position: RelativeRect.fromRect((tapPosition ?? Offset.zero) & touchArea, Offset.zero & overlay.size), items: chipActionsBuilder(filter) .map((action) => PopupMenuItem( value: action, @@ -155,7 +156,7 @@ class FilterNavigationPage extends StatelessWidget { } static int compareFiltersByEntryCount(MapEntry a, MapEntry b) { - final c = b.value.compareTo(a.value) ?? -1; + final c = b.value.compareTo(a.value); return c != 0 ? c : a.key.compareTo(b.key); } @@ -164,14 +165,14 @@ class FilterNavigationPage extends StatelessWidget { } static Iterable> sort(ChipSortFactor sortFactor, CollectionSource source, Set filters) { - Iterable> toGridItem(CollectionSource source, Iterable filters) { + Iterable> toGridItem(CollectionSource source, Set filters) { return filters.map((filter) => FilterGridItem( filter, source.recentEntry(filter), )); } - Iterable> allMapEntries; + Iterable> allMapEntries = {}; switch (sortFactor) { case ChipSortFactor.name: allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByName); diff --git a/lib/widgets/filter_grids/common/overlay.dart b/lib/widgets/filter_grids/common/overlay.dart index 01914443a..6b3cd556b 100644 --- a/lib/widgets/filter_grids/common/overlay.dart +++ b/lib/widgets/filter_grids/common/overlay.dart @@ -12,10 +12,10 @@ class ChipHighlightOverlay extends StatefulWidget { final BorderRadius borderRadius; const ChipHighlightOverlay({ - Key key, - @required this.filter, - @required this.extent, - @required this.borderRadius, + Key? key, + required this.filter, + required this.extent, + required this.borderRadius, }) : super(key: key); @override diff --git a/lib/widgets/filter_grids/common/section_header.dart b/lib/widgets/filter_grids/common/section_header.dart index 90dab210f..b0038fa60 100644 --- a/lib/widgets/filter_grids/common/section_header.dart +++ b/lib/widgets/filter_grids/common/section_header.dart @@ -6,8 +6,8 @@ class FilterChipSectionHeader extends StatelessWidget { final ChipSectionKey sectionKey; const FilterChipSectionHeader({ - Key key, - @required this.sectionKey, + Key? key, + required this.sectionKey, }) : super(key: key); @override diff --git a/lib/widgets/filter_grids/common/section_keys.dart b/lib/widgets/filter_grids/common/section_keys.dart index 2ba69df8a..94231a438 100644 --- a/lib/widgets/filter_grids/common/section_keys.dart +++ b/lib/widgets/filter_grids/common/section_keys.dart @@ -12,7 +12,7 @@ class ChipSectionKey extends SectionKey { this.title = '', }); - Widget get leading => null; + Widget? get leading => null; @override bool operator ==(Object other) { @@ -58,7 +58,6 @@ extension ExtraAlbumImportance on AlbumImportance { case AlbumImportance.regular: return context.l10n.albumTierRegular; } - return null; } IconData getIcon() { @@ -72,15 +71,14 @@ extension ExtraAlbumImportance on AlbumImportance { case AlbumImportance.regular: return AIcons.album; } - return null; } } class StorageVolumeSectionKey extends ChipSectionKey { - final StorageVolume volume; + final StorageVolume? volume; StorageVolumeSectionKey(BuildContext context, this.volume) : super(title: volume?.getDescription(context) ?? context.l10n.sectionUnknown); @override - Widget get leading => (volume?.isRemovable ?? false) ? Icon(AIcons.removableStorage) : null; + Widget? get leading => (volume?.isRemovable ?? false) ? Icon(AIcons.removableStorage) : null; } diff --git a/lib/widgets/filter_grids/common/section_layout.dart b/lib/widgets/filter_grids/common/section_layout.dart index 154bcc401..aab18021b 100644 --- a/lib/widgets/filter_grids/common/section_layout.dart +++ b/lib/widgets/filter_grids/common/section_layout.dart @@ -2,20 +2,20 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/filter_grids/common/section_header.dart'; -import 'package:flutter/foundation.dart'; +import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:flutter/material.dart'; class SectionedFilterListLayoutProvider extends SectionedListLayoutProvider> { const SectionedFilterListLayoutProvider({ - @required this.sections, - @required this.showHeaders, - @required double scrollableWidth, - @required int columnCount, + required this.sections, + required this.showHeaders, + required double scrollableWidth, + required int columnCount, double spacing = 0, - @required double tileExtent, - @required Widget Function(FilterGridItem gridItem) tileBuilder, - @required Duration tileAnimationDelay, - @required Widget child, + required double tileExtent, + required Widget Function(FilterGridItem gridItem) tileBuilder, + required Duration tileAnimationDelay, + required Widget child, }) : super( scrollableWidth: scrollableWidth, columnCount: columnCount, @@ -40,7 +40,7 @@ class SectionedFilterListLayoutProvider extends Sect @override Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent) { return FilterChipSectionHeader( - sectionKey: sectionKey, + sectionKey: sectionKey as ChipSectionKey, ); } } diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 3b1c56f1b..8649853e5 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -26,7 +26,7 @@ class HomePage extends StatefulWidget { static const routeName = '/'; // untyped map as it is coming from the platform - final Map intentData; + final Map? intentData; const HomePage({this.intentData}); @@ -35,9 +35,9 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - AvesEntry _viewerEntry; - String _shortcutRouteName; - List _shortcutFilters; + AvesEntry? _viewerEntry; + String? _shortcutRouteName; + List? _shortcutFilters; static const allowedShortcutRoutes = [CollectionPage.routeName, AlbumListPage.routeName, SearchPage.routeName]; @@ -45,7 +45,7 @@ class _HomePageState extends State { void initState() { super.initState(); _setup(); - imageCache.maximumSizeBytes = 512 * (1 << 20); + imageCache!.maximumSizeBytes = 512 * (1 << 20); settings.keepScreenOn.apply(); } @@ -68,7 +68,7 @@ class _HomePageState extends State { var appMode = AppMode.main; final intentData = widget.intentData ?? await ViewerService.getIntentData(); - if (intentData?.isNotEmpty == true) { + if (intentData.isNotEmpty) { final action = intentData['action']; switch (action) { case 'view': @@ -84,7 +84,7 @@ class _HomePageState extends State { appMode = AppMode.pickExternal; // TODO TLAD apply pick mimetype(s) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) - String pickMimeTypes = intentData['mimeType']; + String? pickMimeTypes = intentData['mimeType']; debugPrint('pick mimeType=$pickMimeTypes'); break; default: @@ -115,7 +115,7 @@ class _HomePageState extends State { )); } - Future _initViewerEntry({@required String uri, @required String mimeType}) async { + Future _initViewerEntry({required String uri, required String? mimeType}) async { final entry = await imageFileService.getEntry(uri, mimeType); if (entry != null) { // cataloguing is essential for coordinates and video rotation @@ -129,13 +129,13 @@ class _HomePageState extends State { return DirectMaterialPageRoute( settings: RouteSettings(name: EntryViewerPage.routeName), builder: (_) => EntryViewerPage( - initialEntry: _viewerEntry, + initialEntry: _viewerEntry!, ), ); } String routeName; - Iterable filters; + Iterable? filters; if (appMode == AppMode.pickExternal) { routeName = CollectionPage.routeName; } else { diff --git a/lib/widgets/search/expandable_filter_row.dart b/lib/widgets/search/expandable_filter_row.dart index 07eba3dc2..e6d6a443e 100644 --- a/lib/widgets/search/expandable_filter_row.dart +++ b/lib/widgets/search/expandable_filter_row.dart @@ -6,18 +6,18 @@ import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; class ExpandableFilterRow extends StatelessWidget { - final String title; + final String? title; final Iterable filters; - final ValueNotifier expandedNotifier; - final HeroType Function(CollectionFilter filter) heroTypeBuilder; + final ValueNotifier expandedNotifier; + final HeroType Function(CollectionFilter filter)? heroTypeBuilder; final FilterCallback onTap; const ExpandableFilterRow({ this.title, - @required this.filters, - @required this.expandedNotifier, + required this.filters, + required this.expandedNotifier, this.heroTypeBuilder, - @required this.onTap, + required this.onTap, }); static const double horizontalPadding = 8; @@ -27,18 +27,18 @@ class ExpandableFilterRow extends StatelessWidget { Widget build(BuildContext context) { if (filters.isEmpty) return SizedBox.shrink(); - final hasTitle = title != null && title.isNotEmpty; + final hasTitle = title != null && title!.isNotEmpty; final isExpanded = hasTitle && expandedNotifier.value == title; - Widget titleRow; + Widget? titleRow; if (hasTitle) { titleRow = Padding( padding: EdgeInsets.all(16), child: Row( children: [ Text( - title, + title!, style: Constants.titleTextStyle, ), const Spacer(), @@ -76,7 +76,7 @@ class ExpandableFilterRow extends StatelessWidget { physics: BouncingScrollPhysics(), padding: EdgeInsets.symmetric(horizontal: horizontalPadding), itemBuilder: (context, index) { - return index < filterList.length ? _buildFilterChip(filterList[index]) : null; + return index < filterList.length ? _buildFilterChip(filterList[index]) : SizedBox(); }, separatorBuilder: (context, index) => SizedBox(width: 8), itemCount: filterList.length, diff --git a/lib/widgets/search/search_button.dart b/lib/widgets/search/search_button.dart index 3e377a670..36990e5df 100644 --- a/lib/widgets/search/search_button.dart +++ b/lib/widgets/search/search_button.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; class CollectionSearchButton extends StatelessWidget { final CollectionSource source; - final CollectionLens parentCollection; + final CollectionLens? parentCollection; const CollectionSearchButton(this.source, {this.parentCollection}); diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index f0f9adfc1..002495e92 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -25,8 +25,8 @@ import 'package:provider/provider.dart'; class CollectionSearchDelegate { final CollectionSource source; - final CollectionLens parentCollection; - final ValueNotifier expandedSectionNotifier = ValueNotifier(null); + final CollectionLens? parentCollection; + final ValueNotifier expandedSectionNotifier = ValueNotifier(null); static const searchHistoryCount = 10; static final typeFilters = [ @@ -41,7 +41,7 @@ class CollectionSearchDelegate { MimeFilter(MimeTypes.svg), ]; - CollectionSearchDelegate({@required this.source, this.parentCollection}); + CollectionSearchDelegate({required this.source, this.parentCollection}); Widget buildLeading(BuildContext context) { return Navigator.canPop(context) @@ -76,7 +76,7 @@ class CollectionSearchDelegate { final upQuery = query.trim().toUpperCase(); bool containQuery(String s) => s.toUpperCase().contains(upQuery); return SafeArea( - child: ValueListenableBuilder( + child: ValueListenableBuilder( valueListenable: expandedSectionNotifier, builder: (context, expandedSection, child) { final queryFilter = _buildQueryFilter(false); @@ -100,7 +100,7 @@ class CollectionSearchDelegate { filters: [ queryFilter, ...visibleTypeFilters, - ].where((f) => f != null && containQuery(f.getLabel(context))).toList(), + ].where((f) => f != null && containQuery(f.getLabel(context))).cast().toList(), // usually perform hero animation only on tapped chips, // but we also need to animate the query chip when it is selected by submitting the search query heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap, @@ -119,7 +119,7 @@ class CollectionSearchDelegate { album, source.getAlbumDisplayName(context, album), )) - .where((filter) => containQuery(filter.displayName)) + .where((filter) => containQuery(filter.displayName ?? filter.album)) .toList() ..sort(); return _buildFilterRow( @@ -174,10 +174,10 @@ class CollectionSearchDelegate { } Widget _buildFilterRow({ - @required BuildContext context, - String title, - @required List filters, - HeroType Function(CollectionFilter filter) heroTypeBuilder, + required BuildContext context, + String? title, + required List filters, + HeroType Function(CollectionFilter filter)? heroTypeBuilder, }) { return ExpandableFilterRow( title: title, @@ -189,7 +189,7 @@ class CollectionSearchDelegate { } Widget buildResults(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance!.addPostFrameCallback((_) { // `buildResults` is called in the build phase, // so we post the call that will filter the collection // and possibly trigger a rebuild here @@ -198,12 +198,17 @@ class CollectionSearchDelegate { return SizedBox.shrink(); } - QueryFilter _buildQueryFilter(bool colorful) { + QueryFilter? _buildQueryFilter(bool colorful) { final cleanQuery = query.trim(); return cleanQuery.isNotEmpty ? QueryFilter(cleanQuery, colorful: colorful) : null; } - void _select(BuildContext context, CollectionFilter filter) { + void _select(BuildContext context, CollectionFilter? filter) { + if (filter == null) { + _goBack(context); + return; + } + if (settings.saveSearchHistory) { final history = settings.searchHistory ..remove(filter) @@ -218,11 +223,11 @@ class CollectionSearchDelegate { } void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) { - parentCollection.addFilter(filter); + parentCollection!.addFilter(filter); // we post closing the search page after applying the filter selection // so that hero animation target is ready in the `FilterBar`, // even when the target is a child of an `AnimatedList` - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance!.addPostFrameCallback((_) { _goBack(context); }); } @@ -261,13 +266,13 @@ class CollectionSearchDelegate { void showSuggestions(BuildContext context) { assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.'); - focusNode.requestFocus(); + focusNode!.requestFocus(); currentBody = SearchBody.suggestions; } Animation get transitionAnimation => proxyAnimation; - FocusNode focusNode; + FocusNode? focusNode; final TextEditingController queryTextController = TextEditingController(); @@ -276,19 +281,18 @@ class CollectionSearchDelegate { String get query => queryTextController.text; set query(String value) { - assert(query != null); queryTextController.text = value; } - final ValueNotifier currentBodyNotifier = ValueNotifier(null); + final ValueNotifier currentBodyNotifier = ValueNotifier(null); - SearchBody get currentBody => currentBodyNotifier.value; + SearchBody? get currentBody => currentBodyNotifier.value; - set currentBody(SearchBody value) { + set currentBody(SearchBody? value) { currentBodyNotifier.value = value; } - SearchPageRoute route; + SearchPageRoute? route; } // adapted from `SearchDelegate` @@ -297,9 +301,8 @@ enum SearchBody { suggestions, results } // adapted from `SearchDelegate` class SearchPageRoute extends PageRoute { SearchPageRoute({ - @required this.delegate, - }) : assert(delegate != null), - super(settings: RouteSettings(name: SearchPage.routeName)) { + required this.delegate, + }) : super(settings: RouteSettings(name: SearchPage.routeName)) { assert( delegate.route == null, 'The ${delegate.runtimeType} instance is currently used by another active ' @@ -312,10 +315,10 @@ class SearchPageRoute extends PageRoute { final CollectionSearchDelegate delegate; @override - Color get barrierColor => null; + Color? get barrierColor => null; @override - String get barrierLabel => null; + String? get barrierLabel => null; @override Duration get transitionDuration => const Duration(milliseconds: 300); @@ -356,7 +359,7 @@ class SearchPageRoute extends PageRoute { } @override - void didComplete(T result) { + void didComplete(T? result) { super.didComplete(result); assert(delegate.route == this); delegate.route = null; diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/search/search_page.dart index df0719d8b..b6437ebd0 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/search/search_page.dart @@ -12,8 +12,8 @@ class SearchPage extends StatefulWidget { final Animation animation; const SearchPage({ - @required this.delegate, - @required this.animation, + required this.delegate, + required this.animation, }); @override @@ -89,7 +89,7 @@ class _SearchPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - Widget body; + Widget? body; switch (widget.delegate.currentBody) { case SearchBody.suggestions: body = KeyedSubtree( @@ -103,6 +103,8 @@ class _SearchPageState extends State { child: widget.delegate.buildResults(context), ); break; + case null: + break; } return Scaffold( appBar: AppBar( diff --git a/lib/widgets/settings/access_grants.dart b/lib/widgets/settings/access_grants.dart index ec0912290..0b6c075cc 100644 --- a/lib/widgets/settings/access_grants.dart +++ b/lib/widgets/settings/access_grants.dart @@ -30,8 +30,8 @@ class StorageAccessPage extends StatefulWidget { } class _StorageAccessPageState extends State { - Future> _pathLoader; - List _lastPaths; + late Future> _pathLoader; + List? _lastPaths; @override void initState() { @@ -72,15 +72,15 @@ class _StorageAccessPageState extends State { if (snapshot.connectionState != ConnectionState.done && _lastPaths == null) { return SizedBox.shrink(); } - _lastPaths = snapshot.data..sort(); - if (_lastPaths.isEmpty) { + _lastPaths = snapshot.data!..sort(); + if (_lastPaths!.isEmpty) { return EmptyContent( text: context.l10n.settingsStorageAccessEmpty, ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: _lastPaths + children: _lastPaths! .map((path) => ListTile( title: Text(path), dense: true, diff --git a/lib/widgets/settings/entry_background.dart b/lib/widgets/settings/entry_background.dart index b0f1995f0..6db16242e 100644 --- a/lib/widgets/settings/entry_background.dart +++ b/lib/widgets/settings/entry_background.dart @@ -9,8 +9,8 @@ class EntryBackgroundSelector extends StatefulWidget { final ValueSetter setter; const EntryBackgroundSelector({ - @required this.getter, - @required this.setter, + required this.getter, + required this.setter, }); @override @@ -25,8 +25,10 @@ class _EntryBackgroundSelectorState extends State { items: _buildItems(context), value: widget.getter(), onChanged: (selected) { - widget.setter(selected); - setState(() {}); + if (selected != null) { + widget.setter(selected); + setState(() {}); + } }, ), ); @@ -40,7 +42,7 @@ class _EntryBackgroundSelectorState extends State { EntryBackground.checkered, EntryBackground.transparent, ].map((selected) { - Widget child; + Widget? child; switch (selected) { case EntryBackground.transparent: child = Icon( diff --git a/lib/widgets/settings/quick_actions/available_actions.dart b/lib/widgets/settings/quick_actions/available_actions.dart index e73891cb2..204969d02 100644 --- a/lib/widgets/settings/quick_actions/available_actions.dart +++ b/lib/widgets/settings/quick_actions/available_actions.dart @@ -7,17 +7,17 @@ class AvailableActionPanel extends StatelessWidget { final List quickActions; final Listenable quickActionsChangeNotifier; final ValueNotifier panelHighlight; - final ValueNotifier draggedQuickAction; - final ValueNotifier draggedAvailableAction; - final bool Function(EntryAction action) removeQuickAction; + final ValueNotifier draggedQuickAction; + final ValueNotifier draggedAvailableAction; + final bool Function(EntryAction? action) removeQuickAction; const AvailableActionPanel({ - @required this.quickActions, - @required this.quickActionsChangeNotifier, - @required this.panelHighlight, - @required this.draggedQuickAction, - @required this.draggedAvailableAction, - @required this.removeQuickAction, + required this.quickActions, + required this.quickActionsChangeNotifier, + required this.panelHighlight, + required this.draggedQuickAction, + required this.draggedAvailableAction, + required this.removeQuickAction, }); static const allActions = [ @@ -95,9 +95,9 @@ class AvailableActionPanel extends StatelessWidget { child: child, ); - void _setDraggedQuickAction(EntryAction action) => draggedQuickAction.value = action; + void _setDraggedQuickAction(EntryAction? action) => draggedQuickAction.value = action; - void _setDraggedAvailableAction(EntryAction action) => draggedAvailableAction.value = action; + void _setDraggedAvailableAction(EntryAction? action) => draggedAvailableAction.value = action; void _setPanelHighlight(bool flag) => panelHighlight.value = flag; } diff --git a/lib/widgets/settings/quick_actions/common.dart b/lib/widgets/settings/quick_actions/common.dart index 7d98968ed..8faee0e46 100644 --- a/lib/widgets/settings/quick_actions/common.dart +++ b/lib/widgets/settings/quick_actions/common.dart @@ -9,7 +9,7 @@ class ActionPanel extends StatelessWidget { const ActionPanel({ this.highlight = false, - @required this.child, + required this.child, }); @override @@ -39,7 +39,7 @@ class ActionButton extends StatelessWidget { final bool enabled, showCaption; const ActionButton({ - @required this.action, + required this.action, this.enabled = true, this.showCaption = true, }); @@ -65,7 +65,7 @@ class ActionButton extends StatelessWidget { SizedBox(height: padding), Text( action.getText(context), - style: enabled ? textStyle : textStyle.copyWith(color: textStyle.color.withOpacity(.2)), + style: enabled ? textStyle : textStyle!.copyWith(color: textStyle.color!.withOpacity(.2)), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2, @@ -82,7 +82,7 @@ class DraggedPlaceholder extends StatelessWidget { final Widget child; const DraggedPlaceholder({ - @required this.child, + required this.child, }); @override diff --git a/lib/widgets/settings/quick_actions/editor.dart b/lib/widgets/settings/quick_actions/editor.dart index 9109dabd4..b62e1083b 100644 --- a/lib/widgets/settings/quick_actions/editor.dart +++ b/lib/widgets/settings/quick_actions/editor.dart @@ -42,10 +42,10 @@ class QuickActionEditorPage extends StatefulWidget { class _QuickActionEditorPageState extends State { final GlobalKey _animatedListKey = GlobalKey(debugLabel: 'quick-actions-animated-list'); - Timer _targetLeavingTimer; - List _quickActions; - final ValueNotifier _draggedQuickAction = ValueNotifier(null); - final ValueNotifier _draggedAvailableAction = ValueNotifier(null); + Timer? _targetLeavingTimer; + late List _quickActions; + final ValueNotifier _draggedQuickAction = ValueNotifier(null); + final ValueNotifier _draggedAvailableAction = ValueNotifier(null); final ValueNotifier _quickActionHighlight = ValueNotifier(false); final ValueNotifier _availableActionHighlight = ValueNotifier(false); final AChangeNotifier _quickActionsChangeNotifier = AChangeNotifier(); @@ -132,7 +132,7 @@ class _QuickActionEditorPageState extends State { valueListenable: _quickActionHighlight, builder: (context, highlight, child) => ActionPanel( highlight: highlight, - child: child, + child: child!, ), child: Container( height: OverlayButton.getSize(context) + quickActionVerticalPadding * 2, @@ -161,7 +161,7 @@ class _QuickActionEditorPageState extends State { shrinkWrap: true, padding: EdgeInsets.zero, itemBuilder: (context, index, animation) { - if (index >= _quickActions.length) return null; + if (index >= _quickActions.length) return SizedBox(); final action = _quickActions[index]; return QuickActionButton( placement: QuickActionPlacement.action, @@ -203,7 +203,7 @@ class _QuickActionEditorPageState extends State { valueListenable: _availableActionHighlight, builder: (context, highlight, child) => ActionPanel( highlight: highlight, - child: child, + child: child!, ), child: AvailableActionPanel( quickActions: _quickActions, @@ -224,14 +224,13 @@ class _QuickActionEditorPageState extends State { void _stopLeavingTimer() => _targetLeavingTimer?.cancel(); - bool _insertQuickAction(EntryAction action, QuickActionPlacement placement, EntryAction overAction) { - if (action == null) return false; + bool _insertQuickAction(EntryAction action, QuickActionPlacement placement, EntryAction? overAction) { _stopLeavingTimer(); if (_reordering) return false; final currentIndex = _quickActions.indexOf(action); final contained = currentIndex != -1; - int targetIndex; + int? targetIndex; switch (placement) { case QuickActionPlacement.header: targetIndex = 0; @@ -240,7 +239,7 @@ class _QuickActionEditorPageState extends State { targetIndex = _quickActions.length - (contained ? 1 : 0); break; case QuickActionPlacement.action: - targetIndex = _quickActions.indexOf(overAction); + targetIndex = _quickActions.indexOf(overAction!); break; } if (currentIndex == targetIndex) return false; @@ -248,7 +247,7 @@ class _QuickActionEditorPageState extends State { _reordering = true; _removeQuickAction(action); _quickActions.insert(targetIndex, action); - _animatedListKey.currentState.insertItem( + _animatedListKey.currentState!.insertItem( targetIndex, duration: Durations.quickActionListAnimation, ); @@ -257,12 +256,12 @@ class _QuickActionEditorPageState extends State { return true; } - bool _removeQuickAction(EntryAction action) { - if (!_quickActions.contains(action)) return false; + bool _removeQuickAction(EntryAction? action) { + if (action == null || !_quickActions.contains(action)) return false; final index = _quickActions.indexOf(action); _quickActions.removeAt(index); - _animatedListKey.currentState.removeItem( + _animatedListKey.currentState!.removeItem( index, (context, animation) => DraggedPlaceholder(child: _buildQuickActionButton(action, animation)), duration: Durations.quickActionListAnimation, @@ -295,9 +294,9 @@ class _QuickActionEditorPageState extends State { builder: (context, child) { final dragged = _draggedQuickAction.value == action || _draggedAvailableAction.value == action; if (dragged) { - child = DraggedPlaceholder(child: child); + child = DraggedPlaceholder(child: child!); } - return child; + return child!; }, child: child, ); diff --git a/lib/widgets/settings/quick_actions/quick_actions.dart b/lib/widgets/settings/quick_actions/quick_actions.dart index c0c6aa2d7..e7e8dd28e 100644 --- a/lib/widgets/settings/quick_actions/quick_actions.dart +++ b/lib/widgets/settings/quick_actions/quick_actions.dart @@ -7,24 +7,24 @@ enum QuickActionPlacement { header, action, footer } class QuickActionButton extends StatelessWidget { final QuickActionPlacement placement; - final EntryAction action; + final EntryAction? action; final ValueNotifier panelHighlight; - final ValueNotifier draggedQuickAction; - final ValueNotifier draggedAvailableAction; - final bool Function(EntryAction action, QuickActionPlacement placement, EntryAction overAction) insertAction; + final ValueNotifier draggedQuickAction; + final ValueNotifier draggedAvailableAction; + final bool Function(EntryAction action, QuickActionPlacement placement, EntryAction? overAction) insertAction; final bool Function(EntryAction action) removeAction; final VoidCallback onTargetLeave; - final Widget child; + final Widget? child; const QuickActionButton({ - @required this.placement, + required this.placement, this.action, - @required this.panelHighlight, - @required this.draggedQuickAction, - @required this.draggedAvailableAction, - @required this.insertAction, - @required this.removeAction, - @required this.onTargetLeave, + required this.panelHighlight, + required this.draggedQuickAction, + required this.draggedAvailableAction, + required this.insertAction, + required this.removeAction, + required this.onTargetLeave, this.child, }); @@ -33,30 +33,30 @@ class QuickActionButton extends StatelessWidget { var child = this.child; child = _buildDragTarget(child); if (action != null) { - child = _buildDraggable(child); + child = _buildDraggable(child, action!); } return child; } - DragTarget _buildDragTarget(Widget child) { + DragTarget _buildDragTarget(Widget? child) { return DragTarget( onWillAccept: (data) { if (draggedQuickAction.value != null) { - insertAction(draggedQuickAction.value, placement, action); + insertAction(draggedQuickAction.value!, placement, action); } if (draggedAvailableAction.value != null) { - insertAction(draggedAvailableAction.value, placement, action); + insertAction(draggedAvailableAction.value!, placement, action); _setPanelHighlight(true); } return true; }, onAcceptWithDetails: (details) => _setPanelHighlight(false), onLeave: (data) => onTargetLeave(), - builder: (context, accepted, rejected) => child, + builder: (context, accepted, rejected) => child ?? SizedBox(), ); } - Widget _buildDraggable(Widget child) => LongPressDraggable( + Widget _buildDraggable(Widget child, EntryAction action) => LongPressDraggable( data: action, maxSimultaneousDrags: 1, onDragStarted: () => _setDraggedQuickAction(action), @@ -74,7 +74,7 @@ class QuickActionButton extends StatelessWidget { child: child, ); - void _setDraggedQuickAction(EntryAction action) => draggedQuickAction.value = action; + void _setDraggedQuickAction(EntryAction? action) => draggedQuickAction.value = action; void _setPanelHighlight(bool flag) => panelHighlight.value = flag; } diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index d6d62725e..4a8487ae7 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -33,7 +33,7 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { - final ValueNotifier _expandedNotifier = ValueNotifier(null); + final ValueNotifier _expandedNotifier = ValueNotifier(null); @override Widget build(BuildContext context) { @@ -133,7 +133,7 @@ class _SettingsPageState extends State { } Widget _buildThumbnailsSection(BuildContext context) { - final iconSize = IconTheme.of(context).size * MediaQuery.of(context).textScaleFactor; + final iconSize = IconTheme.of(context).size! * MediaQuery.of(context).textScaleFactor; double opacityFor(bool enabled) => enabled ? 1 : .2; return AvesExpansionTile( leading: _buildLeading(AIcons.grid, stringToColor('Thumbnails')), diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 9cdfaac47..f062f3e57 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -14,10 +14,10 @@ class FilterTable extends StatelessWidget { final FilterCallback onFilterSelection; const FilterTable({ - @required this.totalEntryCount, - @required this.entryCountMap, - @required this.filterBuilder, - @required this.onFilterSelection, + required this.totalEntryCount, + required this.entryCountMap, + required this.filterBuilder, + required this.onFilterSelection, }); static const chipWidth = AvesFilterChip.maxChipWidth; diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index a411a22f1..d6f27be3a 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -16,6 +16,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/stats/filter_table.dart'; + +// ignore: import_of_legacy_library_into_null_safe import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -27,20 +29,20 @@ class StatsPage extends StatelessWidget { static const routeName = '/collection/stats'; final CollectionSource source; - final CollectionLens parentCollection; + final CollectionLens? parentCollection; final Map entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {}; - Set get entries => parentCollection?.sortedEntries?.toSet() ?? source.visibleEntries; + Set get entries => parentCollection?.sortedEntries.toSet() ?? source.visibleEntries; static const mimeDonutMinWidth = 124.0; StatsPage({ - @required this.source, + required this.source, this.parentCollection, - }) : assert(source != null) { + }) { entries.forEach((entry) { if (entry.hasAddress) { - final address = entry.addressDetails; + final address = entry.addressDetails!; var country = address.countryName; if (country != null && country.isNotEmpty) { country += '${LocationFilter.locationSeparator}${address.countryCode}'; @@ -129,7 +131,7 @@ class StatsPage extends StatelessWidget { ); } - Widget _buildMimeDonut(BuildContext context, String Function(num) label, Map byMimeTypes) { + Widget _buildMimeDonut(BuildContext context, String Function(int) label, Map byMimeTypes) { if (byMimeTypes.isEmpty) return SizedBox.shrink(); final sum = byMimeTypes.values.fold(0, (prev, v) => prev + v); @@ -257,11 +259,11 @@ class StatsPage extends StatelessWidget { } void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) { - parentCollection.addFilter(filter); + parentCollection!.addFilter(filter); // we post closing the search page after applying the filter selection // so that hero animation target is ready in the `FilterBar`, // even when the target is a child of an `AnimatedList` - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance!.addPostFrameCallback((_) { Navigator.pop(context); }); } @@ -287,8 +289,8 @@ class EntryByMimeDatum { final int entryCount; EntryByMimeDatum({ - @required this.mimeType, - @required this.entryCount, + required this.mimeType, + required this.entryCount, }) : displayText = MimeUtils.displayType(mimeType); Color get color => stringToColor(displayText); diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index e586c51d2..dba1a7e1d 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -2,22 +2,23 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/services/services.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class DbTab extends StatefulWidget { final AvesEntry entry; - const DbTab({@required this.entry}); + const DbTab({required this.entry}); @override _DbTabState createState() => _DbTabState(); } class _DbTabState extends State { - Future _dbDateLoader; - Future _dbEntryLoader; - Future _dbMetadataLoader; - Future _dbAddressLoader; + late Future _dbDateLoader; + late Future _dbEntryLoader; + late Future _dbMetadataLoader; + late Future _dbAddressLoader; AvesEntry get entry => widget.entry; @@ -29,10 +30,10 @@ class _DbTabState extends State { void _loadDatabase() { final contentId = entry.contentId; - _dbDateLoader = metadataDb.loadDates().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null)); - _dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null)); - _dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null)); - _dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null)); + _dbDateLoader = metadataDb.loadDates().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); + _dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); + _dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); + _dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); setState(() {}); } @@ -41,7 +42,7 @@ class _DbTabState extends State { return ListView( padding: EdgeInsets.all(16), children: [ - FutureBuilder( + FutureBuilder( future: _dbDateLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); @@ -60,7 +61,7 @@ class _DbTabState extends State { }, ), SizedBox(height: 16), - FutureBuilder( + FutureBuilder( future: _dbEntryLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); @@ -89,7 +90,7 @@ class _DbTabState extends State { }, ), SizedBox(height: 16), - FutureBuilder( + FutureBuilder( future: _dbMetadataLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); @@ -116,7 +117,7 @@ class _DbTabState extends State { }, ), SizedBox(height: 16), - FutureBuilder( + FutureBuilder( future: _dbAddressLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); diff --git a/lib/widgets/viewer/debug/metadata.dart b/lib/widgets/viewer/debug/metadata.dart index 63bb3b178..a36d6c515 100644 --- a/lib/widgets/viewer/debug/metadata.dart +++ b/lib/widgets/viewer/debug/metadata.dart @@ -12,14 +12,14 @@ import 'package:flutter/material.dart'; class MetadataTab extends StatefulWidget { final AvesEntry entry; - const MetadataTab({@required this.entry}); + const MetadataTab({required this.entry}); @override _MetadataTabState createState() => _MetadataTabState(); } class _MetadataTabState extends State { - Future _bitmapFactoryLoader, _contentResolverMetadataLoader, _exifInterfaceMetadataLoader, _mediaMetadataLoader, _metadataExtractorLoader, _tiffStructureLoader; + late Future _bitmapFactoryLoader, _contentResolverMetadataLoader, _exifInterfaceMetadataLoader, _mediaMetadataLoader, _metadataExtractorLoader, _tiffStructureLoader; // MediaStore timestamp keys static const secondTimestampKeys = ['date_added', 'date_modified', 'date_expires', 'isPlayed']; @@ -49,7 +49,7 @@ class _MetadataTabState extends State { final data = SplayTreeMap.of(snapshotData.map((k, v) { final key = k.toString(); var value = v?.toString() ?? 'null'; - if ([...secondTimestampKeys, ...millisecondTimestampKeys].contains(key) && v is num && v != 0) { + if ([...secondTimestampKeys, ...millisecondTimestampKeys].contains(key) && v is int && v != 0) { if (secondTimestampKeys.contains(key)) { v *= 1000; } @@ -62,24 +62,23 @@ class _MetadataTabState extends State { })); return AvesExpansionTile( title: title, - children: data.isNotEmpty - ? [ - Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: InfoRowGroup( - data, - maxValueLength: Constants.infoGroupMaxValueLength, - ), - ) - ] - : null, + children: [ + if (data.isNotEmpty) + Padding( + padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: InfoRowGroup( + data, + maxValueLength: Constants.infoGroupMaxValueLength, + ), + ) + ], ); } Widget builderFromSnapshot(BuildContext context, AsyncSnapshot snapshot, String title) { if (snapshot.hasError) return Text(snapshot.error.toString()); if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - return builderFromSnapshotData(context, snapshot.data, title); + return builderFromSnapshotData(context, snapshot.data!, title); } return ListView( @@ -113,7 +112,7 @@ class _MetadataTabState extends State { if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: snapshot.data.entries.map((kv) => builderFromSnapshotData(context, kv.value as Map, 'TIFF ${kv.key}')).toList(), + children: snapshot.data!.entries.map((kv) => builderFromSnapshotData(context, kv.value as Map, 'TIFF ${kv.key}')).toList(), ); }, ), diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index cd7300585..8eb2877ef 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -16,7 +16,7 @@ class ViewerDebugPage extends StatelessWidget { final AvesEntry entry; - const ViewerDebugPage({@required this.entry}); + const ViewerDebugPage({required this.entry}); @override Widget build(BuildContext context) { @@ -45,7 +45,7 @@ class ViewerDebugPage extends StatelessWidget { } Widget _buildEntryTabView() { - String toDateValue(int time, {int factor = 1}) { + String toDateValue(int? time, {int factor = 1}) { var value = '$time'; if (time != null && time > 0) { value += ' (${DateTime.fromMillisecondsSinceEpoch(time * factor)})'; diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index cf2ff7d53..a756bba08 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -26,12 +26,12 @@ import 'package:pedantic/pedantic.dart'; import 'package:provider/provider.dart'; class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { - final CollectionLens collection; + final CollectionLens? collection; final VoidCallback showInfo; EntryActionDelegate({ - @required this.collection, - @required this.showInfo, + required this.collection, + required this.showInfo, }); bool get hasCollection => collection != null; @@ -76,7 +76,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix }); break; case EntryAction.openMap: - AndroidAppService.openMap(entry.geoUri).then((success) { + AndroidAppService.openMap(entry.geoUri!).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; @@ -106,7 +106,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!success) showFeedback(context, context.l10n.genericFailureFeedback); } - Future _rotate(BuildContext context, AvesEntry entry, {@required bool clockwise}) async { + Future _rotate(BuildContext context, AvesEntry entry, {required bool clockwise}) async { if (!await checkStoragePermission(context, {entry})) return; final success = await entry.rotate(clockwise: clockwise); @@ -141,7 +141,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix showFeedback(context, context.l10n.genericFailureFeedback); } else { if (hasCollection) { - await collection.source.removeEntries({entry.uri}); + await collection!.source.removeEntries({entry.uri}); } EntryDeletedNotification(entry).dispatch(context); } @@ -171,11 +171,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final selection = {}; if (entry.isMultiPage) { final multiPageInfo = await metadataService.getMultiPageInfo(entry); - if (entry.isMotionPhoto) { - await multiPageInfo.extractMotionPhotoVideo(); - } - if (multiPageInfo.pageCount > 1) { - selection.addAll(multiPageInfo.exportEntries); + if (multiPageInfo != null) { + if (entry.isMotionPhoto) { + await multiPageInfo.extractMotionPhotoVideo(); + } + if (multiPageInfo.pageCount > 1) { + selection.addAll(multiPageInfo.exportEntries); + } } } else { selection.add(entry); diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index 96dcd75cc..69aa77926 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -15,10 +15,10 @@ class MultiEntryScroller extends StatefulWidget { final void Function(String uri) onViewDisposed; const MultiEntryScroller({ - this.collection, - this.pageController, - this.onPageChanged, - this.onViewDisposed, + required this.collection, + required this.pageController, + required this.onPageChanged, + required this.onViewDisposed, }); @override @@ -43,15 +43,15 @@ class _MultiEntryScrollerState extends State with AutomaticK itemBuilder: (context, index) { final entry = entries[index]; - Widget child; + Widget? child; if (entry.isMultiPage) { final multiPageController = context.read().getController(entry); if (multiPageController != null) { - child = StreamBuilder( + child = StreamBuilder( stream: multiPageController.infoStream, builder: (context, snapshot) { final multiPageInfo = multiPageController.info; - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { return _buildViewer(entry, pageEntry: multiPageInfo?.getPageEntryByIndex(page)); @@ -72,7 +72,7 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - Widget _buildViewer(AvesEntry mainEntry, {AvesEntry pageEntry}) { + Widget _buildViewer(AvesEntry mainEntry, {AvesEntry? pageEntry}) { return Selector( selector: (c, mq) => mq.size, builder: (c, mqSize, child) { @@ -81,7 +81,7 @@ class _MultiEntryScrollerState extends State with AutomaticK mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, viewportSize: mqSize, - onDisposed: () => widget.onViewDisposed?.call(mainEntry.uri), + onDisposed: () => widget.onViewDisposed(mainEntry.uri), ); }, ); @@ -95,7 +95,7 @@ class SingleEntryScroller extends StatefulWidget { final AvesEntry entry; const SingleEntryScroller({ - this.entry, + required this.entry, }); @override @@ -109,15 +109,15 @@ class _SingleEntryScrollerState extends State with Automati Widget build(BuildContext context) { super.build(context); - Widget child; + Widget? child; if (mainEntry.isMultiPage) { final multiPageController = context.read().getController(mainEntry); if (multiPageController != null) { - child = StreamBuilder( + child = StreamBuilder( stream: multiPageController.infoStream, builder: (context, snapshot) { final multiPageInfo = multiPageController.info; - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { return _buildViewer(pageEntry: multiPageInfo?.getPageEntryByIndex(page)); @@ -135,7 +135,7 @@ class _SingleEntryScrollerState extends State with Automati ); } - Widget _buildViewer({AvesEntry pageEntry}) { + Widget _buildViewer({AvesEntry? pageEntry}) { return Selector( selector: (c, mq) => mq.size, builder: (c, mqSize, child) { diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 1a4b00540..0681c265d 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -11,22 +11,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; class ViewerVerticalPageView extends StatefulWidget { - final CollectionLens collection; - final ValueNotifier entryNotifier; + final CollectionLens? collection; + final ValueNotifier entryNotifier; final PageController horizontalPager, verticalPager; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; final VoidCallback onImagePageRequested; final void Function(String uri) onViewDisposed; const ViewerVerticalPageView({ - @required this.collection, - @required this.entryNotifier, - @required this.verticalPager, - @required this.horizontalPager, - @required this.onVerticalPageChanged, - @required this.onHorizontalPageChanged, - @required this.onImagePageRequested, - @required this.onViewDisposed, + required this.collection, + required this.entryNotifier, + required this.verticalPager, + required this.horizontalPager, + required this.onVerticalPageChanged, + required this.onHorizontalPageChanged, + required this.onImagePageRequested, + required this.onViewDisposed, }); @override @@ -36,13 +36,13 @@ class ViewerVerticalPageView extends StatefulWidget { class _ViewerVerticalPageViewState extends State { final ValueNotifier _backgroundColorNotifier = ValueNotifier(Colors.black); final ValueNotifier _infoPageVisibleNotifier = ValueNotifier(false); - AvesEntry _oldEntry; + AvesEntry? _oldEntry; - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; bool get hasCollection => collection != null; - AvesEntry get entry => widget.entryNotifier.value; + AvesEntry? get entry => widget.entryNotifier.value; @override void initState() { @@ -72,7 +72,7 @@ class _ViewerVerticalPageViewState extends State { void _unregisterWidget(ViewerVerticalPageView widget) { widget.verticalPager.removeListener(_onVerticalPageControllerChanged); widget.entryNotifier.removeListener(_onEntryChanged); - _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); + _oldEntry?.imageChangeNotifier.removeListener(_onImageChanged); } @override @@ -82,18 +82,20 @@ class _ViewerVerticalPageViewState extends State { SizedBox(), hasCollection ? MultiEntryScroller( - collection: collection, + collection: collection!, pageController: widget.horizontalPager, onPageChanged: widget.onHorizontalPageChanged, onViewDisposed: widget.onViewDisposed, ) - : SingleEntryScroller( - entry: entry, - ), - NotificationListener( + : entry != null + ? SingleEntryScroller( + entry: entry!, + ) + : SizedBox(), + NotificationListener( onNotification: (notification) { - if (notification is BackUpNotification) widget.onImagePageRequested(); - return false; + widget.onImagePageRequested(); + return true; }, child: InfoPage( collection: collection, @@ -123,21 +125,21 @@ class _ViewerVerticalPageViewState extends State { } void _onVerticalPageControllerChanged() { - final opacity = min(1.0, widget.verticalPager.page); + final opacity = min(1.0, widget.verticalPager.page!); _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity); } // when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted) void _onEntryChanged() { - _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); + _oldEntry?.imageChangeNotifier.removeListener(_onImageChanged); _oldEntry = entry; if (entry != null) { - entry.imageChangeNotifier.addListener(_onImageChanged); + entry!.imageChangeNotifier.addListener(_onImageChanged); // make sure to locate the entry, // so that we can display the address instead of coordinates // even when initial collection locating has not reached this entry yet - entry.catalog(background: false).then((_) => entry.locate(background: false)); + entry!.catalog(background: false).then((_) => entry!.locate(background: false)); } else { Navigator.pop(context); } diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 8fd6512aa..4c37a6012 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -10,13 +10,13 @@ import 'package:provider/provider.dart'; class EntryViewerPage extends StatelessWidget { static const routeName = '/viewer'; - final CollectionLens collection; + final CollectionLens? collection; final AvesEntry initialEntry; const EntryViewerPage({ - Key key, + Key? key, this.collection, - this.initialEntry, + required this.initialEntry, }) : super(key: key); @override diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 03aa7df4a..01cc2ab74 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -26,6 +26,7 @@ import 'package:aves/widgets/viewer/overlay/top.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -34,13 +35,13 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class EntryViewerStack extends StatefulWidget { - final CollectionLens collection; + final CollectionLens? collection; final AvesEntry initialEntry; const EntryViewerStack({ - Key key, + Key? key, this.collection, - @required this.initialEntry, + required this.initialEntry, }) : super(key: key); @override @@ -48,25 +49,25 @@ class EntryViewerStack extends StatefulWidget { } class _EntryViewerStackState extends State with SingleTickerProviderStateMixin, WidgetsBindingObserver { - final ValueNotifier _entryNotifier = ValueNotifier(null); - int _currentHorizontalPage; - ValueNotifier _currentVerticalPage; - PageController _horizontalPager, _verticalPager; + final ValueNotifier _entryNotifier = ValueNotifier(null); + late int _currentHorizontalPage; + late ValueNotifier _currentVerticalPage; + late PageController _horizontalPager, _verticalPager; final AChangeNotifier _verticalScrollNotifier = AChangeNotifier(); final ValueNotifier _overlayVisible = ValueNotifier(true); - AnimationController _overlayAnimationController; - Animation _topOverlayScale, _bottomOverlayScale; - Animation _bottomOverlayOffset; - EdgeInsets _frozenViewInsets, _frozenViewPadding; - EntryActionDelegate _actionDelegate; + late AnimationController _overlayAnimationController; + late Animation _topOverlayScale, _bottomOverlayScale; + late Animation _bottomOverlayOffset; + EdgeInsets? _frozenViewInsets, _frozenViewPadding; + late EntryActionDelegate _actionDelegate; final List>> _viewStateNotifiers = []; - final ValueNotifier _heroInfoNotifier = ValueNotifier(null); + final ValueNotifier _heroInfoNotifier = ValueNotifier(null); - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; bool get hasCollection => collection != null; - List get entries => hasCollection ? collection.sortedEntries : [widget.initialEntry]; + List get entries => hasCollection ? collection!.sortedEntries : [widget.initialEntry]; static const int transitionPage = 0; @@ -110,8 +111,8 @@ class _EntryViewerStackState extends State with SingleTickerPr ); _initEntryControllers(); _registerWidget(widget); - WidgetsBinding.instance.addObserver(this); - WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay()); + WidgetsBinding.instance!.addObserver(this); + WidgetsBinding.instance!.addPostFrameCallback((_) => _initOverlay()); if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { WindowService.keepScreenOn(true); } @@ -129,7 +130,7 @@ class _EntryViewerStackState extends State with SingleTickerPr _overlayAnimationController.dispose(); _overlayVisible.removeListener(_onOverlayVisibleChange); _verticalPager.removeListener(_onVerticalPageControllerChange); - WidgetsBinding.instance.removeObserver(this); + WidgetsBinding.instance!.removeObserver(this); _unregisterWidget(widget); super.dispose(); } @@ -168,10 +169,10 @@ class _EntryViewerStackState extends State with SingleTickerPr } return SynchronousFuture(false); }, - child: ValueListenableProvider.value( + child: ValueListenableProvider.value( value: _heroInfoNotifier, child: NotificationListener( - onNotification: (notification) { + onNotification: (dynamic notification) { if (notification is FilterSelectedNotification) { _goToCollection(notification.filter); } else if (notification is ViewStateNotification) { @@ -181,13 +182,10 @@ class _EntryViewerStackState extends State with SingleTickerPr } return false; }, - child: NotificationListener( + child: NotificationListener( onNotification: (notification) { - if (notification is ToggleOverlayNotification) { - _overlayVisible.value = !_overlayVisible.value; - return true; - } - return false; + _overlayVisible.value = !_overlayVisible.value; + return true; }, child: Stack( children: [ @@ -212,18 +210,18 @@ class _EntryViewerStackState extends State with SingleTickerPr ); } - void _updateViewState(String uri, ViewState viewState) { - final viewStateNotifier = _viewStateNotifiers.firstWhere((kv) => kv.item1 == uri, orElse: () => null)?.item2; + void _updateViewState(String uri, ViewState? viewState) { + final viewStateNotifier = _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == uri)?.item2; viewStateNotifier?.value = viewState ?? ViewState.zero; } Widget _buildTopOverlay() { - final child = ValueListenableBuilder( + final child = ValueListenableBuilder( valueListenable: _entryNotifier, builder: (context, mainEntry, child) { if (mainEntry == null) return SizedBox.shrink(); - final viewStateNotifier = _viewStateNotifiers.firstWhere((kv) => kv.item1 == mainEntry.uri, orElse: () => null)?.item2; + final viewStateNotifier = _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == mainEntry.uri)?.item2; return ViewerTopOverlay( mainEntry: mainEntry, scale: _topOverlayScale, @@ -253,7 +251,7 @@ class _EntryViewerStackState extends State with SingleTickerPr builder: (context, page, child) { return Visibility( visible: page == imagePage, - child: child, + child: child!, ); }, child: child, @@ -261,15 +259,15 @@ class _EntryViewerStackState extends State with SingleTickerPr } Widget _buildBottomOverlay() { - Widget bottomOverlay = ValueListenableBuilder( + Widget bottomOverlay = ValueListenableBuilder( valueListenable: _entryNotifier, builder: (context, entry, child) { if (entry == null) return SizedBox.shrink(); - Widget _buildExtraBottomOverlay(AvesEntry pageEntry) { + Widget? _buildExtraBottomOverlay(AvesEntry pageEntry) { // a 360 video is both a video and a panorama but only the video controls are displayed if (pageEntry.isVideo) { - return Selector( + return Selector( selector: (context, vc) => vc.getController(pageEntry), builder: (context, videoController, child) => VideoControlOverlay( entry: pageEntry, @@ -288,12 +286,12 @@ class _EntryViewerStackState extends State with SingleTickerPr final multiPageController = entry.isMultiPage ? context.read().getController(entry) : null; final extraBottomOverlay = multiPageController != null - ? StreamBuilder( + ? StreamBuilder( stream: multiPageController.infoStream, builder: (context, snapshot) { final multiPageInfo = multiPageController.info; if (multiPageInfo == null) return SizedBox.shrink(); - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { final pageEntry = multiPageInfo.getPageEntryByIndex(page); @@ -329,7 +327,7 @@ class _EntryViewerStackState extends State with SingleTickerPr builder: (context, animation, child) { return Visibility( visible: _overlayAnimationController.status != AnimationStatus.dismissed, - child: child, + child: child!, ); }, child: child, @@ -342,12 +340,12 @@ class _EntryViewerStackState extends State with SingleTickerPr builder: (c, mqHeight, child) { // when orientation change, the `PageController` offset is not updated right away // and it does not trigger its listeners when it does, so we force a refresh in the next frame - WidgetsBinding.instance.addPostFrameCallback((_) => _onVerticalPageControllerChange()); + WidgetsBinding.instance!.addPostFrameCallback((_) => _onVerticalPageControllerChange()); return AnimatedBuilder( animation: _verticalScrollNotifier, builder: (context, child) => Positioned( bottom: (_verticalPager.position.hasPixels ? _verticalPager.offset : 0) - mqHeight, - child: child, + child: child!, ), child: child, ); @@ -362,19 +360,23 @@ class _EntryViewerStackState extends State with SingleTickerPr } void _goToCollection(CollectionFilter filter) { + final baseCollection = collection; + if (baseCollection == null) return; _onLeave(); Navigator.pushAndRemoveUntil( context, MaterialPageRoute( settings: RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage( - CollectionLens( - source: collection.source, - filters: collection.filters, - groupFactor: collection.groupFactor, - sortFactor: collection.sortFactor, - )..addFilter(filter), - ), + builder: (context) { + return CollectionPage( + CollectionLens( + source: baseCollection.source, + filters: baseCollection.filters, + groupFactor: baseCollection.groupFactor, + sortFactor: baseCollection.sortFactor, + )..addFilter(filter), + ); + }, ), (route) => false, ); @@ -410,7 +412,7 @@ class _EntryViewerStackState extends State with SingleTickerPr void _onEntryDeleted(BuildContext context, AvesEntry entry) { if (hasCollection) { - final entries = collection.sortedEntries; + final entries = collection!.sortedEntries; entries.remove(entry); if (entries.isEmpty) { Navigator.pop(context); @@ -424,14 +426,14 @@ class _EntryViewerStackState extends State with SingleTickerPr } Future _updateEntry() async { - if (_currentHorizontalPage != null && entries.isNotEmpty && _currentHorizontalPage >= entries.length) { + if (entries.isNotEmpty && _currentHorizontalPage >= entries.length) { // as of Flutter v1.22.2, `PageView` does not call `onPageChanged` when the last page is deleted // so we manually track the page change, and let the entry update follow _onHorizontalPageChanged(entries.length - 1); return; } - final newEntry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null; + final newEntry = _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null; if (_entryNotifier.value == newEntry) return; _entryNotifier.value = newEntry; await _pauseVideoControllers(); @@ -450,7 +452,7 @@ class _EntryViewerStackState extends State with SingleTickerPr if (_heroInfoNotifier.value != heroInfo) { _heroInfoNotifier.value = heroInfo; // we post closing the viewer page so that hero animation source is ready - WidgetsBinding.instance.addPostFrameCallback((_) => pop()); + WidgetsBinding.instance!.addPostFrameCallback((_) => pop()); } else { // viewer already has correct hero info, no need to rebuild pop(); @@ -479,7 +481,7 @@ class _EntryViewerStackState extends State with SingleTickerPr Future _initOverlay() async { // wait for MaterialPageRoute.transitionDuration // to show overlay after hero animation is complete - await Future.delayed(ModalRoute.of(context).transitionDuration * timeDilation); + await Future.delayed(ModalRoute.of(context)!.transitionDuration * timeDilation); await _onOverlayVisibleChange(); } @@ -527,7 +529,7 @@ class _EntryViewerStackState extends State with SingleTickerPr void _initViewStateController(AvesEntry entry) { final uri = entry.uri; - var controller = _viewStateNotifiers.firstWhere((kv) => kv.item1 == uri, orElse: () => null); + var controller = _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == uri); if (controller != null) { _viewStateNotifiers.remove(controller); } else { @@ -553,6 +555,9 @@ class _EntryViewerStackState extends State with SingleTickerPr setState(() {}); final multiPageInfo = multiPageController.info ?? await multiPageController.infoStream.first; + assert(multiPageInfo != null); + if (multiPageInfo == null) return; + if (entry.isMotionPhoto) { await multiPageInfo.extractMotionPhotoVideo(); } @@ -568,11 +573,10 @@ class _EntryViewerStackState extends State with SingleTickerPr await _pauseVideoControllers(); if (settings.enableVideoAutoPlay) { final page = multiPageController.page; - final pageInfo = multiPageInfo.getByIndex(page); + final pageInfo = multiPageInfo.getByIndex(page)!; if (pageInfo.isVideo) { final pageEntry = multiPageInfo.getPageEntryByIndex(page); - final pageVideoController = videoConductor.getController(pageEntry); - assert(pageVideoController != null); + final pageVideoController = videoConductor.getController(pageEntry)!; await _playVideo(pageVideoController, () => entry == _entryNotifier.value && page == multiPageController.page); } } diff --git a/lib/widgets/viewer/hero.dart b/lib/widgets/viewer/hero.dart index 80fa947fa..b502a0a93 100644 --- a/lib/widgets/viewer/hero.dart +++ b/lib/widgets/viewer/hero.dart @@ -5,8 +5,8 @@ class HeroInfo { // hero tag should include a collection identifier, so that it animates // between different views of the entry in the same collection (e.g. thumbnails <-> viewer) // but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer) - final int collectionId; - final AvesEntry entry; + final int? collectionId; + final AvesEntry? entry; const HeroInfo(this.collectionId, this.entry); diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index cae16dfa1..1e03dc029 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -19,21 +19,21 @@ import 'package:intl/intl.dart'; class BasicSection extends StatelessWidget { final AvesEntry entry; - final CollectionLens collection; + final CollectionLens? collection; final ValueNotifier visibleNotifier; final FilterCallback onFilter; const BasicSection({ - Key key, - @required this.entry, + Key? key, + required this.entry, this.collection, - @required this.visibleNotifier, - @required this.onFilter, + required this.visibleNotifier, + required this.onFilter, }) : super(key: key); int get megaPixels => entry.megaPixels; - bool get showMegaPixels => entry.isPhoto && megaPixels != null && megaPixels > 0; + bool get showMegaPixels => entry.isPhoto && megaPixels > 0; String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}'; @@ -48,7 +48,7 @@ class BasicSection extends StatelessWidget { // TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081 // inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue) final title = entry.bestTitle ?? infoUnknown; - final uri = entry.uri ?? infoUnknown; + final uri = entry.uri; final path = entry.path; return Column( @@ -59,7 +59,7 @@ class BasicSection extends StatelessWidget { l10n.viewerInfoLabelDate: dateText, if (entry.isVideo) ..._buildVideoRows(context), if (!entry.isSvg && entry.isSized) l10n.viewerInfoLabelResolution: rasterResolutionText, - l10n.viewerInfoLabelSize: entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : infoUnknown, + l10n.viewerInfoLabelSize: entry.sizeBytes != null ? formatFilesize(entry.sizeBytes!) : infoUnknown, l10n.viewerInfoLabelUri: uri, if (path != null) l10n.viewerInfoLabelPath: path, }), @@ -83,7 +83,7 @@ class BasicSection extends StatelessWidget { if (entry.isImage && entry.is360) TypeFilter.panorama, if (entry.isVideo && entry.is360) TypeFilter.sphericalVideo, if (entry.isVideo && !entry.is360) MimeFilter.video, - if (album != null) AlbumFilter(album, collection?.source?.getAlbumDisplayName(context, album)), + if (album != null) AlbumFilter(album, collection?.source.getAlbumDisplayName(context, album)), ...tags.map((tag) => TagFilter(tag)), }; return AnimatedBuilder( @@ -123,8 +123,8 @@ class OwnerProp extends StatefulWidget { final ValueNotifier visibleNotifier; const OwnerProp({ - @required this.entry, - @required this.visibleNotifier, + required this.entry, + required this.visibleNotifier, }); @override @@ -132,8 +132,8 @@ class OwnerProp extends StatefulWidget { } class _OwnerPropState extends State { - final ValueNotifier _loadedUri = ValueNotifier(null); - String _ownerPackage; + final ValueNotifier _loadedUri = ValueNotifier(null); + String? _ownerPackage; AvesEntry get entry => widget.entry; @@ -172,11 +172,11 @@ class _OwnerPropState extends State { @override Widget build(BuildContext context) { - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: _loadedUri, builder: (context, uri, child) { if (_ownerPackage == null) return SizedBox(); - final appName = androidFileUtils.getCurrentAppName(_ownerPackage) ?? _ownerPackage; + final appName = androidFileUtils.getCurrentAppName(_ownerPackage!) ?? _ownerPackage; // as of Flutter v1.22.6, `SelectableText` cannot contain `WidgetSpan` // so be use a basic `Text` instead return Text.rich( @@ -195,7 +195,7 @@ class _OwnerPropState extends State { padding: EdgeInsets.symmetric(horizontal: 4), child: Image( image: AppIconImage( - packageName: _ownerPackage, + packageName: _ownerPackage!, size: iconSize, ), width: iconSize, @@ -215,7 +215,6 @@ class _OwnerPropState extends State { } Future _getOwner() async { - if (entry == null) return; if (_loadedUri.value == entry.uri) return; final isMediaContent = entry.uri.startsWith('content://media/external/'); if (isVisible && isMediaContent) { diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index becd341bc..3416c3eca 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -40,7 +40,7 @@ class SectionRow extends StatelessWidget { class InfoRowGroup extends StatefulWidget { final Map keyValues; final int maxValueLength; - final Map linkHandlers; + final Map? linkHandlers; static const keyValuePadding = 16; static const linkColor = Colors.blue; @@ -66,7 +66,7 @@ class _InfoRowGroupState extends State { int get maxValueLength => widget.maxValueLength; - Map get linkHandlers => widget.linkHandlers; + Map? get linkHandlers => widget.linkHandlers; @override Widget build(BuildContext context) { @@ -93,11 +93,11 @@ class _InfoRowGroupState extends State { (kv) { final key = kv.key; String value; - TextStyle style; - GestureRecognizer recognizer; + TextStyle? style; + GestureRecognizer? recognizer; if (linkHandlers?.containsKey(key) == true) { - final handler = linkHandlers[key]; + final handler = linkHandlers![key]!; value = handler.linkText(context); // open link on tap recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context); @@ -119,7 +119,7 @@ class _InfoRowGroupState extends State { // as of Flutter v1.22.6, `SelectableText` cannot contain `WidgetSpan` // so we add padding using multiple hair spaces instead - final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + InfoRowGroup.keyValuePadding; + final thisSpaceSize = max(0.0, (baseValueX - keySizes[key]!)) + InfoRowGroup.keyValuePadding; final spaceCount = (100 * thisSpaceSize / baseSpaceWidth).round(); return [ @@ -153,7 +153,7 @@ class InfoLinkHandler { final void Function(BuildContext context) onTap; const InfoLinkHandler({ - @required this.linkText, - @required this.onTap, + required this.linkText, + required this.onTap, }); } diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 7ee11eb9d..4c402b17f 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -12,9 +12,9 @@ class InfoAppBar extends StatelessWidget { final VoidCallback onBackPressed; const InfoAppBar({ - @required this.entry, - @required this.metadataNotifier, - @required this.onBackPressed, + required this.entry, + required this.metadataNotifier, + required this.onBackPressed, }); @override diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 741679d17..4eea6c0f3 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -15,15 +15,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class InfoPage extends StatefulWidget { - final CollectionLens collection; - final ValueNotifier entryNotifier; + final CollectionLens? collection; + final ValueNotifier entryNotifier; final ValueNotifier visibleNotifier; const InfoPage({ - Key key, - @required this.collection, - @required this.entryNotifier, - @required this.visibleNotifier, + Key? key, + required this.collection, + required this.entryNotifier, + required this.visibleNotifier, }) : super(key: key); @override @@ -34,9 +34,9 @@ class _InfoPageState extends State { final ScrollController _scrollController = ScrollController(); bool _scrollStartFromTop = false; - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; - AvesEntry get entry => widget.entryNotifier.value; + AvesEntry? get entry => widget.entryNotifier.value; @override Widget build(BuildContext context) { @@ -45,7 +45,7 @@ class _InfoPageState extends State { body: GestureAreaProtectorStack( child: SafeArea( bottom: false, - child: NotificationListener( + child: NotificationListener( onNotification: _handleTopScroll, child: NotificationListener( onNotification: (notification) { @@ -55,7 +55,7 @@ class _InfoPageState extends State { child: Selector( selector: (c, mq) => mq.size.width, builder: (c, mqWidth, child) { - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: widget.entryNotifier, builder: (context, entry, child) { return entry != null @@ -81,22 +81,20 @@ class _InfoPageState extends State { ); } - bool _handleTopScroll(Notification notification) { - if (notification is ScrollNotification) { - if (notification is ScrollStartNotification) { - final metrics = notification.metrics; - _scrollStartFromTop = metrics.pixels == metrics.minScrollExtent; - } - if (_scrollStartFromTop) { - if (notification is ScrollUpdateNotification) { - _scrollStartFromTop = notification.scrollDelta < 0; - } else if (notification is ScrollEndNotification) { + bool _handleTopScroll(ScrollNotification notification) { + if (notification is ScrollStartNotification) { + final metrics = notification.metrics; + _scrollStartFromTop = metrics.pixels == metrics.minScrollExtent; + } + if (_scrollStartFromTop) { + if (notification is ScrollUpdateNotification) { + _scrollStartFromTop = notification.scrollDelta! < 0; + } else if (notification is ScrollEndNotification) { + _scrollStartFromTop = false; + } else if (notification is OverscrollNotification) { + if (notification.overscroll < 0) { + _goToViewer(); _scrollStartFromTop = false; - } else if (notification is OverscrollNotification) { - if (notification.overscroll < 0) { - _goToViewer(); - _scrollStartFromTop = false; - } } } } @@ -126,7 +124,7 @@ class _InfoPageState extends State { } class _InfoPageContent extends StatefulWidget { - final CollectionLens collection; + final CollectionLens? collection; final AvesEntry entry; final ValueNotifier visibleNotifier; final ScrollController scrollController; @@ -134,13 +132,13 @@ class _InfoPageContent extends StatefulWidget { final VoidCallback goToViewer; const _InfoPageContent({ - Key key, - @required this.collection, - @required this.entry, - @required this.visibleNotifier, - @required this.scrollController, - @required this.split, - @required this.goToViewer, + Key? key, + required this.collection, + required this.entry, + required this.visibleNotifier, + required this.scrollController, + required this.split, + required this.goToViewer, }) : super(key: key); @override @@ -152,7 +150,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { final ValueNotifier> _metadataNotifier = ValueNotifier({}); - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; AvesEntry get entry => widget.entry; diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index 83a96338c..207793caa 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -17,9 +17,9 @@ class InfoSearchDelegate extends SearchDelegate { Map get metadata => metadataNotifier.value; InfoSearchDelegate({ - @required String searchFieldLabel, - @required this.entry, - @required this.metadataNotifier, + required String searchFieldLabel, + required this.entry, + required this.metadataNotifier, }) : super( searchFieldLabel: searchFieldLabel, ); diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index e030e804b..463c7360d 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -18,19 +18,19 @@ import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; class LocationSection extends StatefulWidget { - final CollectionLens collection; + final CollectionLens? collection; final AvesEntry entry; final bool showTitle; final ValueNotifier visibleNotifier; final FilterCallback onFilter; const LocationSection({ - Key key, - @required this.collection, - @required this.entry, - @required this.showTitle, - @required this.visibleNotifier, - @required this.onFilter, + Key? key, + required this.collection, + required this.entry, + required this.showTitle, + required this.visibleNotifier, + required this.onFilter, }) : super(key: key); @override @@ -38,12 +38,12 @@ class LocationSection extends StatefulWidget { } class _LocationSectionState extends State with TickerProviderStateMixin { - String _loadedUri; + String? _loadedUri; static const extent = 48.0; static const pointerSize = Size(8.0, 6.0); - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; AvesEntry get entry => widget.entry; @@ -85,7 +85,7 @@ class _LocationSectionState extends State with TickerProviderSt _loadedUri = entry.uri; final filters = []; if (entry.hasAddress) { - final address = entry.addressDetails; + final address = entry.addressDetails!; final country = address.countryName; if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}')); final place = address.place; @@ -108,10 +108,11 @@ class _LocationSectionState extends State with TickerProviderSt future: availability.isConnected, builder: (context, snapshot) { if (snapshot.data != true) return SizedBox(); - return NotificationListener( + final latLng = entry.latLng!; + return NotificationListener( onNotification: (notification) { - if (notification is MapStyleChangedNotification) setState(() {}); - return false; + setState(() {}); + return true; }, child: AnimatedSize( alignment: Alignment.topCenter, @@ -121,15 +122,15 @@ class _LocationSectionState extends State with TickerProviderSt child: settings.infoMapStyle.isGoogleMaps ? EntryGoogleMap( // `LatLng` used by `google_maps_flutter` is not the one from `latlong` package - latLng: Tuple2(entry.latLng.latitude, entry.latLng.longitude), - geoUri: entry.geoUri, + latLng: Tuple2(latLng.latitude, latLng.longitude), + geoUri: entry.geoUri!, initialZoom: settings.infoMapZoom, - markerId: entry.uri ?? entry.path, + markerId: entry.uri, markerBuilder: buildMarker, ) : EntryLeafletMap( - latLng: entry.latLng, - geoUri: entry.geoUri, + latLng: latLng, + geoUri: entry.geoUri!, initialZoom: settings.infoMapZoom, style: settings.infoMapStyle, markerSize: Size(extent, extent + pointerSize.height), @@ -168,14 +169,14 @@ class _LocationSectionState extends State with TickerProviderSt class _AddressInfoGroup extends StatefulWidget { final AvesEntry entry; - const _AddressInfoGroup({@required this.entry}); + const _AddressInfoGroup({required this.entry}); @override _AddressInfoGroupState createState() => _AddressInfoGroupState(); } class _AddressInfoGroupState extends State<_AddressInfoGroup> { - Future _addressLineLoader; + late Future _addressLineLoader; AvesEntry get entry => widget.entry; @@ -192,14 +193,14 @@ class _AddressInfoGroupState extends State<_AddressInfoGroup> { @override Widget build(BuildContext context) { - return FutureBuilder( + return FutureBuilder( future: _addressLineLoader, builder: (context, snapshot) { final fullAddress = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : null; final address = fullAddress ?? entry.shortAddress; final l10n = context.l10n; return InfoRowGroup({ - l10n.viewerInfoLabelCoordinates: settings.coordinateFormat.format(entry.latLng), + l10n.viewerInfoLabelCoordinates: settings.coordinateFormat.format(entry.latLng!), if (address.isNotEmpty) l10n.viewerInfoLabelAddress: address, }); }, diff --git a/lib/widgets/viewer/info/maps/common.dart b/lib/widgets/viewer/info/maps/common.dart index efc307b0c..21b9aa08b 100644 --- a/lib/widgets/viewer/info/maps/common.dart +++ b/lib/widgets/viewer/info/maps/common.dart @@ -19,7 +19,7 @@ class MapDecorator extends StatelessWidget { static final BorderRadius mapBorderRadius = BorderRadius.circular(24); // to match button circles - const MapDecorator({@required this.child}); + const MapDecorator({required this.child}); @override Widget build(BuildContext context) { @@ -47,8 +47,8 @@ class MapButtonPanel extends StatelessWidget { static const double padding = 4; const MapButtonPanel({ - @required this.geoUri, - @required this.zoomBy, + required this.geoUri, + required this.zoomBy, }); @override @@ -126,9 +126,9 @@ class MapOverlayButton extends StatelessWidget { final VoidCallback onPressed; const MapOverlayButton({ - @required this.icon, - @required this.tooltip, - @required this.onPressed, + required this.icon, + required this.tooltip, + required this.onPressed, }); @override diff --git a/lib/widgets/viewer/info/maps/google_map.dart b/lib/widgets/viewer/info/maps/google_map.dart index c4ebfc55a..63db30514 100644 --- a/lib/widgets/viewer/info/maps/google_map.dart +++ b/lib/widgets/viewer/info/maps/google_map.dart @@ -17,12 +17,12 @@ class EntryGoogleMap extends StatefulWidget { final WidgetBuilder markerBuilder; EntryGoogleMap({ - Key key, - Tuple2 latLng, - this.geoUri, - this.initialZoom, - this.markerId, - this.markerBuilder, + Key? key, + required Tuple2 latLng, + required this.geoUri, + required this.initialZoom, + required this.markerId, + required this.markerBuilder, }) : latLng = LatLng(latLng.item1, latLng.item2), super(key: key); @@ -31,8 +31,8 @@ class EntryGoogleMap extends StatefulWidget { } class _EntryGoogleMapState extends State with AutomaticKeepAliveClientMixin { - GoogleMapController _controller; - Completer _markerLoaderCompleter; + GoogleMapController? _controller; + late Completer _markerLoaderCompleter; @override void initState() { @@ -44,7 +44,7 @@ class _EntryGoogleMapState extends State with AutomaticKeepAlive void didUpdateWidget(covariant EntryGoogleMap oldWidget) { super.didUpdateWidget(oldWidget); if (widget.latLng != oldWidget.latLng && _controller != null) { - _controller.moveCamera(CameraUpdate.newLatLng(widget.latLng)); + _controller!.moveCamera(CameraUpdate.newLatLng(widget.latLng)); } if (widget.markerId != oldWidget.markerId) { _markerLoaderCompleter = Completer(); @@ -84,7 +84,7 @@ class _EntryGoogleMapState extends State with AutomaticKeepAlive builder: (context, snapshot) { final markers = {}; if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) { - final markerBytes = snapshot.data; + final markerBytes = snapshot.data!; markers.add(Marker( markerId: MarkerId(widget.markerId), icon: BitmapDescriptor.fromBytes(markerBytes), @@ -117,7 +117,7 @@ class _EntryGoogleMapState extends State with AutomaticKeepAlive void _zoomBy(double amount) { settings.infoMapZoom += amount; - _controller.animateCamera(CameraUpdate.zoomBy(amount)); + _controller?.animateCamera(CameraUpdate.zoomBy(amount)); } MapType _toMapStyle(EntryMapStyle style) { diff --git a/lib/widgets/viewer/info/maps/leaflet_map.dart b/lib/widgets/viewer/info/maps/leaflet_map.dart index 7780b1475..c31f0cdc7 100644 --- a/lib/widgets/viewer/info/maps/leaflet_map.dart +++ b/lib/widgets/viewer/info/maps/leaflet_map.dart @@ -20,13 +20,13 @@ class EntryLeafletMap extends StatefulWidget { final WidgetBuilder markerBuilder; const EntryLeafletMap({ - Key key, - this.latLng, - this.geoUri, - this.initialZoom, - this.style, - this.markerBuilder, - this.markerSize, + Key? key, + required this.latLng, + required this.geoUri, + required this.initialZoom, + required this.style, + required this.markerBuilder, + required this.markerSize, }) : super(key: key); @override @@ -39,7 +39,7 @@ class _EntryLeafletMapState extends State with AutomaticKeepAli @override void didUpdateWidget(covariant EntryLeafletMap oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.latLng != oldWidget.latLng && _mapController != null) { + if (widget.latLng != oldWidget.latLng) { _mapController.move(widget.latLng, settings.infoMapZoom); } } @@ -132,7 +132,7 @@ class _EntryLeafletMapState extends State with AutomaticKeepAli p: TextStyle(color: Colors.white70, fontSize: InfoRowGroup.fontSize), ), onTapLink: (text, href, title) async { - if (await canLaunch(href)) { + if (href != null && await canLaunch(href)) { await launch(href); } }, @@ -141,8 +141,6 @@ class _EntryLeafletMapState extends State with AutomaticKeepAli } void _zoomBy(double amount) { - if (_mapController == null) return; - final endZoom = (settings.infoMapZoom + amount).clamp(1.0, 16.0); settings.infoMapZoom = endZoom; diff --git a/lib/widgets/viewer/info/maps/marker.dart b/lib/widgets/viewer/info/maps/marker.dart index ef019964d..69eb140d8 100644 --- a/lib/widgets/viewer/info/maps/marker.dart +++ b/lib/widgets/viewer/info/maps/marker.dart @@ -16,12 +16,12 @@ class ImageMarker extends StatelessWidget { static const double outerBorderRadiusDim = 8; static const double outerBorderWidth = 1.5; static const double innerBorderWidth = 2; - static const outerBorderColor = Colors.white30; - static final innerBorderColor = Colors.grey[900]; + static const Color outerBorderColor = Colors.white30; + static final Color innerBorderColor = Colors.grey[900]!; const ImageMarker({ - @required this.entry, - @required this.extent, + required this.entry, + required this.extent, this.pointerSize = Size.zero, }); @@ -83,10 +83,10 @@ class MarkerPointerPainter extends CustomPainter { final Size size; const MarkerPointerPainter({ - this.color, - this.outlineColor, - this.outlineWidth, - this.size, + required this.color, + required this.outlineColor, + required this.outlineWidth, + required this.size, }); @override @@ -127,10 +127,10 @@ class MarkerGeneratorWidget extends StatefulWidget { final Function(List bitmaps) onComplete; const MarkerGeneratorWidget({ - Key key, - @required this.markers, + Key? key, + required this.markers, this.delay = Duration.zero, - @required this.onComplete, + required this.onComplete, }) : super(key: key); @override @@ -143,7 +143,7 @@ class _MarkerGeneratorWidgetState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance!.addPostFrameCallback((_) async { if (widget.delay > Duration.zero) { await Future.delayed(widget.delay); } @@ -174,10 +174,10 @@ class _MarkerGeneratorWidgetState extends State { Future> _getBitmaps(BuildContext context) async { final pixelRatio = context.read().devicePixelRatio; return Future.wait(_globalKeys.map((key) async { - RenderRepaintBoundary boundary = key.currentContext.findRenderObject(); + final boundary = key.currentContext!.findRenderObject() as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: pixelRatio); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); - return byteData.buffer.asUint8List(); + return byteData != null ? byteData.buffer.asUint8List() : Uint8List(0); })); } } diff --git a/lib/widgets/viewer/info/maps/scale_layer.dart b/lib/widgets/viewer/info/maps/scale_layer.dart index 0a216c00e..5a0a1e6bf 100644 --- a/lib/widgets/viewer/info/maps/scale_layer.dart +++ b/lib/widgets/viewer/info/maps/scale_layer.dart @@ -11,7 +11,7 @@ class ScaleLayerOptions extends LayerOptions { final Widget Function(double width, String distance) builder; ScaleLayerOptions({ - Key key, + Key? key, this.builder = defaultBuilder, rebuild, }) : super(key: key, rebuild: rebuild); @@ -27,7 +27,7 @@ class ScaleLayerOptions extends LayerOptions { class ScaleLayerWidget extends StatelessWidget { final ScaleLayerOptions options; - ScaleLayerWidget({@required this.options}) : super(key: options.key); + ScaleLayerWidget({required this.options}) : super(key: options.key); @override Widget build(BuildContext context) { @@ -86,7 +86,7 @@ class ScaleLayer extends StatelessWidget { final targetPoint = util.calculateEndingGlobalCoordinates(center, 90, distance); final end = map.project(targetPoint); final displayDistance = distance > 999 ? '${(distance / 1000).toStringAsFixed(0)} km' : '${distance.toStringAsFixed(0)} m'; - final double width = (end.x - start.x); + final width = end.x - (start.x as double); return scaleLayerOpts.builder(width, displayDistance); }, @@ -104,8 +104,8 @@ class ScaleBar extends StatelessWidget { static const double barThickness = 1; const ScaleBar({ - @required this.distance, - @required this.width, + required this.distance, + required this.width, }); @override diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index 078a39143..da9220fdf 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -26,13 +26,13 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { final AvesEntry entry; final String title; final MetadataDirectory dir; - final ValueNotifier expandedDirectoryNotifier; + final ValueNotifier? expandedDirectoryNotifier; final bool initiallyExpanded, showThumbnails; const MetadataDirTile({ - @required this.entry, - @required this.title, - @required this.dir, + required this.entry, + required this.title, + required this.dir, this.expandedDirectoryNotifier, this.initiallyExpanded = false, this.showThumbnails = true, @@ -53,7 +53,7 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { initiallyExpanded: initiallyExpanded, ); } else { - Map linkHandlers; + Map? linkHandlers; switch (dirName) { case SvgMetadataService.metadataDirectory: linkHandlers = getSvgLinkHandlers(tags); @@ -100,7 +100,7 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { MaterialPageRoute( settings: RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( - loader: () => SynchronousFuture(tags['Metadata']), + loader: () => SynchronousFuture(tags['Metadata'] ?? ''), ), ), ); @@ -119,7 +119,7 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { } Future _openEmbeddedData(BuildContext context, OpenEmbeddedDataNotification notification) async { - Map fields; + late Map fields; switch (notification.source) { case EmbeddedDataSource.motionPhotoVideo: fields = await embeddedDataService.extractMotionPhotoVideo(entry); @@ -131,13 +131,13 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { fields = await embeddedDataService.extractXmpDataProp(entry, notification.propPath, notification.mimeType); break; } - if (fields == null || !fields.containsKey('mimeType') || !fields.containsKey('uri')) { + if (!fields.containsKey('mimeType') || !fields.containsKey('uri')) { showFeedback(context, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); return; } - final mimeType = fields['mimeType']; - final uri = fields['uri']; + final mimeType = fields['mimeType']!; + final uri = fields['uri']!; if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) { // open with another app unawaited(AndroidAppService.open(uri, mimeType).then((success) { diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 307a05343..0353243cf 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -23,9 +23,9 @@ class MetadataSectionSliver extends StatefulWidget { final ValueNotifier> metadataNotifier; const MetadataSectionSliver({ - @required this.entry, - @required this.visibleNotifier, - @required this.metadataNotifier, + required this.entry, + required this.visibleNotifier, + required this.metadataNotifier, }); @override @@ -33,8 +33,8 @@ class MetadataSectionSliver extends StatefulWidget { } class _MetadataSectionSliverState extends State with AutomaticKeepAliveClientMixin { - final ValueNotifier _loadedMetadataUri = ValueNotifier(null); - final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); + final ValueNotifier _loadedMetadataUri = ValueNotifier(null); + final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); AvesEntry get entry => widget.entry; @@ -92,7 +92,7 @@ class _MetadataSectionSliverState extends State with Auto // cancel notification bubbling so that the info page // does not misinterpret content scrolling for page scrolling onNotification: (notification) => true, - child: ValueListenableBuilder( + child: ValueListenableBuilder( valueListenable: _loadedMetadataUri, builder: (context, uri, child) { Widget content; @@ -124,7 +124,7 @@ class _MetadataSectionSliverState extends State with Auto return AnimationLimiter( // we update the limiter key after fetching the metadata of a new entry, // in order to restart the staggered animation of the metadata section - key: Key(uri), + key: Key(uri ?? ''), child: content, ); }, @@ -140,21 +140,20 @@ class _MetadataSectionSliverState extends State with Auto } Future _getMetadata() async { - if (entry == null) return; if (_loadedMetadataUri.value == entry.uri) return; if (isVisible) { - final rawMetadata = await (entry.isSvg ? SvgMetadataService.getAllMetadata(entry) : metadataService.getAllMetadata(entry)) ?? {}; + final rawMetadata = await (entry.isSvg ? SvgMetadataService.getAllMetadata(entry) : metadataService.getAllMetadata(entry)); final directories = rawMetadata.entries.map((dirKV) { - var directoryName = dirKV.key as String ?? ''; + var directoryName = dirKV.key as String; - String parent; + String? parent; final parts = directoryName.split(parentChildSeparator); if (parts.length > 1) { parent = parts[0]; directoryName = parts[1]; } - final rawTags = dirKV.value as Map ?? {}; + final rawTags = dirKV.value as Map; return MetadataDirectory(directoryName, parent, _toSortedTags(rawTags)); }).toList(); @@ -232,19 +231,19 @@ class _MetadataSectionSliverState extends State with Auto // group attachments by format (e.g. TTF fonts) if (attachmentStreams.isNotEmpty) { - final formatCount = >{}; + final formatCount = >{}; for (final stream in attachmentStreams) { - final codec = (stream[Keys.codecName] as String ?? 'unknown').toUpperCase(); + final codec = (stream[Keys.codecName] as String? ?? 'unknown').toUpperCase(); if (!formatCount.containsKey(codec)) { formatCount[codec] = []; } - formatCount[codec].add(stream[Keys.filename]); + formatCount[codec]!.add(stream[Keys.filename]); } if (formatCount.isNotEmpty) { final rawTags = formatCount.map((key, value) { final count = value.length; // remove duplicate names, so number of displayed names may not match displayed count - final names = value.where((v) => v != null).toSet().toList()..sort(compareAsciiUpperCase); + final names = value.where((v) => v != null).cast().toSet().toList()..sort(compareAsciiUpperCase); return MapEntry(key, '$count items: ${names.join(', ')}'); }); directories.add(MetadataDirectory('Attachments', null, _toSortedTags(rawTags))); @@ -255,12 +254,15 @@ class _MetadataSectionSliverState extends State with Auto } SplayTreeMap _toSortedTags(Map rawTags) { - final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) { - final value = (tagKV.value as String ?? '').trim(); - if (value.isEmpty) return null; - final tagName = tagKV.key as String ?? ''; - return MapEntry(tagName, value); - }).where((kv) => kv != null))); + final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries + .map((tagKV) { + var value = (tagKV.value as String? ?? '').trim(); + if (value.isEmpty) return null; + final tagName = tagKV.key as String; + return MapEntry(tagName, value); + }) + .where((kv) => kv != null) + .cast>())); return tags; } @@ -270,8 +272,8 @@ class _MetadataSectionSliverState extends State with Auto class MetadataDirectory { final String name; - final Color color; - final String parent; + final Color? color; + final String? parent; final SplayTreeMap allTags; final SplayTreeMap tags; @@ -281,7 +283,7 @@ class MetadataDirectory { static const mediaDirectory = 'Media'; // custom static const coverDirectory = 'Cover'; // custom - const MetadataDirectory(this.name, this.parent, SplayTreeMap allTags, {SplayTreeMap tags, this.color}) + const MetadataDirectory(this.name, this.parent, SplayTreeMap allTags, {SplayTreeMap? tags, this.color}) : allTags = allTags, tags = tags ?? allTags; diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 3c2c55005..eef9a8f92 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -10,8 +10,8 @@ class MetadataThumbnails extends StatefulWidget { final AvesEntry entry; const MetadataThumbnails({ - Key key, - @required this.entry, + Key? key, + required this.entry, }) : super(key: key); @override @@ -19,7 +19,7 @@ class MetadataThumbnails extends StatefulWidget { } class _MetadataThumbnailsState extends State { - Future> _loader; + late Future> _loader; AvesEntry get entry => widget.entry; @@ -36,12 +36,12 @@ class _MetadataThumbnailsState extends State { return FutureBuilder>( future: _loader, builder: (context, snapshot) { - if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) { + if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data!.isNotEmpty) { return Container( alignment: AlignmentDirectional.topStart, padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4), child: Wrap( - children: snapshot.data.map((bytes) { + children: snapshot.data!.map((bytes) { return Image.memory( bytes, scale: context.select((mq) => mq.devicePixelRatio), diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index 8a2b552b6..fc53c323a 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -62,7 +62,8 @@ class XmpNamespace { final prop = XmpProp(kv.key, kv.value); return extractData(prop) ? null : prop; }) - .where((e) => e != null) + .where((v) => v != null) + .cast() .toList() ..sort((a, b) => compareAsciiUpperCaseNatural(a.displayKey, b.displayKey)); @@ -97,7 +98,7 @@ class XmpNamespace { if (matches.isEmpty) return false; final match = matches.first; - final field = XmpProp.formatKey(match.group(1)); + final field = XmpProp.formatKey(match.group(1)!); store[field] = formatValue(prop); return true; } @@ -107,8 +108,8 @@ class XmpNamespace { if (matches.isEmpty) return false; final match = matches.first; - final index = int.parse(match.group(1)); - final field = XmpProp.formatKey(match.group(2)); + final index = int.parse(match.group(1)!); + final field = XmpProp.formatKey(match.group(2)!); final fields = store.putIfAbsent(index, () => {}); fields[field] = formatValue(prop); return true; @@ -120,7 +121,7 @@ class XmpNamespace { String formatValue(XmpProp prop) => prop.value; - Map linkifyValues(List props) => null; + Map linkifyValues(List props) => {}; // identity diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 24db7a4da..ee21f1079 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -2,6 +2,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; +import 'package:collection/collection.dart'; import 'package:tuple/tuple.dart'; abstract class XmpGoogleNamespace extends XmpNamespace { @@ -14,8 +15,8 @@ abstract class XmpGoogleNamespace extends XmpNamespace { return Map.fromEntries(dataProps.map((t) { final dataPropPath = t.item1; final mimePropPath = t.item2; - final dataProp = props.firstWhere((prop) => prop.path == dataPropPath, orElse: () => null); - final mimeProp = props.firstWhere((prop) => prop.path == mimePropPath, orElse: () => null); + final dataProp = props.firstWhereOrNull((prop) => prop.path == dataPropPath); + final mimeProp = props.firstWhereOrNull((prop) => prop.path == mimePropPath); return (dataProp != null && mimeProp != null) ? MapEntry( dataProp.displayKey, @@ -27,7 +28,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace { ).dispatch(context), )) : null; - }).where((e) => e != null)); + }).where((kv) => kv != null).cast>()); } } @@ -75,7 +76,7 @@ class XmpGCameraNamespace extends XmpNamespace { static const videoOffsetKey = 'GCamera:MicroVideoOffset'; static const videoDataKey = 'Data'; - bool _isMotionPhoto; + late bool _isMotionPhoto; XmpGCameraNamespace(Map rawProps) : super(ns, rawProps) { _isMotionPhoto = rawProps.keys.any((key) => key == videoOffsetKey); diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index d39e5c1d4..6d007bcdb 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart @@ -29,7 +29,7 @@ class XmpBasicNamespace extends XmpNamespace { title: 'Thumbnail', structByIndex: thumbnails, linkifier: (index) { - final struct = thumbnails[index]; + final struct = thumbnails[index]!; return { if (struct.containsKey(thumbnailDataDisplayKey)) thumbnailDataDisplayKey: InfoLinkHandler( diff --git a/lib/widgets/viewer/info/metadata/xmp_structs.dart b/lib/widgets/viewer/info/metadata/xmp_structs.dart index 6060f46bf..b94eb4d81 100644 --- a/lib/widgets/viewer/info/metadata/xmp_structs.dart +++ b/lib/widgets/viewer/info/metadata/xmp_structs.dart @@ -12,15 +12,15 @@ import 'package:flutter/material.dart'; class XmpStructArrayCard extends StatefulWidget { final String title; final List> structs = []; - final Map Function(int index) linkifier; + final Map Function(int index)? linkifier; XmpStructArrayCard({ - @required this.title, - @required Map> structByIndex, + required this.title, + required Map> structByIndex, this.linkifier, }) { structs.length = structByIndex.keys.fold(0, max); - structByIndex.keys.forEach((index) => structs[index - 1] = structByIndex[index]); + structByIndex.keys.forEach((index) => structs[index - 1] = structByIndex[index]!); } @override @@ -28,7 +28,7 @@ class XmpStructArrayCard extends StatefulWidget { } class _XmpStructArrayCardState extends State { - int _index; + late int _index; List> get structs => widget.structs; @@ -90,7 +90,7 @@ class _XmpStructArrayCardState extends State { // without clipping the text padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup( - structs[_index] ?? {}, + structs[_index], maxValueLength: Constants.infoGroupMaxValueLength, linkHandlers: widget.linkifier?.call(_index + 1), ), @@ -105,13 +105,13 @@ class _XmpStructArrayCardState extends State { class XmpStructCard extends StatelessWidget { final String title; final Map struct; - final Map Function() linkifier; + final Map Function()? linkifier; static const cardMargin = EdgeInsets.symmetric(vertical: 8, horizontal: 0); const XmpStructCard({ - @required this.title, - @required this.struct, + required this.title, + required this.struct, this.linkifier, }); diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 1663aac44..bd58a4baf 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -10,14 +10,14 @@ import 'package:flutter/material.dart'; class XmpDirTile extends StatefulWidget { final AvesEntry entry; final SplayTreeMap tags; - final ValueNotifier expandedNotifier; + final ValueNotifier? expandedNotifier; final bool initiallyExpanded; const XmpDirTile({ - @required this.entry, - @required this.tags, - @required this.expandedNotifier, - @required this.initiallyExpanded, + required this.entry, + required this.tags, + required this.expandedNotifier, + required this.initiallyExpanded, }); @override @@ -29,7 +29,7 @@ class _XmpDirTileState extends State { @override Widget build(BuildContext context) { - final sections = groupBy(widget.tags.entries, (kv) { + final sections = groupBy, String>(widget.tags.entries, (kv) { final fullKey = kv.key; final i = fullKey.indexOf(XMP.propNamespaceSeparator); final namespace = i == -1 ? '' : fullKey.substring(0, i); diff --git a/lib/widgets/viewer/info/notifications.dart b/lib/widgets/viewer/info/notifications.dart index c5d23c7a3..3c0d422ad 100644 --- a/lib/widgets/viewer/info/notifications.dart +++ b/lib/widgets/viewer/info/notifications.dart @@ -21,7 +21,7 @@ class OpenTempEntryNotification extends Notification { final AvesEntry entry; const OpenTempEntryNotification({ - @required this.entry, + required this.entry, }); @override @@ -32,11 +32,11 @@ enum EmbeddedDataSource { motionPhotoVideo, videoCover, xmp } class OpenEmbeddedDataNotification extends Notification { final EmbeddedDataSource source; - final String propPath; - final String mimeType; + final String? propPath; + final String? mimeType; const OpenEmbeddedDataNotification._private({ - @required this.source, + required this.source, this.propPath, this.mimeType, }); @@ -50,8 +50,8 @@ class OpenEmbeddedDataNotification extends Notification { ); factory OpenEmbeddedDataNotification.xmp({ - @required String propPath, - @required String mimeType, + required String propPath, + required String mimeType, }) => OpenEmbeddedDataNotification._private( source: EmbeddedDataSource.xmp, diff --git a/lib/widgets/viewer/multipage/conductor.dart b/lib/widgets/viewer/multipage/conductor.dart index 709c3a711..da6915149 100644 --- a/lib/widgets/viewer/multipage/conductor.dart +++ b/lib/widgets/viewer/multipage/conductor.dart @@ -1,5 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; +import 'package:collection/collection.dart'; class MultiPageConductor { final List _controllers = []; @@ -7,7 +8,7 @@ class MultiPageConductor { static const maxControllerCount = 3; Future dispose() async { - await Future.forEach(_controllers, (controller) => controller.dispose()); + await Future.forEach(_controllers, (controller) => controller.dispose()); _controllers.clear(); } @@ -25,7 +26,7 @@ class MultiPageConductor { return controller; } - MultiPageController getController(AvesEntry entry) { - return _controllers.firstWhere((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId, orElse: () => null); + MultiPageController? getController(AvesEntry entry) { + return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); } } diff --git a/lib/widgets/viewer/multipage/controller.dart b/lib/widgets/viewer/multipage/controller.dart index 00224f8f5..d45029b69 100644 --- a/lib/widgets/viewer/multipage/controller.dart +++ b/lib/widgets/viewer/multipage/controller.dart @@ -8,23 +8,24 @@ import 'package:flutter/material.dart'; class MultiPageController { final AvesEntry entry; - final ValueNotifier pageNotifier = ValueNotifier(null); + final ValueNotifier pageNotifier = ValueNotifier(null); - MultiPageInfo _info; + MultiPageInfo? _info; - final StreamController _infoStreamController = StreamController.broadcast(); + final StreamController _infoStreamController = StreamController.broadcast(); - Stream get infoStream => _infoStreamController.stream; + Stream get infoStream => _infoStreamController.stream; - MultiPageInfo get info => _info; + MultiPageInfo? get info => _info; - int get page => pageNotifier.value; + int? get page => pageNotifier.value; - set page(int page) => pageNotifier.value = page; + set page(int? page) => pageNotifier.value = page; MultiPageController(this.entry) { metadataService.getMultiPageInfo(entry).then((value) { - pageNotifier.value = value.defaultPage.index; + if (value == null) return; + pageNotifier.value = value.defaultPage!.index; _info = value; _infoStreamController.add(_info); }); diff --git a/lib/widgets/viewer/overlay/bottom/common.dart b/lib/widgets/viewer/overlay/bottom/common.dart index ae870bd3b..01d1a7a6d 100644 --- a/lib/widgets/viewer/overlay/bottom/common.dart +++ b/lib/widgets/viewer/overlay/bottom/common.dart @@ -15,6 +15,7 @@ import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/bottom/multipage.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -24,17 +25,17 @@ class ViewerBottomOverlay extends StatefulWidget { final List entries; final int index; final bool showPosition; - final EdgeInsets viewInsets, viewPadding; - final MultiPageController multiPageController; + final EdgeInsets? viewInsets, viewPadding; + final MultiPageController? multiPageController; const ViewerBottomOverlay({ - Key key, - @required this.entries, - @required this.index, - @required this.showPosition, + Key? key, + required this.entries, + required this.index, + required this.showPosition, this.viewInsets, this.viewPadding, - @required this.multiPageController, + required this.multiPageController, }) : super(key: key); @override @@ -42,17 +43,17 @@ class ViewerBottomOverlay extends StatefulWidget { } class _ViewerBottomOverlayState extends State { - Future _detailLoader; - AvesEntry _lastEntry; - OverlayMetadata _lastDetails; + late Future _detailLoader; + AvesEntry? _lastEntry; + OverlayMetadata? _lastDetails; - AvesEntry get entry { + AvesEntry? get entry { final entries = widget.entries; final index = widget.index; return index < entries.length ? entries[index] : null; } - MultiPageController get multiPageController => widget.multiPageController; + MultiPageController? get multiPageController => widget.multiPageController; @override void initState() { @@ -69,7 +70,8 @@ class _ViewerBottomOverlayState extends State { } void _initDetailLoader() { - _detailLoader = metadataService.getOverlayMetadata(entry); + final requestEntry = entry; + _detailLoader = requestEntry != null ? metadataService.getOverlayMetadata(requestEntry) : SynchronousFuture(null); } @override @@ -91,7 +93,7 @@ class _ViewerBottomOverlayState extends State { return Container( color: hasEdgeContent ? kOverlayBackgroundColor : Colors.transparent, padding: viewInsets + viewPadding.copyWith(top: 0), - child: FutureBuilder( + child: FutureBuilder( future: _detailLoader, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) { @@ -100,9 +102,9 @@ class _ViewerBottomOverlayState extends State { } if (_lastEntry == null) return SizedBox.shrink(); - Widget _buildContent({MultiPageInfo multiPageInfo, int page}) => _BottomOverlayContent( - mainEntry: _lastEntry, - pageEntry: multiPageInfo?.getPageEntryByIndex(page) ?? _lastEntry, + Widget _buildContent({MultiPageInfo? multiPageInfo, int? page}) => _BottomOverlayContent( + mainEntry: _lastEntry!, + pageEntry: multiPageInfo?.getPageEntryByIndex(page) ?? _lastEntry!, details: _lastDetails, position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null, availableWidth: availableWidth, @@ -111,12 +113,12 @@ class _ViewerBottomOverlayState extends State { if (multiPageController == null) return _buildContent(); - return StreamBuilder( - stream: multiPageController.infoStream, + return StreamBuilder( + stream: multiPageController!.infoStream, builder: (context, snapshot) { - final multiPageInfo = multiPageController.info; - return ValueListenableBuilder( - valueListenable: multiPageController.pageNotifier, + final multiPageInfo = multiPageController!.info; + return ValueListenableBuilder( + valueListenable: multiPageController!.pageNotifier, builder: (context, page, child) { return _buildContent(multiPageInfo: multiPageInfo, page: page); }, @@ -139,20 +141,20 @@ const double _subRowMinWidth = 300.0; class _BottomOverlayContent extends AnimatedWidget { final AvesEntry mainEntry, pageEntry; - final OverlayMetadata details; - final String position; + final OverlayMetadata? details; + final String? position; final double availableWidth; - final MultiPageController multiPageController; + final MultiPageController? multiPageController; static const infoPadding = EdgeInsets.symmetric(vertical: 4, horizontal: 8); _BottomOverlayContent({ - Key key, - this.mainEntry, - this.pageEntry, + Key? key, + required this.mainEntry, + required this.pageEntry, this.details, this.position, - this.availableWidth, + required this.availableWidth, this.multiPageController, }) : super( key: key, @@ -165,7 +167,7 @@ class _BottomOverlayContent extends AnimatedWidget { @override Widget build(BuildContext context) { return DefaultTextStyle( - style: Theme.of(context).textTheme.bodyText2.copyWith( + style: Theme.of(context).textTheme.bodyText2!.copyWith( shadows: [Constants.embossShadow], ), softWrap: false, @@ -176,7 +178,7 @@ class _BottomOverlayContent extends AnimatedWidget { child: Selector( selector: (c, mq) => mq.orientation, builder: (c, orientation, child) { - Widget infoColumn; + Widget? infoColumn; if (settings.showOverlayInfo) { infoColumn = _buildInfoColumn(orientation); @@ -188,7 +190,7 @@ class _BottomOverlayContent extends AnimatedWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ MultiPageOverlay( - controller: multiPageController, + controller: multiPageController!, availableWidth: availableWidth, ), if (infoColumn != null) infoColumn, @@ -208,7 +210,7 @@ class _BottomOverlayContent extends AnimatedWidget { final twoColumns = orientation == Orientation.landscape && infoMaxWidth / 2 > _subRowMinWidth; final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth; final positionTitle = _PositionTitleRow(entry: pageEntry, collectionPosition: position, multiPageController: multiPageController); - final hasShootingDetails = details != null && !details.isEmpty && settings.showOverlayShootingDetails; + final hasShootingDetails = details != null && !details!.isEmpty && settings.showOverlayShootingDetails; return Padding( padding: infoPadding, @@ -271,7 +273,7 @@ class _BottomOverlayContent extends AnimatedWidget { ? Container( padding: EdgeInsets.only(top: _interRowPadding), width: subRowWidth, - child: _ShootingRow(details), + child: _ShootingRow(details!), ) : SizedBox.shrink(), ); @@ -287,7 +289,7 @@ class _BottomOverlayContent extends AnimatedWidget { child: hasShootingDetails ? Container( width: subRowWidth, - child: _ShootingRow(details), + child: _ShootingRow(details!), ) : SizedBox.shrink(), ); @@ -306,18 +308,13 @@ class _LocationRow extends AnimatedWidget { final AvesEntry entry; _LocationRow({ - Key key, - this.entry, + Key? key, + required this.entry, }) : super(key: key, listenable: entry.addressChangeNotifier); @override Widget build(BuildContext context) { - String location; - if (entry.hasAddress) { - location = entry.shortAddress; - } else if (entry.hasGps) { - location = settings.coordinateFormat.format(entry.latLng); - } + final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(entry.latLng!); return Row( children: [ DecoratedIcon(AIcons.location, shadows: [Constants.embossShadow], size: _iconSize), @@ -330,16 +327,16 @@ class _LocationRow extends AnimatedWidget { class _PositionTitleRow extends StatelessWidget { final AvesEntry entry; - final String collectionPosition; - final MultiPageController multiPageController; + final String? collectionPosition; + final MultiPageController? multiPageController; const _PositionTitleRow({ - @required this.entry, - @required this.collectionPosition, - @required this.multiPageController, + required this.entry, + required this.collectionPosition, + required this.multiPageController, }); - String get title => entry.bestTitle; + String? get title => entry.bestTitle; bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null; @@ -347,7 +344,7 @@ class _PositionTitleRow extends StatelessWidget { @override Widget build(BuildContext context) { - Text toText({String pagePosition}) => Text( + Text toText({String? pagePosition}) => Text( [ if (collectionPosition != null) collectionPosition, if (pagePosition != null) pagePosition, @@ -357,11 +354,11 @@ class _PositionTitleRow extends StatelessWidget { if (multiPageController == null) return toText(); - return StreamBuilder( - stream: multiPageController.infoStream, + return StreamBuilder( + stream: multiPageController!.infoStream, builder: (context, snapshot) { - final multiPageInfo = multiPageController.info; - String pagePosition; + final multiPageInfo = multiPageController!.info; + String? pagePosition; if (multiPageInfo != null) { // page count may be 0 when we know an entry to have multiple pages // but fail to get information about these pages @@ -379,11 +376,11 @@ class _PositionTitleRow extends StatelessWidget { class _DateRow extends StatelessWidget { final AvesEntry entry; - final MultiPageController multiPageController; + final MultiPageController? multiPageController; const _DateRow({ - @required this.entry, - @required this.multiPageController, + required this.entry, + required this.multiPageController, }); @override @@ -429,14 +426,14 @@ class _ShootingRow extends StatelessWidget { } class ExtraBottomOverlay extends StatelessWidget { - final EdgeInsets viewInsets, viewPadding; + final EdgeInsets? viewInsets, viewPadding; final Widget child; const ExtraBottomOverlay({ - Key key, + Key? key, this.viewInsets, this.viewPadding, - @required this.child, + required this.child, }) : super(key: key); @override diff --git a/lib/widgets/viewer/overlay/bottom/multipage.dart b/lib/widgets/viewer/overlay/bottom/multipage.dart index 0ac68df62..fc3a63589 100644 --- a/lib/widgets/viewer/overlay/bottom/multipage.dart +++ b/lib/widgets/viewer/overlay/bottom/multipage.dart @@ -13,11 +13,10 @@ class MultiPageOverlay extends StatefulWidget { final double availableWidth; const MultiPageOverlay({ - Key key, - @required this.controller, - @required this.availableWidth, - }) : assert(controller != null), - super(key: key); + Key? key, + required this.controller, + required this.availableWidth, + }) : super(key: key); @override _MultiPageOverlayState createState() => _MultiPageOverlayState(); @@ -25,9 +24,9 @@ class MultiPageOverlay extends StatefulWidget { class _MultiPageOverlayState extends State { final _cancellableNotifier = ValueNotifier(true); - ScrollController _scrollController; + late ScrollController _scrollController; bool _syncScroll = true; - int _initControllerPage; + int? _initControllerPage; static const double extent = 48; static const double separatorWidth = 2; @@ -75,8 +74,8 @@ class _MultiPageOverlayState extends State { await controller.infoStream.first; if (_initControllerPage == null) { _initControllerPage = controller.page; - if (_initControllerPage != 0) { - WidgetsBinding.instance.addPostFrameCallback((_) => _goToPage(_initControllerPage)); + if (_initControllerPage != null && _initControllerPage != 0) { + WidgetsBinding.instance!.addPostFrameCallback((_) => _goToPage(_initControllerPage!)); } } } @@ -88,14 +87,14 @@ class _MultiPageOverlayState extends State { @override Widget build(BuildContext context) { - final marginWidth = max(0, (availableWidth - extent) / 2 - separatorWidth); + final marginWidth = max(0.0, (availableWidth - extent) / 2 - separatorWidth); final horizontalMargin = SizedBox(width: marginWidth); final separator = SizedBox(width: separatorWidth); return ThumbnailTheme( extent: extent, showLocation: false, - child: StreamBuilder( + child: StreamBuilder( stream: controller.infoStream, builder: (context, snapshot) { final multiPageInfo = controller.info; @@ -112,7 +111,7 @@ class _MultiPageOverlayState extends State { itemBuilder: (context, index) { if (index == 0 || index == pageCount + 1) return horizontalMargin; final page = index - 1; - final pageEntry = multiPageInfo.getPageEntryByIndex(page); + final pageEntry = multiPageInfo!.getPageEntryByIndex(page); return Stack( children: [ diff --git a/lib/widgets/viewer/overlay/bottom/panorama.dart b/lib/widgets/viewer/overlay/bottom/panorama.dart index 4998f86c8..499a72280 100644 --- a/lib/widgets/viewer/overlay/bottom/panorama.dart +++ b/lib/widgets/viewer/overlay/bottom/panorama.dart @@ -11,9 +11,9 @@ class PanoramaOverlay extends StatelessWidget { final Animation scale; const PanoramaOverlay({ - Key key, - @required this.entry, - @required this.scale, + Key? key, + required this.entry, + required this.scale, }) : super(key: key); @override diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video.dart index 8327a1c74..80d865bf1 100644 --- a/lib/widgets/viewer/overlay/bottom/video.dart +++ b/lib/widgets/viewer/overlay/bottom/video.dart @@ -15,14 +15,14 @@ import 'package:flutter/material.dart'; class VideoControlOverlay extends StatefulWidget { final AvesEntry entry; - final AvesVideoController controller; + final AvesVideoController? controller; final Animation scale; const VideoControlOverlay({ - Key key, - @required this.entry, - @required this.controller, - @required this.scale, + Key? key, + required this.entry, + required this.controller, + required this.scale, }) : super(key: key); @override @@ -32,14 +32,14 @@ class VideoControlOverlay extends StatefulWidget { class _VideoControlOverlayState extends State with SingleTickerProviderStateMixin { final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar'); bool _playingOnDragStart = false; - AnimationController _playPauseAnimation; + late AnimationController _playPauseAnimation; final List _subscriptions = []; AvesEntry get entry => widget.entry; Animation get scale => widget.scale; - AvesVideoController get controller => widget.controller; + AvesVideoController? get controller => widget.controller; Stream get statusStream => controller?.statusStream ?? Stream.value(VideoStatus.idle); @@ -72,9 +72,10 @@ class _VideoControlOverlayState extends State with SingleTi } void _registerWidget(VideoControlOverlay widget) { - if (widget.controller != null) { - _subscriptions.add(widget.controller.statusStream.listen(_onStatusChange)); - _onStatusChange(widget.controller.status); + final controller = widget.controller; + if (controller != null) { + _subscriptions.add(controller.statusStream.listen(_onStatusChange)); + _onStatusChange(controller.status); } } @@ -142,13 +143,13 @@ class _VideoControlOverlayState extends State with SingleTi }, onHorizontalDragStart: (details) { _playingOnDragStart = isPlaying; - if (_playingOnDragStart) controller.pause(); + if (_playingOnDragStart) controller!.pause(); }, onHorizontalDragUpdate: (details) { _seekFromTap(details.globalPosition); }, onHorizontalDragEnd: (details) { - if (_playingOnDragStart) controller.play(); + if (_playingOnDragStart) controller!.play(); }, child: Container( padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16) + EdgeInsets.only(bottom: 16), @@ -166,7 +167,7 @@ class _VideoControlOverlayState extends State with SingleTi stream: positionStream, builder: (context, snapshot) { // do not use stream snapshot because it is obsolete when switching between videos - final position = controller?.currentPosition?.floor() ?? 0; + final position = controller?.currentPosition.floor() ?? 0; return Text(formatFriendlyDuration(Duration(milliseconds: position))); }), Spacer(), @@ -207,9 +208,9 @@ class _VideoControlOverlayState extends State with SingleTi Future _togglePlayPause() async { if (controller == null) return; if (isPlaying) { - await controller.pause(); + await controller!.pause(); } else { - await controller.play(); + await controller!.play(); // hide overlay await Future.delayed(Durations.iconAnimation); ToggleOverlayNotification().dispatch(context); @@ -218,9 +219,9 @@ class _VideoControlOverlayState extends State with SingleTi void _seekFromTap(Offset globalPosition) async { if (controller == null) return; - final keyContext = _progressBarKey.currentContext; - final RenderBox box = keyContext.findRenderObject(); + final keyContext = _progressBarKey.currentContext!; + final box = keyContext.findRenderObject() as RenderBox; final localPosition = box.globalToLocal(globalPosition); - await controller.seekToProgress(localPosition.dx / box.size.width); + await controller!.seekToProgress(localPosition.dx / box.size.width); } } diff --git a/lib/widgets/viewer/overlay/common.dart b/lib/widgets/viewer/overlay/common.dart index e2bcdd9d6..49e131a8c 100644 --- a/lib/widgets/viewer/overlay/common.dart +++ b/lib/widgets/viewer/overlay/common.dart @@ -9,9 +9,9 @@ class OverlayButton extends StatelessWidget { final Widget child; const OverlayButton({ - Key key, + Key? key, this.scale = kAlwaysCompleteAnimation, - @required this.child, + required this.child, }) : super(key: key); @override @@ -41,15 +41,14 @@ class OverlayButton extends StatelessWidget { class OverlayTextButton extends StatelessWidget { final Animation scale; final String buttonLabel; - final VoidCallback onPressed; + final VoidCallback? onPressed; const OverlayTextButton({ - Key key, - @required this.scale, - @required this.buttonLabel, + Key? key, + required this.scale, + required this.buttonLabel, this.onPressed, - }) : assert(scale != null), - super(key: key); + }) : super(key: key); static const _borderRadius = 123.0; static final _minSize = MaterialStateProperty.all(Size(kMinInteractiveDimension, kMinInteractiveDimension)); diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index 0f1431e39..f383f9979 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -13,8 +13,8 @@ class Minimap extends StatelessWidget { static const defaultSize = Size(96, 96); const Minimap({ - @required this.entry, - @required this.viewStateNotifier, + required this.entry, + required this.viewStateNotifier, this.size = defaultSize, }); @@ -33,7 +33,7 @@ class Minimap extends StatelessWidget { viewportSize: viewportSize, entrySize: entry.displaySize, viewCenterOffset: viewState.position, - viewScale: viewState.scale, + viewScale: viewState.scale!, minimapBorderColor: Colors.white30, ), size: size, @@ -51,16 +51,13 @@ class MinimapPainter extends CustomPainter { final Color minimapBorderColor, viewportBorderColor; const MinimapPainter({ - @required this.viewportSize, - @required this.entrySize, - @required this.viewCenterOffset, - @required this.viewScale, + required this.viewportSize, + required this.entrySize, + required this.viewCenterOffset, + required this.viewScale, this.minimapBorderColor = Colors.white, this.viewportBorderColor = Colors.white, - }) : assert(viewportSize != null), - assert(entrySize != null), - assert(viewCenterOffset != null), - assert(viewScale != null); + }); @override void paint(Canvas canvas, Size size) { diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index e04854eb6..a30e70a89 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -20,22 +20,22 @@ import 'package:provider/provider.dart'; class ViewerTopOverlay extends StatelessWidget { final AvesEntry mainEntry; final Animation scale; - final EdgeInsets viewInsets, viewPadding; + final EdgeInsets? viewInsets, viewPadding; final Function(EntryAction value) onActionSelected; final bool canToggleFavourite; - final ValueNotifier viewStateNotifier; + final ValueNotifier? viewStateNotifier; static const double padding = 8; const ViewerTopOverlay({ - Key key, - @required this.mainEntry, - @required this.scale, - @required this.canToggleFavourite, - @required this.viewInsets, - @required this.viewPadding, - @required this.onActionSelected, - @required this.viewStateNotifier, + Key? key, + required this.mainEntry, + required this.scale, + required this.canToggleFavourite, + required this.viewInsets, + required this.viewPadding, + required this.onActionSelected, + required this.viewStateNotifier, }) : super(key: key); @override @@ -49,15 +49,15 @@ class ViewerTopOverlay extends StatelessWidget { builder: (c, mqWidth, child) { final availableCount = (mqWidth / (OverlayButton.getSize(context) + padding)).floor() - 2; - Widget child; + Widget? child; if (mainEntry.isMultiPage) { final multiPageController = context.read().getController(mainEntry); if (multiPageController != null) { - child = StreamBuilder( + child = StreamBuilder( stream: multiPageController.infoStream, builder: (context, snapshot) { final multiPageInfo = multiPageController.info; - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { return _buildOverlay(availableCount, mainEntry, pageEntry: multiPageInfo?.getPageEntryByIndex(page)); @@ -75,11 +75,11 @@ class ViewerTopOverlay extends StatelessWidget { ); } - Widget _buildOverlay(int availableCount, AvesEntry mainEntry, {AvesEntry pageEntry}) { + Widget _buildOverlay(int availableCount, AvesEntry mainEntry, {AvesEntry? pageEntry}) { pageEntry ??= mainEntry; bool _canDo(EntryAction action) { - final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; + final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry! : mainEntry; switch (action) { case EntryAction.toggleFavourite: return canToggleFavourite; @@ -106,7 +106,6 @@ class ViewerTopOverlay extends StatelessWidget { case EntryAction.debug: return kDebugMode; } - return false; } final quickActions = settings.viewerQuickActions.where(_canDo).take(availableCount).toList(); @@ -132,7 +131,7 @@ class ViewerTopOverlay extends StatelessWidget { opacity: scale, child: Minimap( entry: pageEntry, - viewStateNotifier: viewStateNotifier, + viewStateNotifier: viewStateNotifier!, ), ) ], @@ -148,14 +147,14 @@ class _TopOverlayRow extends StatelessWidget { final Function(EntryAction value) onActionSelected; const _TopOverlayRow({ - Key key, - @required this.quickActions, - @required this.inAppActions, - @required this.externalAppActions, - @required this.scale, - @required this.mainEntry, - @required this.pageEntry, - @required this.onActionSelected, + Key? key, + required this.quickActions, + required this.inAppActions, + required this.externalAppActions, + required this.scale, + required this.mainEntry, + required this.pageEntry, + required this.onActionSelected, }) : super(key: key); static const double padding = 8; @@ -195,7 +194,7 @@ class _TopOverlayRow extends StatelessWidget { } Widget _buildOverlayButton(BuildContext context, EntryAction action) { - Widget child; + Widget? child; void onPressed() => onActionSelected(action); switch (action) { case EntryAction.toggleFavourite: @@ -239,7 +238,7 @@ class _TopOverlayRow extends StatelessWidget { } PopupMenuEntry _buildPopupMenuItem(BuildContext context, EntryAction action) { - Widget child; + Widget? child; switch (action) { // in app actions case EntryAction.toggleFavourite: @@ -313,10 +312,10 @@ class _TopOverlayRow extends StatelessWidget { class _FavouriteToggler extends StatefulWidget { final AvesEntry entry; final bool isMenuItem; - final VoidCallback onPressed; + final VoidCallback? onPressed; const _FavouriteToggler({ - @required this.entry, + required this.entry, this.isMenuItem = false, this.onPressed, }); @@ -326,7 +325,7 @@ class _FavouriteToggler extends StatefulWidget { } class _FavouriteTogglerState extends State<_FavouriteToggler> { - final ValueNotifier isFavouriteNotifier = ValueNotifier(null); + final ValueNotifier isFavouriteNotifier = ValueNotifier(false); @override void initState() { diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 9672a4ccc..9daab4b28 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -20,8 +20,8 @@ class PanoramaPage extends StatefulWidget { final PanoramaInfo info; const PanoramaPage({ - @required this.entry, - @required this.info, + required this.entry, + required this.info, }); @override @@ -40,7 +40,7 @@ class _PanoramaPageState extends State { void initState() { super.initState(); _overlayVisible.addListener(_onOverlayVisibleChange); - WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay()); + WidgetsBinding.instance!.addPostFrameCallback((_) => _initOverlay()); } @override @@ -65,11 +65,11 @@ class _PanoramaPageState extends State { builder: (context, sensorControl, child) { return Panorama( sensorControl: sensorControl, - croppedArea: info.hasCroppedArea ? info.croppedAreaRect : Rect.fromLTWH(0.0, 0.0, 1.0, 1.0), - croppedFullWidth: info.hasCroppedArea ? info.fullPanoSize.width : 1.0, - croppedFullHeight: info.hasCroppedArea ? info.fullPanoSize.height : 1.0, + croppedArea: info.hasCroppedArea ? info.croppedAreaRect! : Rect.fromLTWH(0.0, 0.0, 1.0, 1.0), + croppedFullWidth: info.hasCroppedArea ? info.fullPanoSize!.width : 1.0, + croppedFullHeight: info.hasCroppedArea ? info.fullPanoSize!.height : 1.0, onTap: (longitude, latitude, tilt) => _overlayVisible.value = !_overlayVisible.value, - child: child, + child: child as Image?, ); }, child: Image( @@ -148,7 +148,7 @@ class _PanoramaPageState extends State { Future _initOverlay() async { // wait for MaterialPageRoute.transitionDuration // to show overlay after page animation is complete - await Future.delayed(ModalRoute.of(context).transitionDuration * timeDilation); + await Future.delayed(ModalRoute.of(context)!.transitionDuration * timeDilation); await _onOverlayVisibleChange(); } diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index da21d2174..ec640d9a3 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -33,7 +33,7 @@ class EntryPrinter with FeedbackMixin { Future> _buildPages(BuildContext context) async { final pages = []; - void _addPdfPage(pdf.Widget pdfChild) { + void _addPdfPage(pdf.Widget? pdfChild) { if (pdfChild == null) return; final displaySize = entry.displaySize; pages.add(pdf.Page( @@ -49,20 +49,22 @@ class EntryPrinter with FeedbackMixin { if (entry.isMultiPage && !entry.isMotionPhoto) { final multiPageInfo = await metadataService.getMultiPageInfo(entry); - final pageCount = multiPageInfo.pageCount; - if (pageCount > 1) { - final streamController = StreamController.broadcast(); - showOpReport( - context: context, - opStream: streamController.stream, - itemCount: pageCount, - ); - for (var page = 0; page < pageCount; page++) { - final pageEntry = multiPageInfo.getPageEntryByIndex(page); - _addPdfPage(await _buildPageImage(pageEntry)); - streamController.sink.add(pageEntry); + if (multiPageInfo != null) { + final pageCount = multiPageInfo.pageCount; + if (pageCount > 1) { + final streamController = StreamController.broadcast(); + showOpReport( + context: context, + opStream: streamController.stream, + itemCount: pageCount, + ); + for (var page = 0; page < pageCount; page++) { + final pageEntry = multiPageInfo.getPageEntryByIndex(page); + _addPdfPage(await _buildPageImage(pageEntry)); + streamController.sink.add(pageEntry); + } + await streamController.close(); } - await streamController.close(); } } if (pages.isEmpty) { @@ -71,10 +73,10 @@ class EntryPrinter with FeedbackMixin { return pages; } - Future _buildPageImage(AvesEntry entry) async { + Future _buildPageImage(AvesEntry entry) async { if (entry.isSvg) { final bytes = await imageFileService.getSvg(entry.uri, entry.mimeType); - if (bytes != null && bytes.isNotEmpty) { + if (bytes.isNotEmpty) { return pdf.SvgImage(svg: utf8.decode(bytes)); } } else { diff --git a/lib/widgets/viewer/source_viewer_page.dart b/lib/widgets/viewer/source_viewer_page.dart index 62b40b044..41f4d3113 100644 --- a/lib/widgets/viewer/source_viewer_page.dart +++ b/lib/widgets/viewer/source_viewer_page.dart @@ -9,7 +9,7 @@ class SourceViewerPage extends StatefulWidget { final Future Function() loader; const SourceViewerPage({ - @required this.loader, + required this.loader, }); @override @@ -17,7 +17,7 @@ class SourceViewerPage extends StatefulWidget { } class _SourceViewerPageState extends State { - Future _loader; + late Future _loader; @override void initState() { @@ -38,7 +38,7 @@ class _SourceViewerPageState extends State { if (snapshot.hasError) return Text(snapshot.error.toString()); if (!snapshot.hasData) return SizedBox.shrink(); - final source = snapshot.data; + final source = snapshot.data!; final highlightView = AvesHighlightView( source, language: 'xml', diff --git a/lib/widgets/viewer/video/conductor.dart b/lib/widgets/viewer/video/conductor.dart index 7a026aac5..0b46c9301 100644 --- a/lib/widgets/viewer/video/conductor.dart +++ b/lib/widgets/viewer/video/conductor.dart @@ -1,6 +1,8 @@ import 'package:aves/model/entry.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart'; +import 'package:collection/collection.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:fijkplayer/fijkplayer.dart'; class VideoConductor { @@ -13,7 +15,7 @@ class VideoConductor { } Future dispose() async { - await Future.forEach(_controllers, (controller) => controller.dispose()); + await Future.forEach(_controllers, (controller) => controller.dispose()); _controllers.clear(); } @@ -31,9 +33,9 @@ class VideoConductor { return controller; } - AvesVideoController getController(AvesEntry entry) { - return _controllers.firstWhere((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId, orElse: () => null); + AvesVideoController? getController(AvesEntry entry) { + return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); } - Future pauseAll() => Future.forEach(_controllers, (controller) => controller.pause()); + Future pauseAll() => Future.forEach(_controllers, (controller) => controller.pause()); } diff --git a/lib/widgets/viewer/video/controller.dart b/lib/widgets/viewer/video/controller.dart index 3b586908b..73048e113 100644 --- a/lib/widgets/viewer/video/controller.dart +++ b/lib/widgets/viewer/video/controller.dart @@ -33,7 +33,7 @@ abstract class AvesVideoController { int get currentPosition; - double get progress => (currentPosition ?? 0).toDouble() / duration; + double get progress => currentPosition.toDouble() / duration; Stream get positionStream; diff --git a/lib/widgets/viewer/video/fijkplayer.dart b/lib/widgets/viewer/video/fijkplayer.dart index 11fc20ba6..27efb7d20 100644 --- a/lib/widgets/viewer/video/fijkplayer.dart +++ b/lib/widgets/viewer/video/fijkplayer.dart @@ -8,23 +8,25 @@ import 'package:aves/model/video/keys.dart'; import 'package:aves/model/video/metadata.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:collection/collection.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; class IjkPlayerAvesVideoController extends AvesVideoController { - FijkPlayer _instance; + late FijkPlayer _instance; final List _subscriptions = []; final StreamController _valueStreamController = StreamController.broadcast(); final AChangeNotifier _completedNotifier = AChangeNotifier(); Offset _macroBlockCrop = Offset.zero; final List _streams = []; - final ValueNotifier _selectedVideoStream = ValueNotifier(null); - final ValueNotifier _selectedAudioStream = ValueNotifier(null); - final ValueNotifier _selectedTextStream = ValueNotifier(null); - final ValueNotifier> _sar = ValueNotifier(null); - Timer _initialPlayTimer; + final ValueNotifier _selectedVideoStream = ValueNotifier(null); + final ValueNotifier _selectedAudioStream = ValueNotifier(null); + final ValueNotifier _selectedTextStream = ValueNotifier(null); + final ValueNotifier> _sar = ValueNotifier(Tuple2(1, 1)); + Timer? _initialPlayTimer; Stream get _valueStream => _valueStreamController.stream; @@ -80,7 +82,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { // playing with HW acceleration seems to skip the last frames of some videos // so HW acceleration is always disabled for gif-like videos where the last frames may be significant - final hwAccelerationEnabled = settings.enableVideoHardwareAcceleration && entry.durationMillis > gifLikeVideoDurationThreshold.inMilliseconds; + final hwAccelerationEnabled = settings.enableVideoHardwareAcceleration && entry.durationMillis! > gifLikeVideoDurationThreshold.inMilliseconds; // TODO TLAD HW codecs sometimes fail when seek-starting some videos, e.g. MP2TS/h264(HDPR) if (hwAccelerationEnabled) { @@ -142,12 +144,12 @@ class IjkPlayerAvesVideoController extends AvesVideoController { } }); - StreamSummary _getSelectedStream(String selectedIndexKey) { + StreamSummary? _getSelectedStream(String selectedIndexKey) { final indexString = mediaInfo[selectedIndexKey]; if (indexString != null) { final index = int.tryParse(indexString); if (index != null && index != -1) { - return _streams.firstWhere((stream) => stream.index == index, orElse: () => null); + return _streams.firstWhereOrNull((stream) => stream.index == index); } } return null; @@ -158,12 +160,12 @@ class IjkPlayerAvesVideoController extends AvesVideoController { _selectedTextStream.value = _getSelectedStream(Keys.selectedTextStream); if (_selectedVideoStream.value != null) { - final streamIndex = _selectedVideoStream.value.index; - final streamInfo = allStreams.firstWhere((stream) => stream[Keys.index] == streamIndex, orElse: () => null); + final streamIndex = _selectedVideoStream.value!.index; + final streamInfo = allStreams.firstWhereOrNull((stream) => stream[Keys.index] == streamIndex); if (streamInfo != null) { - final num = streamInfo[Keys.sarNum]; - final den = streamInfo[Keys.sarDen]; - _sar.value = Tuple2((num ?? 0) != 0 ? num : 1, (den ?? 0) != 0 ? den : 1); + final num = streamInfo[Keys.sarNum] ?? 0; + final den = streamInfo[Keys.sarDen] ?? 0; + _sar.value = Tuple2(num != 0 ? num : 1, den != 0 ? den : 1); } } } @@ -219,7 +221,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { int get duration { final controllerDuration = _instance.value.duration.inMilliseconds; // use expected duration when controller duration is not set yet - return (controllerDuration == null || controllerDuration == 0) ? entry.durationMillis : controllerDuration; + return (controllerDuration == 0) ? entry.durationMillis! : controllerDuration; } @override @@ -288,7 +290,6 @@ extension ExtraIjkStatus on FijkState { case FijkState.error: return VideoStatus.error; } - return VideoStatus.idle; } } @@ -321,7 +322,7 @@ extension ExtraFijkPlayer on FijkPlayer { enum StreamType { video, audio, text } extension ExtraStreamType on StreamType { - static StreamType fromTypeString(String type) { + static StreamType? fromTypeString(String? type) { switch (type) { case StreamTypes.video: return StreamType.video; @@ -338,14 +339,14 @@ extension ExtraStreamType on StreamType { class StreamSummary { final StreamType type; - final int index; - final String language, title; + final int? index; + final String? language, title; const StreamSummary({ - @required this.type, - @required this.index, - @required this.language, - @required this.title, + required this.type, + required this.index, + required this.language, + required this.title, }); @override diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 92f960cea..1bfaf9176 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -30,15 +30,15 @@ import 'package:provider/provider.dart'; class EntryPageView extends StatefulWidget { final AvesEntry mainEntry, pageEntry; - final Size viewportSize; - final VoidCallback onDisposed; + final Size? viewportSize; + final VoidCallback? onDisposed; static const decorationCheckSize = 20.0; const EntryPageView({ - Key key, - this.mainEntry, - this.pageEntry, + Key? key, + required this.mainEntry, + required this.pageEntry, this.viewportSize, this.onDisposed, }) : super(key: key); @@ -48,7 +48,7 @@ class EntryPageView extends StatefulWidget { } class _EntryPageViewState extends State { - MagnifierController _magnifierController; + late MagnifierController _magnifierController; final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); final List _subscriptions = []; @@ -56,7 +56,7 @@ class _EntryPageViewState extends State { AvesEntry get entry => widget.pageEntry; - Size get viewportSize => widget.viewportSize; + Size? get viewportSize => widget.viewportSize; static const initialScale = ScaleLevel(ref: ScaleReference.contained); static const minScale = ScaleLevel(ref: ScaleReference.contained); @@ -96,7 +96,7 @@ class _EntryPageViewState extends State { minScale: minScale, maxScale: maxScale, initialScale: initialScale, - viewportSize: viewportSize, + viewportSize: viewportSize!, childSize: entry.displaySize, ).initialScale, viewportSize, @@ -109,7 +109,7 @@ class _EntryPageViewState extends State { } void _unregisterWidget() { - _magnifierController?.dispose(); + _magnifierController.dispose(); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -120,7 +120,7 @@ class _EntryPageViewState extends State { final child = AnimatedBuilder( animation: entry.imageChangeNotifier, builder: (context, child) { - Widget child; + Widget? child; if (entry.isSvg) { child = _buildSvgView(); } else if (!entry.displaySize.isEmpty) { @@ -138,11 +138,11 @@ class _EntryPageViewState extends State { }, ); - return Consumer( + return Consumer( builder: (context, info, child) => Hero( - tag: info?.entry == mainEntry ? hashValues(info.collectionId, mainEntry) : hashCode, + tag: info != null && info.entry == mainEntry ? hashValues(info.collectionId, mainEntry) : hashCode, transitionOnUserGestures: true, - child: child, + child: child!, ), child: child, ); @@ -230,7 +230,7 @@ class _EntryPageViewState extends State { ScaleLevel maxScale = maxScale, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, bool applyScale = true, - @required Widget child, + required Widget child, }) { return Magnifier( // key includes modified date to refresh when the image is modified by metadata (e.g. rotated) diff --git a/lib/widgets/viewer/visual/error.dart b/lib/widgets/viewer/visual/error.dart index 1cbcc81ce..54f480fdf 100644 --- a/lib/widgets/viewer/visual/error.dart +++ b/lib/widgets/viewer/visual/error.dart @@ -12,8 +12,8 @@ class ErrorView extends StatefulWidget { final VoidCallback onTap; const ErrorView({ - @required this.entry, - @required this.onTap, + required this.entry, + required this.onTap, }); @override @@ -21,20 +21,20 @@ class ErrorView extends StatefulWidget { } class _ErrorViewState extends State { - Future _exists; + late Future _exists; AvesEntry get entry => widget.entry; @override void initState() { super.initState(); - _exists = entry.path != null ? File(entry.path).exists() : SynchronousFuture(true); + _exists = entry.path != null ? File(entry.path!).exists() : SynchronousFuture(true); } @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => widget.onTap?.call(), + onTap: () => widget.onTap(), // use container to expand constraints, so that the user can tap anywhere child: Container( // opaque to cover potential lower quality layer below @@ -43,7 +43,7 @@ class _ErrorViewState extends State { future: _exists, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) return SizedBox(); - final exists = snapshot.data; + final exists = snapshot.data!; return EmptyContent( icon: exists ? AIcons.error : AIcons.broken, text: exists ? context.l10n.viewerErrorUnknown : context.l10n.viewerErrorDoesNotExist, diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 77bf923b5..e1b7bb90a 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -21,9 +21,9 @@ class RasterImageView extends StatefulWidget { final ImageErrorWidgetBuilder errorBuilder; const RasterImageView({ - @required this.entry, - @required this.viewStateNotifier, - @required this.errorBuilder, + required this.entry, + required this.viewStateNotifier, + required this.errorBuilder, }); @override @@ -31,13 +31,13 @@ class RasterImageView extends StatefulWidget { } class _RasterImageViewState extends State { - Size _displaySize; + late Size _displaySize; bool _isTilingInitialized = false; - int _maxSampleSize; - double _tileSide; - Matrix4 _tileTransform; - ImageStream _fullImageStream; - ImageStreamListener _fullImageListener; + late int _maxSampleSize; + late double _tileSide; + Matrix4? _tileTransform; + ImageStream? _fullImageStream; + late ImageStreamListener _fullImageListener; final ValueNotifier _fullImageLoaded = ValueNotifier(false); AvesEntry get entry => widget.entry; @@ -91,7 +91,7 @@ class _RasterImageViewState extends State { void _registerFullImage() { _fullImageStream = fullImageProvider.resolve(ImageConfiguration.empty); - _fullImageStream.addListener(_fullImageListener); + _fullImageStream!.addListener(_fullImageListener); } void _unregisterFullImage() { @@ -106,18 +106,16 @@ class _RasterImageViewState extends State { @override Widget build(BuildContext context) { - if (viewStateNotifier == null) return SizedBox.shrink(); - final useTiles = entry.useTiles; return ValueListenableBuilder( valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; final viewportSized = viewportSize?.isEmpty == false; - if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize); + if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize!); return SizedBox.fromSize( - size: _displaySize * viewState.scale, + size: _displaySize * viewState.scale!, child: Stack( alignment: Alignment.center, children: [ @@ -129,7 +127,7 @@ class _RasterImageViewState extends State { image: fullImageProvider, gaplessPlayback: true, errorBuilder: widget.errorBuilder, - width: (_displaySize * viewState.scale).width, + width: (_displaySize * viewState.scale!).width, fit: BoxFit.contain, filterQuality: FilterQuality.medium, ), @@ -161,7 +159,7 @@ class _RasterImageViewState extends State { } Widget _buildLoading() { - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { if (fullImageLoaded) return SizedBox.shrink(); @@ -181,10 +179,8 @@ class _RasterImageViewState extends State { } Widget _buildBackground() { - final viewportSize = viewState.viewportSize; - assert(viewportSize != null); - - final viewSize = _displaySize * viewState.scale; + final viewportSize = viewState.viewportSize!; + final viewSize = _displaySize * viewState.scale!; final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position; // deflate as a quick way to prevent background bleed final decorationSize = (applyBoxFit(BoxFit.none, viewSize, viewportSize).source - Offset(.5, .5)) as Size; @@ -195,7 +191,7 @@ class _RasterImageViewState extends State { final side = viewportSize.shortestSide; final checkSize = side / ((side / EntryPageView.decorationCheckSize).round()); final offset = ((decorationSize - viewportSize) as Offset) / 2; - child = ValueListenableBuilder( + child = ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { if (!fullImageLoaded) return SizedBox.shrink(); @@ -230,7 +226,7 @@ class _RasterImageViewState extends State { final displayWidth = _displaySize.width.round(); final displayHeight = _displaySize.height.round(); final viewRect = _getViewRect(displayWidth, displayHeight); - final scale = viewState.scale; + final scale = viewState.scale!; // for the largest sample size (matching the initial scale), the whole image is in view // so we subsample the whole image without tiling @@ -271,9 +267,9 @@ class _RasterImageViewState extends State { } Rect _getViewRect(int displayWidth, int displayHeight) { - final scale = viewState.scale; + final scale = viewState.scale!; final centerOffset = viewState.position; - final viewportSize = viewState.viewportSize; + final viewportSize = viewState.viewportSize!; final viewOrigin = Offset( ((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx), ((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy), @@ -281,14 +277,14 @@ class _RasterImageViewState extends State { return viewOrigin & viewportSize; } - Tuple2> _getTileRects({ - @required int x, - @required int y, - @required int regionSide, - @required int displayWidth, - @required int displayHeight, - @required double scale, - @required Rect viewRect, + Tuple2>? _getTileRects({ + required int x, + required int y, + required int regionSide, + required int displayWidth, + required int displayHeight, + required double scale, + required Rect viewRect, }) { final nextX = x + regionSide; final nextY = y + regionSide; @@ -303,8 +299,8 @@ class _RasterImageViewState extends State { if (_tileTransform != null) { // apply EXIF orientation final regionRectDouble = Rect.fromLTWH(x.toDouble(), y.toDouble(), thisRegionWidth.toDouble(), thisRegionHeight.toDouble()); - final tl = MatrixUtils.transformPoint(_tileTransform, regionRectDouble.topLeft); - final br = MatrixUtils.transformPoint(_tileTransform, regionRectDouble.bottomRight); + final tl = MatrixUtils.transformPoint(_tileTransform!, regionRectDouble.topLeft); + final br = MatrixUtils.transformPoint(_tileTransform!, regionRectDouble.bottomRight); regionRect = Rectangle.fromPoints( Point(tl.dx.round(), tl.dy.round()), Point(br.dx.round(), br.dy.round()), @@ -330,14 +326,14 @@ class RegionTile extends StatefulWidget { // `tileRect` uses Flutter view coordinates // `regionRect` uses the raw image pixel coordinates final Rect tileRect; - final Rectangle regionRect; + final Rectangle? regionRect; final int sampleSize; const RegionTile({ - @required this.entry, - @required this.tileRect, + required this.entry, + required this.tileRect, this.regionRect, - @required this.sampleSize, + required this.sampleSize, }); @override @@ -354,7 +350,7 @@ class RegionTile extends StatefulWidget { } class _RegionTileState extends State { - RegionProvider _provider; + late RegionProvider _provider; AvesEntry get entry => widget.entry; @@ -388,15 +384,13 @@ class _RegionTileState extends State { } void _initProvider() { - if (!entry.canDecode) return; - _provider = entry.getRegion( sampleSize: widget.sampleSize, region: widget.regionRect, ); } - void _pauseProvider() => _provider?.pause(); + void _pauseProvider() => _provider.pause(); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/visual/state.dart b/lib/widgets/viewer/visual/state.dart index 97760372f..2bcc3d3a4 100644 --- a/lib/widgets/viewer/visual/state.dart +++ b/lib/widgets/viewer/visual/state.dart @@ -3,8 +3,8 @@ import 'package:flutter/widgets.dart'; class ViewState { final Offset position; - final double scale; - final Size viewportSize; + final double? scale; + final Size? viewportSize; static const ViewState zero = ViewState(Offset.zero, 0, null); diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index 1fe9d3e28..da5cfa639 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -10,9 +10,9 @@ class VectorViewCheckeredBackground extends StatelessWidget { final Widget child; const VectorViewCheckeredBackground({ - @required this.displaySize, - @required this.viewStateNotifier, - @required this.child, + required this.displaySize, + required this.viewStateNotifier, + required this.child, }); @override @@ -21,12 +21,12 @@ class VectorViewCheckeredBackground extends StatelessWidget { valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; - if (viewportSize == null) return child; + if (viewportSize == null) return child!; final side = viewportSize.shortestSide; final checkSize = side / ((side / EntryPageView.decorationCheckSize).round()); - final viewSize = displaySize * viewState.scale; + final viewSize = displaySize * viewState.scale!; final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source; final offset = ((decorationSize - viewportSize) as Offset) / 2; @@ -43,7 +43,7 @@ class VectorViewCheckeredBackground extends StatelessWidget { ), ), ), - child, + child!, ], ); }, diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index eda8c6d2a..f2ad00b7e 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -7,9 +7,9 @@ class VideoView extends StatefulWidget { final AvesVideoController controller; const VideoView({ - Key key, - @required this.entry, - @required this.controller, + Key? key, + required this.entry, + required this.controller, }) : super(key: key); @override @@ -50,7 +50,6 @@ class _VideoViewState extends State { @override Widget build(BuildContext context) { - if (controller == null) return SizedBox(); return StreamBuilder( stream: controller.statusStream, builder: (context, snapshot) { diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 5b8fe9181..a83af8490 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -22,7 +22,7 @@ class WelcomePage extends StatefulWidget { class _WelcomePageState extends State { bool _hasAcceptedTerms = false; - Future _termsLoader; + late Future _termsLoader; @override void initState() { @@ -42,7 +42,7 @@ class _WelcomePageState extends State { future: _termsLoader, builder: (context, snapshot) { if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final terms = snapshot.data; + final terms = snapshot.data!; return Column( mainAxisSize: MainAxisSize.min, children: _toStaggeredList( @@ -102,13 +102,17 @@ class _WelcomePageState extends State { children: [ LabeledCheckbox( value: settings.isCrashlyticsEnabled, - onChanged: (v) => setState(() => settings.isCrashlyticsEnabled = v), + onChanged: (v) { + if (v != null) setState(() => settings.isCrashlyticsEnabled = v); + }, text: context.l10n.welcomeAnalyticsToggle, ), LabeledCheckbox( key: Key('agree-checkbox'), value: _hasAcceptedTerms, - onChanged: (v) => setState(() => _hasAcceptedTerms = v), + onChanged: (v) { + if (v != null) setState(() => _hasAcceptedTerms = v); + }, text: context.l10n.welcomeTermsToggle, ), ], @@ -171,7 +175,7 @@ class _WelcomePageState extends State { data: terms, selectable: true, onTapLink: (text, href, title) async { - if (await canLaunch(href)) { + if (href != null && await canLaunch(href)) { await launch(href); } }, @@ -186,10 +190,10 @@ class _WelcomePageState extends State { // as of flutter_staggered_animations v0.1.2, `AnimationConfiguration.toStaggeredList` does not handle `Flexible` widgets // so we use this workaround instead static List _toStaggeredList({ - Duration duration, - Duration delay, - @required Widget Function(Widget) childAnimationBuilder, - @required List children, + required Duration duration, + required Duration delay, + required Widget Function(Widget) childAnimationBuilder, + required List children, }) => children .asMap() diff --git a/pubspec.yaml b/pubspec.yaml index 51670fcd7..05375f8c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.4.1+45 publish_to: none environment: - sdk: '>=2.10.0 <3.0.0' + sdk: '>=2.12.0 <3.0.0' # TODO TLAD switch to Flutter stable when possible, currently on dev/beta because of the following mess: # printing >=5.0.1 depends on pdf ^3.0.1, pdf >=3.0.1 depends on crypto ^3.0.0 and archive ^3.1.0 diff --git a/test/fake/image_file_service.dart b/test/fake/image_file_service.dart index 4c4716ae4..88d6803e0 100644 --- a/test/fake/image_file_service.dart +++ b/test/fake/image_file_service.dart @@ -7,14 +7,14 @@ import 'media_store_service.dart'; class FakeImageFileService extends Fake implements ImageFileService { @override - Future rename(AvesEntry entry, String newName) { + Future> rename(AvesEntry entry, String newName) { final contentId = FakeMediaStoreService.nextContentId; return SynchronousFuture({ 'uri': 'content://media/external/images/media/$contentId', 'contentId': contentId, 'path': '${entry.directory}/$newName', 'displayName': newName, - 'title': newName.substring(0, newName.length - entry.extension.length), + 'title': newName.substring(0, newName.length - entry.extension!.length), 'dateModifiedSecs': FakeMediaStoreService.dateSecs, }); } diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart index fce6490e6..2fc34b787 100644 --- a/test/fake/media_store_service.dart +++ b/test/fake/media_store_service.dart @@ -12,7 +12,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { Future> checkObsoleteContentIds(List knownContentIds) => SynchronousFuture([]); @override - Future> checkObsoletePaths(Map knownPathById) => SynchronousFuture([]); + Future> checkObsoletePaths(Map knownPathById) => SynchronousFuture([]); @override Stream getEntries(Map knownEntries) => Stream.fromIterable(entries); @@ -52,7 +52,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { 'deletedSource': true, 'uri': 'content://media/external/images/media/$newContentId', 'contentId': newContentId, - 'path': entry.path.replaceFirst(sourceAlbum, destinationAlbum), + 'path': entry.path!.replaceFirst(sourceAlbum, destinationAlbum), 'displayName': '${entry.filenameWithoutExtension}${entry.extension}', 'title': entry.filenameWithoutExtension, 'dateModifiedSecs': FakeMediaStoreService.dateSecs, diff --git a/test/fake/metadata_db.dart b/test/fake/metadata_db.dart index 6b0d8b0a5..3c738bbce 100644 --- a/test/fake/metadata_db.dart +++ b/test/fake/metadata_db.dart @@ -1,6 +1,7 @@ import 'package:aves/model/covers.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:flutter/foundation.dart'; @@ -8,19 +9,19 @@ import 'package:flutter_test/flutter_test.dart'; class FakeMetadataDb extends Fake implements MetadataDb { @override - Future init() => null; + Future init() => SynchronousFuture(true); @override - Future removeIds(Set contentIds, {@required bool metadataOnly}) => null; + Future removeIds(Set contentIds, {required bool metadataOnly}) => SynchronousFuture(true); @override Future> loadEntries() => SynchronousFuture({}); @override - Future saveEntries(Iterable entries) => null; + Future saveEntries(Iterable entries) => SynchronousFuture(true); @override - Future updateEntryId(int oldId, AvesEntry entry) => null; + Future updateEntryId(int oldId, AvesEntry entry) => SynchronousFuture(true); @override Future> loadDates() => SynchronousFuture([]); @@ -29,38 +30,38 @@ class FakeMetadataDb extends Fake implements MetadataDb { Future> loadMetadataEntries() => SynchronousFuture([]); @override - Future saveMetadata(Iterable metadataEntries) => null; + Future saveMetadata(Set metadataEntries) => SynchronousFuture(true); @override - Future updateMetadataId(int oldId, CatalogMetadata metadata) => null; + Future updateMetadataId(int oldId, CatalogMetadata? metadata) => SynchronousFuture(true); @override Future> loadAddresses() => SynchronousFuture([]); @override - Future updateAddressId(int oldId, AddressDetails address) => null; + Future updateAddressId(int oldId, AddressDetails? address) => SynchronousFuture(true); @override Future> loadFavourites() => SynchronousFuture({}); @override - Future addFavourites(Iterable rows) => null; + Future addFavourites(Iterable rows) => SynchronousFuture(true); @override - Future updateFavouriteId(int oldId, FavouriteRow row) => null; + Future updateFavouriteId(int oldId, FavouriteRow row) => SynchronousFuture(true); @override - Future removeFavourites(Iterable rows) => null; + Future removeFavourites(Iterable rows) => SynchronousFuture(true); @override Future> loadCovers() => SynchronousFuture({}); @override - Future addCovers(Iterable rows) => null; + Future addCovers(Iterable rows) => SynchronousFuture(true); @override - Future updateCoverEntryId(int oldId, CoverRow row) => null; + Future updateCoverEntryId(int oldId, CoverRow row) => SynchronousFuture(true); @override - Future removeCovers(Iterable rows) => null; + Future removeCovers(Set filters) => SynchronousFuture(true); } diff --git a/test/fake/metadata_service.dart b/test/fake/metadata_service.dart index 766f27caa..684e41d9d 100644 --- a/test/fake/metadata_service.dart +++ b/test/fake/metadata_service.dart @@ -1,9 +1,10 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/services/metadata_service.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; class FakeMetadataService extends Fake implements MetadataService { @override - Future getCatalogMetadata(AvesEntry entry, {bool background = false}) => null; + Future getCatalogMetadata(AvesEntry entry, {bool background = false}) => SynchronousFuture(null); } diff --git a/test/fake/storage_service.dart b/test/fake/storage_service.dart index e8dddc162..cc494fe26 100644 --- a/test/fake/storage_service.dart +++ b/test/fake/storage_service.dart @@ -17,12 +17,14 @@ class FakeStorageService extends Fake implements StorageService { description: primaryDescription, isPrimary: true, isRemovable: false, + state: 'fake', ), StorageVolume( path: removablePath, description: removableDescription, isPrimary: false, isRemovable: true, + state: 'fake', ), }); } diff --git a/test/geo/countries_test.dart b/test/geo/countries_test.dart index e81d5e7c2..c1dda9647 100644 --- a/test/geo/countries_test.dart +++ b/test/geo/countries_test.dart @@ -7,7 +7,7 @@ void main() { // [lng, lat, z] const buenosAires = [-58.381667, -34.603333]; const paris = [2.348777, 48.875683]; - const seoul = [126.99, 37.56, 42]; + const seoul = [126.99, 37.56, 42.0]; const argentinaN3String = '032'; const franceN3String = '250'; const southKoreaN3String = '410'; @@ -15,10 +15,13 @@ void main() { test('Parse countries', () async { TestWidgetsFlutterBinding.ensureInitialized(); final topo = await countryTopology.getTopology(); + expect(topo != null, true); + if (topo == null) return; + final countries = topo.objects['countries'] as GeometryCollection; final argentina = countries.geometries.firstWhere((geometry) => geometry.id == argentinaN3String); - expect(argentina.properties['name'], 'Argentina'); + expect(argentina.properties!['name'], 'Argentina'); expect(argentina.containsPoint(topo, buenosAires), true); expect(argentina.containsPoint(topo, seoul), false); }); diff --git a/test/geo/topojson_test.dart b/test/geo/topojson_test.dart index 124435d7b..9606a3e7f 100644 --- a/test/geo/topojson_test.dart +++ b/test/geo/topojson_test.dart @@ -92,6 +92,9 @@ void main() { test('parse example', () async { final topo = await TopoJson().parse(example1); + expect(topo != null, true); + if (topo == null) return; + expect(topo.objects.containsKey('example'), true); final exampleObj = topo.objects['example'] as GeometryCollection; @@ -105,12 +108,15 @@ void main() { final polygon = exampleObj.geometries[2] as Polygon; expect(polygon.arcs.first, [-2]); - expect(polygon.properties.containsKey('prop0'), true); + expect(polygon.properties!.containsKey('prop0'), true); }); test('parse quantized example', () async { final topo = await TopoJson().parse(example1Quantized); + expect(topo != null, true); + if (topo == null) return; + expect(topo.arcs.first.first, [4000, 0]); - expect(topo.transform.scale, [0.0005000500050005, 0.00010001000100010001]); + expect(topo.transform!.scale, [0.0005000500050005, 0.00010001000100010001]); }); } diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index 874a128f1..069a3e54c 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -1,3 +1,4 @@ +// @dart=2.9 import 'dart:async'; import 'package:aves/model/availability.dart'; diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index 2e12a3422..f5271b933 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -1,3 +1,4 @@ +// @dart=2.9 import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; diff --git a/test/widget_test.dart b/test/widget_test.dart index abe19491a..db070e29b 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,4 +1,4 @@ -import 'package:aves/main.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/test_driver/app.dart b/test_driver/app.dart index 23aaba037..e6be0dc8b 100644 --- a/test_driver/app.dart +++ b/test_driver/app.dart @@ -1,3 +1,4 @@ +// @dart=2.9 import 'dart:ui'; import 'package:aves/main.dart' as app; diff --git a/test_driver/app_test.dart b/test_driver/app_test.dart index f650626c9..07e12f358 100644 --- a/test_driver/app_test.dart +++ b/test_driver/app_test.dart @@ -8,7 +8,7 @@ import 'constants.dart'; import 'utils/adb_utils.dart'; import 'utils/driver_extension.dart'; -FlutterDriver driver; +late FlutterDriver driver; void main() { group('[Aves app]', () { @@ -26,7 +26,7 @@ void main() { tearDownAll(() async { await removeDirectory(targetPicturesDir); - unawaited(driver?.close()); + unawaited(driver.close()); }); agreeToTerms(); diff --git a/test_driver/utils/adb_utils.dart b/test_driver/utils/adb_utils.dart index 3377223e5..a663f6ea9 100644 --- a/test_driver/utils/adb_utils.dart +++ b/test_driver/utils/adb_utils.dart @@ -5,7 +5,7 @@ import 'package:path/path.dart' as p; String get adb { final env = Platform.environment; // e.g. C:\Users\\AppData\Local\Android\Sdk - final sdkDir = env['ANDROID_SDK_ROOT'] ?? env['ANDROID_SDK']; + final sdkDir = env['ANDROID_SDK_ROOT'] ?? env['ANDROID_SDK']!; return p.join(sdkDir, 'platform-tools', Platform.isWindows ? 'adb.exe' : 'adb'); } @@ -40,7 +40,7 @@ Future copyContent(String sourceDir, String targetDir) async { // only works in debug mode Future grantPermissions(String packageName, Iterable permissions) async { - await Future.forEach(permissions, (permission) => runAdb(['shell', 'pm', 'grant', packageName, permission])); + await Future.forEach(permissions, (permission) => runAdb(['shell', 'pm', 'grant', packageName, permission])); } Future pressDeviceBackButton() => runAdb(['shell', 'input', 'keyevent', 'KEYCODE_BACK']); diff --git a/test_driver/utils/driver_extension.dart b/test_driver/utils/driver_extension.dart index 9e10657c9..3d70e7569 100644 --- a/test_driver/utils/driver_extension.dart +++ b/test_driver/utils/driver_extension.dart @@ -3,7 +3,7 @@ import 'package:flutter_driver/flutter_driver.dart'; extension ExtraFlutterDriver on FlutterDriver { static const doubleTapDelay = Duration(milliseconds: 100); // in [kDoubleTapMinTime = 40 ms, kDoubleTapTimeout = 300 ms] - Future doubleTap(SerializableFinder finder, {Duration timeout}) async { + Future doubleTap(SerializableFinder finder, {Duration? timeout}) async { await tap(finder, timeout: timeout); await Future.delayed(doubleTapDelay); await tap(finder, timeout: timeout);