packages upgrade, migration to sound null safety
This commit is contained in:
parent
b965063c8c
commit
a87db49c99
37 changed files with 149 additions and 136 deletions
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
@ -31,4 +31,4 @@ jobs:
|
|||
run: flutter analyze
|
||||
|
||||
- name: Unit tests.
|
||||
run: flutter test --no-sound-null-safety
|
||||
run: flutter test
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @dart=2.9
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}';
|
||||
|
|
9
lib/utils/collection_utils.dart
Normal file
9
lib/utils/collection_utils.dart
Normal 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]};
|
||||
}
|
|
@ -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>(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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()
|
||||
|
|
20
pubspec.lock
20
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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});
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue