packages upgrade, migration to sound null safety

This commit is contained in:
Thibault Deckers 2021-07-08 16:15:43 +09:00
parent b965063c8c
commit a87db49c99
37 changed files with 149 additions and 136 deletions

View file

@ -31,4 +31,4 @@ jobs:
run: flutter analyze
- name: Unit tests.
run: flutter test --no-sound-null-safety
run: flutter test

View file

@ -38,7 +38,7 @@ jobs:
run: flutter analyze
- name: Unit tests.
run: flutter test --no-sound-null-safety
run: flutter test
- name: Build signed artifacts.
# `KEY_JKS` should contain the result of:

View file

@ -37,15 +37,10 @@ class CountryTopology {
final numericMap = await numericCodeMap(positions);
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<MapEntry<CountryCode, Set<LatLng>>>();
return Map.fromEntries(codeMapEntries);
return Map.fromEntries(numericMap.entries.map((kv) {
final code = _countryOfNumeric(kv.key);
return code != null ? MapEntry(code, kv.value) : null;
}).whereNotNull());
}
// returns a map of the given positions by the ISO 3166-1 numeric code of the country containing them

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
// cf https://github.com/topojson/topojson-specification
@ -57,16 +58,11 @@ class Topology extends TopologyJsonObject {
final Transform? transform;
Topology.parse(Map<String, dynamic> data)
: objects = Map.fromEntries((data['objects'] as Map)
.cast<String, dynamic>()
.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<MapEntry<String, Geometry>>()),
: objects = Map.fromEntries((data['objects'] as Map).cast<String, dynamic>().entries.map((kv) {
final name = kv.key;
final geometry = Geometry.build(kv.value);
return geometry != null ? MapEntry(name, geometry) : null;
}).whereNotNull()),
arcs = (data['arcs'] as List).cast<List>().map((arc) => arc.cast<List>().map((position) => position.cast<num>()).toList()).toList(),
transform = data.containsKey('transform') ? Transform.parse((data['transform'] as Map).cast<String, dynamic>()) : null,
super.parse(data);
@ -244,7 +240,7 @@ class GeometryCollection extends Geometry {
final List<Geometry> geometries;
GeometryCollection.parse(Map<String, dynamic> data)
: geometries = (data['geometries'] as List).cast<Map<String, dynamic>>().map(Geometry.build).where((geometry) => geometry != null).cast<Geometry>().toList(),
: geometries = (data['geometries'] as List).cast<Map<String, dynamic>>().map(Geometry.build).whereNotNull().toList(),
super.parse(data);
@override

View file

@ -1,4 +1,3 @@
// @dart=2.9
import 'dart:isolate';
import 'package:aves/widgets/aves_app.dart';

View file

@ -541,7 +541,7 @@ class AvesEntry {
_addressDetails?.countryName,
_addressDetails?.adminArea,
_addressDetails?.locality,
}.where((part) => part != null && part.isNotEmpty).join(', ');
}.whereNotNull().where((v) => v.isNotEmpty).join(', ');
}
bool search(String query) => {

View file

@ -7,6 +7,7 @@ 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';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
@ -430,7 +431,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future<Set<CoverRow>> loadCovers() async {
final db = await _database;
final maps = await db.query(coverTable);
final rows = maps.map(CoverRow.fromMap).where((v) => v != null).cast<CoverRow>().toSet();
final rows = maps.map(CoverRow.fromMap).whereNotNull().toSet();
return rows;
}

View file

@ -230,11 +230,11 @@ class Settings extends ChangeNotifier {
set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString());
Set<CollectionFilter> get pinnedFilters => (_prefs!.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast<CollectionFilter>().toSet();
Set<CollectionFilter> get pinnedFilters => (_prefs!.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
set pinnedFilters(Set<CollectionFilter> newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());
Set<CollectionFilter> get hiddenFilters => (_prefs!.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast<CollectionFilter>().toSet();
Set<CollectionFilter> get hiddenFilters => (_prefs!.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
set hiddenFilters(Set<CollectionFilter> newValue) => setAndNotify(hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList());
@ -326,7 +326,7 @@ class Settings extends ChangeNotifier {
set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue);
List<CollectionFilter> get searchHistory => (_prefs!.getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast<CollectionFilter>().toList();
List<CollectionFilter> get searchHistory => (_prefs!.getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList();
set searchHistory(List<CollectionFilter> newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList());
@ -351,8 +351,8 @@ class Settings extends ChangeNotifier {
return defaultValue;
}
List<T> getEnumListOrDefault<T>(String key, List<T> defaultValue, Iterable<T> values) {
return _prefs!.getStringList(key)?.map((s) => values.firstWhereOrNull((v) => v.toString() == s)).where((v) => v != null).cast<T>().toList() ?? defaultValue;
List<T> getEnumListOrDefault<T extends Object>(String key, List<T> defaultValue, Iterable<T> values) {
return _prefs!.getStringList(key)?.map((s) => values.firstWhereOrNull((v) => v.toString() == s)).whereNotNull().toList() ?? defaultValue;
}
void setAndNotify(String key, dynamic newValue) {

View file

@ -37,7 +37,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin {
Iterable<CollectionFilter?>? filters,
this.id,
this.listenToSource = true,
}) : filters = (filters ?? {}).where((f) => f != null).cast<CollectionFilter>().toSet(),
}) : filters = (filters ?? {}).whereNotNull().toSet(),
groupFactor = settings.collectionGroupFactor,
sortFactor = settings.collectionSortFactor {
id ??= hashCode;

View file

@ -204,8 +204,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
}
});
await metadataDb.saveEntries(movedEntries);
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata).where((v) => v != null).cast<CatalogMetadata>().toSet());
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).where((v) => v != null).cast<AddressDetails>().toSet());
await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata).whereNotNull().toSet());
await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).whereNotNull().toSet());
} else {
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
final newFields = movedOp.newFields;

View file

@ -132,8 +132,8 @@ mixin LocationMixin on SourceBase {
}
void updateLocations() {
final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).cast<AddressDetails>().toList();
final updatedPlaces = locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase as int Function(String?, String?)?);
final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).whereNotNull().toList();
final updatedPlaces = locations.map((address) => address.place).whereNotNull().where((v) => v.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase);
if (!listEquals(updatedPlaces, sortedPlaces)) {
sortedPlaces = List.unmodifiable(updatedPlaces);
eventBus.fire(PlacesChangedEvent());
@ -142,7 +142,10 @@ 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) {
final code = address.countryCode;
return code?.isNotEmpty == true ? MapEntry(code, address.countryName) : null;
}).whereNotNull());
final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase);
if (!listEquals(updatedCountries, sortedCountries)) {
sortedCountries = List.unmodifiable(updatedCountries);
@ -163,7 +166,7 @@ mixin LocationMixin on SourceBase {
_filterEntryCountMap.clear();
_filterRecentEntryMap.clear();
} else {
countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails!.countryCode).where((v) => v != null).cast<String>().toSet();
countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull().toSet();
countryCodes.forEach(_filterEntryCountMap.remove);
}
eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes));

View file

@ -121,22 +121,19 @@ class MediaStoreSource extends CollectionSource {
Future<Set<String>> refreshUris(Set<String> changedUris) async {
if (!_initialized || !isMonitoring) return changedUris;
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<MapEntry<int, String>>());
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);
}).whereNotNull());
// clean up obsolete entries
final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(uriByContentId.keys.toList())).toSet();
final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).where((v) => v != null).cast<String>().toSet();
final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).whereNotNull().toSet();
await removeEntries(obsoleteUris);
obsoleteContentIds.forEach(uriByContentId.remove);
@ -183,8 +180,8 @@ class MediaStoreSource extends CollectionSource {
@override
Future<void> refreshMetadata(Set<AvesEntry> entries) {
final contentIds = entries.map((entry) => entry.contentId).toSet();
metadataDb.removeIds(contentIds as Set<int>, metadataOnly: true);
final contentIds = entries.map((entry) => entry.contentId).whereNotNull().toSet();
metadataDb.removeIds(contentIds, metadataOnly: true);
return refresh();
}
}

View file

@ -71,7 +71,10 @@ 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).cast<MapEntry<String, String>>(),
..._textElements.map((name) {
final value = root.getElement(name)?.text;
return value != null ? MapEntry(formatKey(name), value) : null;
}).whereNotNull(),
]);
final metadata = root.getElement(_metadataElement);

View file

@ -1,5 +1,4 @@
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 {

View file

@ -113,7 +113,7 @@ class Package {
currentLabel,
englishLabel,
...ownedDirs,
].where((dir) => dir != null).cast<String>().toSet();
].whereNotNull().toSet();
@override
String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}';

View file

@ -0,0 +1,9 @@
import 'package:collection/collection.dart';
extension ExtraMapNullableKey<K extends Object, V> on Map<K?, V> {
Map<K, V> whereNotNullKey() => <K, V>{for (var v in keys.whereNotNull()) v: this[v]!};
}
extension ExtraMapNullableKeyValue<K extends Object, V> on Map<K?, V?> {
Map<K, V?> whereNotNullKey() => <K, V?>{for (var v in keys.whereNotNull()) v: this[v]};
}

View file

@ -68,7 +68,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final source = collection.source;
final selection = collection.selection;
final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).cast<String>().toSet();
final selectionDirs = selection.map((e) => e.directory).whereNotNull().toSet();
if (moveType == MoveType.move) {
// check whether moving is possible given OS restrictions,
// before asking to pick a destination album
@ -178,7 +178,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final collection = context.read<CollectionLens>();
final source = collection.source;
final selection = collection.selection;
final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).cast<String>().toSet();
final selectionDirs = selection.map((e) => e.directory).whereNotNull().toSet();
final todoCount = selection.length;
final confirmed = await showDialog<bool>(

View file

@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
mixin PermissionAwareMixin {
Future<bool> checkStoragePermission(BuildContext context, Set<AvesEntry> entries) {
return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).cast<String>().toSet());
return checkStoragePermissionForAlbums(context, entries.map((e) => e.directory).whereNotNull().toSet());
}
Future<bool> checkStoragePermissionForAlbums(BuildContext context, Set<String> albumPaths) async {

View file

@ -5,6 +5,7 @@ import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/services/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/collection_utils.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
@ -35,7 +36,7 @@ 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 = Map.fromEntries(groupBy<AvesEntry, StorageVolume?>(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)).entries.where((kv) => kv.key != null).cast<MapEntry<StorageVolume, List<AvesEntry>>>());
final byVolume = groupBy<AvesEntry, StorageVolume?>(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)).whereNotNullKey();
final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume);
final fromOtherVolumes = otherVolumes.fold<int>(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

View file

@ -41,8 +41,8 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
super.initState();
final entries = collection.sortedEntries;
if (entries.isNotEmpty) {
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;
final coverEntries = filters.map(covers.coverContentId).whereNotNull().map((id) => entries.firstWhereOrNull((entry) => entry.contentId == id)).whereNotNull();
_coverEntry = coverEntries.firstOrNull ?? entries.first;
}
_nameController.text = widget.defaultName;
_validate();

View file

@ -32,7 +32,7 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
// check width/height to exclude image streams (that are included among video streams)
_videoStreams = (byType[StreamType.video] ?? []).where((v) => v.width != null && v.height != null).toList();
_audioStreams = (byType[StreamType.audio] ?? []);
_textStreams = (byType[StreamType.text] ?? [])..insert(0, null);
_textStreams = [null, ...byType[StreamType.text] ?? []];
final streamEntries = widget.streams.entries;
_currentVideo = streamEntries.firstWhereOrNull((kv) => kv.key.type == StreamType.video && kv.value)?.key;

View file

@ -101,12 +101,13 @@ class AlbumListPage extends StatelessWidget {
return specialKey;
}
});
sections = Map.fromEntries({
sections = {
// group ordering
specialKey: sections[specialKey],
appsKey: sections[appsKey],
regularKey: sections[regularKey],
}.entries.where((kv) => kv.value != null).cast<MapEntry<ChipSectionKey, List<FilterGridItem<AlbumFilter>>>>());
if (sections.containsKey(specialKey)) specialKey: sections[specialKey]!,
if (sections.containsKey(appsKey)) appsKey: sections[appsKey]!,
if (sections.containsKey(regularKey)) regularKey: sections[regularKey]!,
};
break;
case AlbumChipGroupFactor.volume:
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {

View file

@ -19,6 +19,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/search/expandable_filter_row.dart';
import 'package:aves/widgets/search/search_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
@ -100,7 +101,7 @@ class CollectionSearchDelegate {
filters: [
queryFilter,
...visibleTypeFilters,
].where((f) => f != null && containQuery(f.getLabel(context))).cast<CollectionFilter>().toList(),
].whereNotNull().where((f) => containQuery(f.getLabel(context))).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,

View file

@ -16,7 +16,6 @@ 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';
@ -166,7 +165,7 @@ class StatsPage extends StatelessWidget {
children: [
charts.PieChart(
series,
defaultRenderer: charts.ArcRendererConfig(
defaultRenderer: charts.ArcRendererConfig<String>(
arcWidth: 16,
),
),

View file

@ -226,7 +226,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
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).cast<String>().toSet().toList()..sort(compareAsciiUpperCase);
final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase);
return MapEntry(key, '$count items: ${names.join(', ')}');
});
directories.add(MetadataDirectory('Attachments', null, _toSortedTags(rawTags)));
@ -237,15 +237,12 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
}
SplayTreeMap<String, String> _toSortedTags(Map rawTags) {
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<MapEntry<String, String>>()));
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);
}).whereNotNull()));
return tags;
}
}

View file

@ -62,8 +62,7 @@ class XmpNamespace {
final prop = XmpProp(kv.key, kv.value);
return extractData(prop) ? null : prop;
})
.where((v) => v != null)
.cast<XmpProp>()
.whereNotNull()
.toList()
..sort((a, b) => compareAsciiUpperCaseNatural(a.displayKey, b.displayKey));

View file

@ -12,26 +12,23 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
@override
Map<String, InfoLinkHandler> linkifyValues(List<XmpProp> props) {
return Map.fromEntries(dataProps
.map((t) {
final dataPropPath = t.item1;
final mimePropPath = t.item2;
final dataProp = props.firstWhereOrNull((prop) => prop.path == dataPropPath);
final mimeProp = props.firstWhereOrNull((prop) => prop.path == mimePropPath);
return (dataProp != null && mimeProp != null)
? MapEntry(
dataProp.displayKey,
InfoLinkHandler(
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
onTap: (context) => OpenEmbeddedDataNotification.xmp(
propPath: dataProp.path,
mimeType: mimeProp.value,
).dispatch(context),
))
: null;
})
.where((kv) => kv != null)
.cast<MapEntry<String, InfoLinkHandler>>());
return Map.fromEntries(dataProps.map((t) {
final dataPropPath = t.item1;
final mimePropPath = t.item2;
final dataProp = props.firstWhereOrNull((prop) => prop.path == dataPropPath);
final mimeProp = props.firstWhereOrNull((prop) => prop.path == mimePropPath);
return (dataProp != null && mimeProp != null)
? MapEntry(
dataProp.displayKey,
InfoLinkHandler(
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
onTap: (context) => OpenEmbeddedDataNotification.xmp(
propPath: dataProp.path,
mimeType: mimeProp.value,
).dispatch(context),
))
: null;
}).whereNotNull());
}
}

View file

@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
class XmpStructArrayCard extends StatefulWidget {
final String title;
final List<Map<String, String>> structs = [];
late final List<Map<String, String>> structs;
final Map<String, InfoLinkHandler> Function(int index)? linkifier;
XmpStructArrayCard({
@ -21,10 +21,7 @@ class XmpStructArrayCard extends StatefulWidget {
this.linkifier,
}) : super(key: key) {
final length = structByIndex.keys.fold(0, max);
structs.length = length;
for (var i = 0; i < length; i++) {
structs[i] = structByIndex[i + 1] ?? {};
}
structs = [for (var i = 0; i < length; i++) structByIndex[i + 1] ?? {}];
}
@override

View file

@ -59,12 +59,12 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
_staticInitialized = true;
}
_instance = FijkPlayer();
_valueStream.firstWhere((value) => value.videoRenderStart).then(
(value) => canCaptureFrameNotifier.value = true,
_valueStream.map((value) => value.videoRenderStart).firstWhere((v) => v, orElse: () => false).then(
(started) => canCaptureFrameNotifier.value = started,
onError: (error) {},
);
_valueStream.firstWhere((value) => value.audioRenderStart).then(
(value) => canSetSpeedNotifier.value = true,
_valueStream.map((value) => value.audioRenderStart).firstWhere((v) => v, orElse: () => false).then(
(started) => canSetSpeedNotifier.value = started,
onError: (error) {},
);
_startListening();

View file

@ -125,9 +125,9 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
Future<void> _showStreamSelectionDialog(BuildContext context, AvesVideoController controller) async {
final streams = controller.streams;
final currentSelectedStreams = await Future.wait(StreamType.values.map(controller.getSelectedStream));
final currentSelectedIndices = currentSelectedStreams.where((v) => v != null).cast<StreamSummary>().map((v) => v.index).toSet();
final currentSelectedIndices = currentSelectedStreams.whereNotNull().map((v) => v.index).toSet();
final userSelectedStreams = await showDialog<Map<StreamType, StreamSummary>>(
final userSelectedStreams = await showDialog<Map<StreamType, StreamSummary?>>(
context: context,
builder: (context) => VideoStreamSelectionDialog(
streams: Map.fromEntries(streams.map((stream) => MapEntry(stream, currentSelectedIndices.contains(stream.index)))),
@ -135,7 +135,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
);
if (userSelectedStreams == null || userSelectedStreams.isEmpty) return;
await Future.forEach<MapEntry<StreamType, StreamSummary>>(
await Future.forEach<MapEntry<StreamType, StreamSummary?>>(
userSelectedStreams.entries,
(kv) => controller.selectStream(kv.key, kv.value),
);

View file

@ -1,6 +1,7 @@
import 'package:aves/widgets/viewer/visual/subtitle/line.dart';
import 'package:aves/widgets/viewer/visual/subtitle/span.dart';
import 'package:aves/widgets/viewer/visual/subtitle/style.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
class AssParser {
@ -501,7 +502,7 @@ class AssParser {
pathPattern.allMatches(commands).forEach((match) {
if (match.groupCount == 2) {
final command = match.group(1)!;
final params = match.group(2)!.trim().split(' ').map(double.tryParse).where((v) => v != null).cast<double>().map((v) => v / scale).toList();
final params = match.group(2)!.trim().split(' ').map(double.tryParse).whereNotNull().map((v) => v / scale).toList();
switch (command) {
case 'b':
if (path != null) {
@ -565,7 +566,7 @@ class AssParser {
if (g != null) {
final params = g.split(',');
if (params.length == 4) {
final points = params.map(double.tryParse).where((v) => v != null).cast<double>().toList();
final points = params.map(double.tryParse).whereNotNull().toList();
if (points.length == 4) {
paths = [
Path()

View file

@ -70,21 +70,21 @@ packages:
name: charts_common
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.0"
version: "0.11.0"
charts_flutter:
dependency: "direct main"
description:
name: charts_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.0"
version: "0.11.0"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
version: "0.3.3"
clock:
dependency: transitive
description:
@ -182,7 +182,7 @@ packages:
name: decorated_icon
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.2.1"
event_bus:
dependency: "direct main"
description:
@ -301,7 +301,7 @@ packages:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "1.0.4"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -440,7 +440,7 @@ packages:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.3"
js:
dependency: transitive
description:
@ -503,7 +503,7 @@ packages:
name: material_design_icons_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.5955"
version: "5.0.5955-rc.1"
meta:
dependency: transitive
description:
@ -993,7 +993,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.8"
version: "6.0.9"
url_launcher_linux:
dependency: transitive
description:
@ -1014,7 +1014,7 @@ packages:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
version: "2.0.4"
url_launcher_web:
dependency: transitive
description:
@ -1084,7 +1084,7 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.4"
version: "2.2.5"
wkt_parser:
dependency: transitive
description:

View file

@ -7,9 +7,6 @@ publish_to: none
environment:
sdk: '>=2.12.0 <3.0.0'
# not null safe, as of 2021/06/07
# `charts_flutter` - https://github.com/google/charts/issues/579
dependencies:
flutter:
sdk: flutter
@ -41,7 +38,8 @@ dependencies:
google_maps_flutter:
intl:
latlong2:
material_design_icons_flutter:
# TODO TLAD as of 2021/07/08, MDI package null safe version is pre-release
material_design_icons_flutter: '>=5.0.5955-rc.1'
overlay_support:
package_info_plus:
palette_generator:

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,3 @@
// @dart=2.9
import 'dart:async';
import 'package:aves/model/availability.dart';
@ -137,7 +136,7 @@ void main() {
final source = await _initSource();
await image1.toggleFavourite();
final albumFilter = AlbumFilter(image1.directory, 'whatever');
final albumFilter = AlbumFilter(image1.directory!, 'whatever');
await covers.set(albumFilter, image1.contentId);
await source.removeEntries({image1.uri});

View file

@ -1,4 +1,3 @@
// @dart=2.9
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart';
@ -11,7 +10,7 @@ import 'package:test/test.dart';
void main() {
test('Filter serialization', () {
CollectionFilter jsonRoundTrip(filter) => CollectionFilter.fromJson(filter.toJson());
CollectionFilter? jsonRoundTrip(filter) => CollectionFilter.fromJson(filter.toJson());
const album = AlbumFilter('path/to/album', 'album');
expect(album, jsonRoundTrip(album));

View file

@ -1,10 +1,13 @@
// @dart=2.9
import 'dart:ui';
import 'package:aves/main.dart' as app;
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/services.dart';
import 'package:aves/services/storage_service.dart';
import 'package:aves/services/window_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/src/widgets/media_query.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'package:path/path.dart' as p;
@ -24,11 +27,30 @@ void main() {
}
Future<void> configureAndLaunch() async {
// set up fake services called during settings initialization
final fakeWindowService = FakeWindowService();
getIt.registerSingleton<WindowService>(fakeWindowService);
await settings.init();
settings.keepScreenOn = KeepScreenOn.always;
settings.hasAcceptedTerms = false;
settings.locale = const Locale('en');
settings.homePage = HomePageSetting.collection;
settings.imageBackground = EntryBackground.checkered;
// tear down fake services
getIt.unregister<WindowService>(instance: fakeWindowService);
app.main();
}
class FakeWindowService implements WindowService {
@override
Future<void> keepScreenOn(bool on) => SynchronousFuture(null);
@override
Future<bool> isRotationLocked() => SynchronousFuture(false);
@override
Future<void> requestOrientation([Orientation? orientation]) => SynchronousFuture(null);
}