upgraded Flutter to stable v3.10.0, agp 8, dart 3, use media query aspects

This commit is contained in:
Thibault Deckers 2023-05-13 17:39:39 +02:00
parent 01d2e21369
commit 28973ec322
208 changed files with 1905 additions and 1988 deletions

@ -1 +1 @@
Subproject commit 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
Subproject commit 84a1e904f44f9b0e9c4510138010edcc653163f8

View file

@ -7,10 +7,12 @@ All notable changes to this project will be documented in this file.
### Added
- option to set the Tags page as home
- support for animated PNG
### Changed
- remember whether to show the title filter when picking albums
- upgraded Flutter to stable v3.10.0
## <a id="v1.8.6"></a>[v1.8.6] - 2023-04-30

View file

@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id 'com.android.application'
id 'kotlin-android'
@ -46,7 +48,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace 'deckers.thibault.aves'
compileSdkVersion 33
compileSdk 33
ndkVersion flutter.ndkVersion
compileOptions {
@ -60,10 +62,6 @@ android {
disable 'InvalidPackage'
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
@ -174,6 +172,15 @@ android {
}
}
tasks.withType(KotlinCompile).configureEach {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
kotlinOptions {
jvmTarget = '1.8'
}
}
flutter {
source '../..'
}
@ -195,10 +202,10 @@ repositories {
}
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation "androidx.appcompat:appcompat:1.6.1"
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
implementation 'androidx.media:media:1.6.0'

View file

@ -8,7 +8,6 @@ This change eventually prevents building the app with Flutter v3.7.11.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="deckers.thibault.aves"
android:installLocation="auto">
<uses-feature

View file

@ -1,7 +1,7 @@
buildscript {
ext {
kotlin_version = '1.8.21'
agp_version = '7.4.2'
agp_version = '8.0.1'
glide_version = '4.15.1'
huawei_agconnect_version = '1.8.0.300'
abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]

View file

@ -15,3 +15,6 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip

View file

@ -27,7 +27,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
}
@override
ImageStreamCompleter loadBuffer(AppIconImageKey key, DecoderBufferCallback decode) {
ImageStreamCompleter loadImage(AppIconImageKey key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
@ -37,7 +37,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
);
}
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
Future<ui.Codec> _loadAsync(AppIconImageKey key, ImageDecoderCallback decode) async {
try {
final bytes = await appService.getAppIcon(key.packageName, key.size);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);

View file

@ -18,7 +18,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
}
@override
ImageStreamCompleter loadBuffer(RegionProviderKey key, DecoderBufferCallback decode) {
ImageStreamCompleter loadImage(RegionProviderKey key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
@ -28,7 +28,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
);
}
Future<ui.Codec> _loadAsync(RegionProviderKey key, DecoderBufferCallback decode) async {
Future<ui.Codec> _loadAsync(RegionProviderKey key, ImageDecoderCallback decode) async {
final uri = key.uri;
final mimeType = key.mimeType;
final pageId = key.pageId;

View file

@ -19,7 +19,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
}
@override
ImageStreamCompleter loadBuffer(ThumbnailProviderKey key, DecoderBufferCallback decode) {
ImageStreamCompleter loadImage(ThumbnailProviderKey key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
@ -30,7 +30,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
);
}
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderBufferCallback decode) async {
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, ImageDecoderCallback decode) async {
final uri = key.uri;
final mimeType = key.mimeType;
final pageId = key.pageId;

View file

@ -32,7 +32,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
}
@override
ImageStreamCompleter loadBuffer(UriImage key, DecoderBufferCallback decode) {
ImageStreamCompleter loadImage(UriImage key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
@ -45,7 +45,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
);
}
Future<ui.Codec> _loadAsync(UriImage key, DecoderBufferCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
Future<ui.Codec> _loadAsync(UriImage key, ImageDecoderCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
assert(key == this);
try {

View file

@ -21,34 +21,24 @@ class MetadataDbUpgrader {
switch (oldVersion) {
case 1:
await _upgradeFrom1(db);
break;
case 2:
await _upgradeFrom2(db);
break;
case 3:
await _upgradeFrom3(db);
break;
case 4:
await _upgradeFrom4(db);
break;
case 5:
await _upgradeFrom5(db);
break;
case 6:
await _upgradeFrom6(db);
break;
case 7:
await _upgradeFrom7(db);
break;
case 8:
await _upgradeFrom8(db);
break;
case 9:
await _upgradeFrom9(db);
break;
case 10:
await _upgradeFrom10(db);
break;
}
oldVersion++;
}

View file

@ -39,7 +39,7 @@ extension ExtraAvesEntryCatalog on AvesEntry {
if (isGeotiff && !hasGps) {
final info = await metadataFetchService.getGeoTiffInfo(this);
if (info != null) {
final center = MappedGeoTiff(
final center = GeoTiffCoordinateConverter(
info: info,
entry: this,
).center;

View file

@ -52,7 +52,6 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
case DateEditAction.copyItem:
case DateEditAction.extractFromTitle:
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
break;
case DateEditAction.shift:
final xmpDate = XMP.getString(descriptions, XmpAttributes.xmpCreateDate, namespace: XmpNamespaces.xmp);
if (xmpDate != null) {
@ -65,10 +64,8 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
reportService.recordError('failed to parse XMP date=$xmpDate', null);
}
}
break;
case DateEditAction.remove:
editCreateDateXmp(descriptions, null);
break;
}
return true;
}),
@ -541,10 +538,8 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
}
}
} on FileSystemException catch (_) {}
break;
default:
date = await metadataFetchService.getDate(this, source.toMetadataField()!);
break;
}
}
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;

View file

@ -78,7 +78,6 @@ class AlbumFilter extends CoveredCollectionFilter {
case AlbumType.app:
final appColor = colors.appColor(album);
if (appColor != null) return appColor;
break;
case AlbumType.camera:
return SynchronousFuture(colors.albumCamera);
case AlbumType.download:

View file

@ -21,13 +21,10 @@ class AspectRatioFilter extends CollectionFilter {
switch (op) {
case QueryFilter.opEqual:
_test = (entry) => entry.displayAspectRatio == threshold;
break;
case QueryFilter.opLower:
_test = (entry) => entry.displayAspectRatio < threshold;
break;
case QueryFilter.opGreater:
_test = (entry) => entry.displayAspectRatio > threshold;
break;
}
}

View file

@ -25,13 +25,10 @@ class DateFilter extends CollectionFilter {
switch (level) {
case DateLevel.y:
_test = (entry) => entry.bestDate?.isAtSameYearAs(_effectiveDate) ?? false;
break;
case DateLevel.ym:
_test = (entry) => entry.bestDate?.isAtSameMonthAs(_effectiveDate) ?? false;
break;
case DateLevel.ymd:
_test = (entry) => entry.bestDate?.isAtSameDayAs(_effectiveDate) ?? false;
break;
case DateLevel.md:
final month = _effectiveDate.month;
final day = _effectiveDate.day;
@ -39,15 +36,12 @@ class DateFilter extends CollectionFilter {
final bestDate = entry.bestDate;
return bestDate != null && bestDate.month == month && bestDate.day == day;
};
break;
case DateLevel.m:
final month = _effectiveDate.month;
_test = (entry) => entry.bestDate?.month == month;
break;
case DateLevel.d:
final day = _effectiveDate.day;
_test = (entry) => entry.bestDate?.day == day;
break;
}
}

View file

@ -29,13 +29,10 @@ class LocationFilter extends CoveredCollectionFilter {
switch (level) {
case LocationLevel.country:
_test = (entry) => entry.addressDetails?.countryCode == _code;
break;
case LocationLevel.state:
_test = (entry) => entry.addressDetails?.stateCode == _code;
break;
case LocationLevel.place:
_test = (entry) => entry.addressDetails?.place == _location;
break;
}
}
}
@ -57,7 +54,6 @@ class LocationFilter extends CoveredCollectionFilter {
if (_code != null) {
location = _nameAndCode;
}
break;
case LocationLevel.place:
break;
}

View file

@ -26,15 +26,12 @@ class MissingFilter extends CollectionFilter {
case _date:
_test = (entry) => (entry.catalogMetadata?.dateMillis ?? 0) == 0;
_icon = AIcons.dateUndated;
break;
case _fineAddress:
_test = (entry) => entry.hasGps && !entry.hasFineAddress;
_icon = AIcons.locationUnlocated;
break;
case _title:
_test = (entry) => (entry.catalogMetadata?.xmpTitle ?? '').isEmpty;
_icon = AIcons.descriptionUntitled;
break;
}
}

View file

@ -28,13 +28,10 @@ class PlaceholderFilter extends CollectionFilter {
switch (placeholder) {
case _country:
_icon = AIcons.country;
break;
case _state:
_icon = AIcons.state;
break;
case _place:
_icon = AIcons.place;
break;
}
}
@ -74,7 +71,6 @@ class PlaceholderFilter extends CollectionFilter {
case _place:
return address.place;
}
break;
}
return null;
}

View file

@ -117,7 +117,6 @@ class QueryFilter extends CollectionFilter {
if (op == opEqual) {
return (entry) => entry.contentId == valueInt;
}
break;
case keyContentYear:
if (valueInt == null) return null;
switch (op) {
@ -128,7 +127,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => (entry.bestDate?.year ?? 0) > valueInt;
}
break;
case keyContentMonth:
if (valueInt == null) return null;
switch (op) {
@ -139,7 +137,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => (entry.bestDate?.month ?? 0) > valueInt;
}
break;
case keyContentDay:
if (valueInt == null) return null;
switch (op) {
@ -150,7 +147,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => (entry.bestDate?.day ?? 0) > valueInt;
}
break;
case keyContentWidth:
if (valueInt == null) return null;
switch (op) {
@ -161,7 +157,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => entry.displaySize.width > valueInt;
}
break;
case keyContentHeight:
if (valueInt == null) return null;
switch (op) {
@ -172,7 +167,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => entry.displaySize.height > valueInt;
}
break;
case keyContentSize:
match = _fileSizePattern.firstMatch(valueString);
if (match == null) return null;
@ -187,13 +181,10 @@ class QueryFilter extends CollectionFilter {
switch (multiplierString) {
case 'K':
bytes *= kilo;
break;
case 'M':
bytes *= mega;
break;
case 'G':
bytes *= giga;
break;
}
switch (op) {
@ -204,7 +195,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => (entry.sizeBytes ?? 0) > bytes;
}
break;
}
return null;

View file

@ -37,27 +37,21 @@ class TypeFilter extends CollectionFilter {
case _animated:
_test = (entry) => entry.isAnimated;
_icon = AIcons.animated;
break;
case _geotiff:
_test = (entry) => entry.isGeotiff;
_icon = AIcons.geo;
break;
case _motionPhoto:
_test = (entry) => entry.isMotionPhoto;
_icon = AIcons.motionPhoto;
break;
case _panorama:
_test = (entry) => entry.isImage && entry.is360;
_icon = AIcons.panorama;
break;
case _raw:
_test = (entry) => entry.isRaw;
_icon = AIcons.raw;
break;
case _sphericalVideo:
_test = (entry) => entry.isVideo && entry.is360;
_icon = AIcons.sphericalVideo;
break;
}
}

View file

@ -8,8 +8,7 @@ import 'package:aves/ref/metadata/geotiff.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:aves_map/aves_map.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart';
import 'package:latlong2/latlong.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;
@ -42,19 +41,145 @@ class GeoTiffInfo extends Equatable {
class MappedGeoTiff with MapOverlay {
final AvesEntry entry;
late LatLng? Function(Point<int> pixel) pointToLatLng;
late Point<int>? Function(Point<double> smPoint) epsg3857ToPoint;
static final mapServiceTileSize = (256 * ui.window.devicePixelRatio).round();
static final mapServiceHelper = MapServiceHelper(mapServiceTileSize);
static final tileImagePaint = Paint();
static final tileMissingPaint = Paint()
late final GeoTiffCoordinateConverter _converter;
late final int _mapServiceTileSize;
late final MapServiceHelper _mapServiceHelper;
static final _tileImagePaint = Paint();
static final _tileMissingPaint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFF000000);
MappedGeoTiff({
required GeoTiffInfo info,
required this.entry,
required double devicePixelRatio,
}) {
_converter = GeoTiffCoordinateConverter(info: info, entry: entry);
_mapServiceTileSize = (256 * devicePixelRatio).round();
_mapServiceHelper = MapServiceHelper(_mapServiceTileSize);
}
@override
Future<MapTile?> getTile(int tx, int ty, int? zoomLevel) async {
zoomLevel ??= 0;
// global projected coordinates in meters (EPSG:3857 Spherical Mercator)
final tileTopLeft3857 = _mapServiceHelper.tileTopLeft(tx, ty, zoomLevel);
final tileBottomRight3857 = _mapServiceHelper.tileTopLeft(tx + 1, ty + 1, zoomLevel);
// image region coordinates in pixels
final tileTopLeftPx = _converter.epsg3857ToPoint(tileTopLeft3857);
final tileBottomRightPx = _converter.epsg3857ToPoint(tileBottomRight3857);
if (tileTopLeftPx == null || tileBottomRightPx == null) return null;
final tileLeft = tileTopLeftPx.x;
final tileRight = tileBottomRightPx.x;
final tileTop = tileTopLeftPx.y;
final tileBottom = tileBottomRightPx.y;
final width = entry.width;
final height = entry.height;
final regionLeft = tileLeft.clamp(0, width);
final regionRight = tileRight.clamp(0, width);
final regionTop = tileTop.clamp(0, height);
final regionBottom = tileBottom.clamp(0, height);
final regionWidth = regionRight - regionLeft;
final regionHeight = regionBottom - regionTop;
if (regionWidth == 0 || regionHeight == 0) return null;
final tileXScale = (tileRight - tileLeft) / _mapServiceTileSize;
final sampleSize = max<int>(1, highestPowerOf2(tileXScale));
final region = entry.getRegion(
sampleSize: sampleSize,
region: Rectangle(regionLeft, regionTop, regionWidth, regionHeight),
);
final imageInfoCompleter = Completer<ImageInfo?>();
final imageStream = region.resolve(ImageConfiguration.empty);
final imageStreamListener = ImageStreamListener((image, synchronousCall) {
imageInfoCompleter.complete(image);
}, onError: imageInfoCompleter.completeError);
imageStream.addListener(imageStreamListener);
ImageInfo? regionImageInfo;
try {
regionImageInfo = await imageInfoCompleter.future;
} catch (error) {
debugPrint('failed to get image for region=$region with error=$error');
}
imageStream.removeListener(imageStreamListener);
final imageOffset = Offset(
regionLeft > tileLeft ? (regionLeft - tileLeft).toDouble() : 0,
regionTop > tileTop ? (regionTop - tileTop).toDouble() : 0,
);
final tileImageScaleX = (tileRight - tileLeft) / _mapServiceTileSize;
final tileImageScaleY = (tileBottom - tileTop) / _mapServiceTileSize;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
canvas.scale(1 / tileImageScaleX, 1 / tileImageScaleY);
if (regionImageInfo != null) {
final s = sampleSize.toDouble();
canvas.scale(s, s);
canvas.drawImage(regionImageInfo.image, imageOffset / s, _tileImagePaint);
canvas.scale(1 / s, 1 / s);
} else {
// fallback to show area
canvas.drawRect(
Rect.fromLTWH(
imageOffset.dx,
imageOffset.dy,
regionWidth.toDouble(),
regionHeight.toDouble(),
),
_tileMissingPaint,
);
}
canvas.scale(tileImageScaleX, tileImageScaleY);
final picture = recorder.endRecording();
final tileImage = await picture.toImage(_mapServiceTileSize, _mapServiceTileSize);
final byteData = await tileImage.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) return null;
return MapTile(
width: tileImage.width,
height: tileImage.height,
data: byteData.buffer.asUint8List(),
);
}
@override
String get id => entry.uri;
@override
ImageProvider get imageProvider => entry.uriImage;
@override
bool get canOverlay => center != null;
LatLng? get center => _converter.center;
@override
LatLng? get topLeft => _converter.topLeft;
@override
LatLng? get bottomRight => _converter.bottomRight;
}
class GeoTiffCoordinateConverter {
final AvesEntry entry;
late LatLng? Function(Point<int> pixel) pointToLatLng;
late Point<int>? Function(Point<double> smPoint) epsg3857ToPoint;
GeoTiffCoordinateConverter({
required GeoTiffInfo info,
required this.entry,
}) {
pointToLatLng = (_) => null;
epsg3857ToPoint = (_) => null;
@ -129,113 +254,13 @@ class MappedGeoTiff with MapOverlay {
};
}
@override
Future<MapTile?> getTile(int tx, int ty, int? zoomLevel) async {
zoomLevel ??= 0;
// global projected coordinates in meters (EPSG:3857 Spherical Mercator)
final tileTopLeft3857 = mapServiceHelper.tileTopLeft(tx, ty, zoomLevel);
final tileBottomRight3857 = mapServiceHelper.tileTopLeft(tx + 1, ty + 1, zoomLevel);
// image region coordinates in pixels
final tileTopLeftPx = epsg3857ToPoint(tileTopLeft3857);
final tileBottomRightPx = epsg3857ToPoint(tileBottomRight3857);
if (tileTopLeftPx == null || tileBottomRightPx == null) return null;
final tileLeft = tileTopLeftPx.x;
final tileRight = tileBottomRightPx.x;
final tileTop = tileTopLeftPx.y;
final tileBottom = tileBottomRightPx.y;
final regionLeft = tileLeft.clamp(0, width);
final regionRight = tileRight.clamp(0, width);
final regionTop = tileTop.clamp(0, height);
final regionBottom = tileBottom.clamp(0, height);
final regionWidth = regionRight - regionLeft;
final regionHeight = regionBottom - regionTop;
if (regionWidth == 0 || regionHeight == 0) return null;
final tileXScale = (tileRight - tileLeft) / mapServiceTileSize;
final sampleSize = max<int>(1, highestPowerOf2(tileXScale));
final region = entry.getRegion(
sampleSize: sampleSize,
region: Rectangle(regionLeft, regionTop, regionWidth, regionHeight),
);
final imageInfoCompleter = Completer<ImageInfo?>();
final imageStream = region.resolve(ImageConfiguration.empty);
final imageStreamListener = ImageStreamListener((image, synchronousCall) {
imageInfoCompleter.complete(image);
}, onError: imageInfoCompleter.completeError);
imageStream.addListener(imageStreamListener);
ImageInfo? regionImageInfo;
try {
regionImageInfo = await imageInfoCompleter.future;
} catch (error) {
debugPrint('failed to get image for region=$region with error=$error');
}
imageStream.removeListener(imageStreamListener);
final imageOffset = Offset(
regionLeft > tileLeft ? (regionLeft - tileLeft).toDouble() : 0,
regionTop > tileTop ? (regionTop - tileTop).toDouble() : 0,
);
final tileImageScaleX = (tileRight - tileLeft) / mapServiceTileSize;
final tileImageScaleY = (tileBottom - tileTop) / mapServiceTileSize;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
canvas.scale(1 / tileImageScaleX, 1 / tileImageScaleY);
if (regionImageInfo != null) {
final s = sampleSize.toDouble();
canvas.scale(s, s);
canvas.drawImage(regionImageInfo.image, imageOffset / s, tileImagePaint);
canvas.scale(1 / s, 1 / s);
} else {
// fallback to show area
canvas.drawRect(
Rect.fromLTWH(
imageOffset.dx,
imageOffset.dy,
regionWidth.toDouble(),
regionHeight.toDouble(),
),
tileMissingPaint,
);
}
canvas.scale(tileImageScaleX, tileImageScaleY);
final picture = recorder.endRecording();
final tileImage = await picture.toImage(mapServiceTileSize, mapServiceTileSize);
final byteData = await tileImage.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) return null;
return MapTile(
width: tileImage.width,
height: tileImage.height,
data: byteData.buffer.asUint8List(),
);
}
@override
String get id => entry.uri;
@override
ImageProvider get imageProvider => entry.uriImage;
int get width => entry.width;
int get height => entry.height;
@override
bool get canOverlay => center != null;
LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round()));
@override
LatLng? get topLeft => pointToLatLng(const Point(0, 0));
@override
LatLng? get bottomRight => pointToLatLng(Point(width, height));
}

View file

@ -38,10 +38,8 @@ class NamingPattern {
if (processorOptions != null) {
processors.add(DateNamingProcessor(processorOptions.trim()));
}
break;
case NameNamingProcessor.key:
processors.add(const NameNamingProcessor());
break;
case CounterNamingProcessor.key:
int? start, padding;
_applyProcessorOptions(processorOptions, (key, value) {
@ -50,18 +48,14 @@ class NamingPattern {
switch (key) {
case CounterNamingProcessor.optionStart:
start = valueInt;
break;
case CounterNamingProcessor.optionPadding:
padding = valueInt;
break;
}
}
});
processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding));
break;
default:
debugPrint('unsupported naming processor: ${match.group(0)}');
break;
}
index = end;
});

View file

@ -15,13 +15,10 @@ extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode {
switch (this) {
case DisplayRefreshRateMode.auto:
await FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
break;
case DisplayRefreshRateMode.highest:
await FlutterDisplayMode.setHighRefreshRate();
break;
case DisplayRefreshRateMode.lowest:
await FlutterDisplayMode.setLowRefreshRate();
break;
}
}
}

View file

@ -375,7 +375,7 @@ class Settings extends ChangeNotifier {
if (_locale != null) {
preferredLocales.add(_locale);
} else {
preferredLocales.addAll(WidgetsBinding.instance.window.locales);
preferredLocales.addAll(WidgetsBinding.instance.platformDispatcher.locales);
if (preferredLocales.isEmpty) {
// the `window` locales may be empty in a window-less service context
preferredLocales.addAll(_systemLocalesFallback);
@ -1022,7 +1022,6 @@ class Settings extends ChangeNotifier {
if (value is num) {
isRotationLocked = value == 0;
}
break;
case platformTransitionAnimationScaleKey:
if (value is num) {
areAnimationsRemoved = value == 0;
@ -1080,7 +1079,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not an int');
}
break;
case subtitleFontSizeKey:
case infoMapZoomKey:
if (newValue is double) {
@ -1088,7 +1086,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not a double');
}
break;
case isInstalledAppAccessAllowedKey:
case isErrorReportingAllowedKey:
case enableDynamicColorKey:
@ -1144,7 +1141,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not a bool');
}
break;
case localeKey:
case displayRefreshRateModeKey:
case themeBrightnessKey:
@ -1187,7 +1183,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not a string');
}
break;
case drawerTypeBookmarksKey:
case drawerAlbumBookmarksKey:
case drawerPageBookmarksKey:
@ -1203,7 +1198,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not a list');
}
break;
}
}
if (oldValue != newValue) {

View file

@ -47,13 +47,10 @@ mixin AlbumMixin on SourceBase {
switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.regular:
regularAlbums.add(album);
break;
case AlbumType.app:
appAlbums.add(album);
break;
default:
specialAlbums.add(album);
break;
}
}
return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) => MapEntry(

View file

@ -68,10 +68,8 @@ class CollectionLens with ChangeNotifier {
case MoveType.move:
case MoveType.fromBin:
refresh();
break;
case MoveType.toBin:
_onEntryRemoved(e.entries);
break;
}
}));
_subscriptions.add(sourceEvents.on<EntryRefreshedEvent>().listen((e) => refresh()));
@ -213,16 +211,12 @@ class CollectionLens with ChangeNotifier {
switch (sortFactor) {
case EntrySortFactor.date:
_filteredSortedEntries.sort(AvesEntrySort.compareByDate);
break;
case EntrySortFactor.name:
_filteredSortedEntries.sort(AvesEntrySort.compareByName);
break;
case EntrySortFactor.rating:
_filteredSortedEntries.sort(AvesEntrySort.compareByRating);
break;
case EntrySortFactor.size:
_filteredSortedEntries.sort(AvesEntrySort.compareBySize);
break;
}
if (sortReverse) {
_filteredSortedEntries = _filteredSortedEntries.reversed.toList();
@ -240,33 +234,25 @@ class CollectionLens with ChangeNotifier {
switch (sectionFactor) {
case EntryGroupFactor.album:
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
break;
case EntryGroupFactor.month:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
break;
case EntryGroupFactor.day:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
break;
case EntryGroupFactor.none:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
break;
}
break;
case EntrySortFactor.name:
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
final compare = sortReverse ? (a, b) => source.compareAlbumsByName(b.directory!, a.directory!) : (a, b) => source.compareAlbumsByName(a.directory!, b.directory!);
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, compare);
break;
case EntrySortFactor.rating:
sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
break;
case EntrySortFactor.size:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
break;
}
}
sections = Map.unmodifiable(sections);

View file

@ -221,18 +221,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
switch (key) {
case 'contentId':
entry.contentId = newValue as int?;
break;
case 'dateModifiedSecs':
// `dateModifiedSecs` changes when moving entries to another directory,
// but it does not change when renaming the containing directory
entry.dateModifiedSecs = newValue as int?;
break;
case 'path':
entry.path = newValue as String?;
break;
case 'title':
entry.sourceTitle = newValue as String?;
break;
case 'trashed':
final trashed = newValue as bool;
entry.trashed = trashed;
@ -243,13 +239,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
dateMillis: DateTime.now().millisecondsSinceEpoch,
)
: null;
break;
case 'uri':
entry.uri = newValue as String;
break;
case 'origin':
entry.origin = newValue as int;
break;
}
});
if (entry.trashed) {
@ -371,16 +364,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
switch (moveType) {
case MoveType.copy:
addEntries(movedEntries);
break;
case MoveType.move:
case MoveType.export:
cleanEmptyAlbums(fromAlbums.whereNotNull().toSet());
addDirectories(albums: destinationAlbums);
break;
case MoveType.toBin:
case MoveType.fromBin:
updateDerivedFilters(movedEntries);
break;
}
invalidateAlbumFilterSummary(directories: fromAlbums);
_invalidate(entries: movedEntries);

View file

@ -210,38 +210,29 @@ class VideoMetadataFormatter {
case Keys.androidCaptureFramerate:
final captureFps = double.parse(value);
save('Capture Frame Rate', '${roundToPrecision(captureFps, decimals: 3).toString()} FPS');
break;
case Keys.androidManufacturer:
save('Android Manufacturer', value);
break;
case Keys.androidModel:
save('Android Model', value);
break;
case Keys.androidVersion:
save('Android Version', value);
break;
case Keys.bitrate:
case Keys.bps:
save('Bit Rate', _formatMetric(value, 'b/s'));
break;
case Keys.byteCount:
save('Size', _formatFilesize(value));
break;
case Keys.channelLayout:
save('Channel Layout', _formatChannelLayout(value));
break;
case Keys.codecName:
if (value != 'none') {
save('Format', _formatCodecName(value));
}
break;
case Keys.codecPixelFormat:
if (streamType == MediaStreamTypes.video) {
// this is just a short name used by FFmpeg
// user-friendly descriptions for related enums are defined in libavutil/pixfmt.h
save('Pixel Format', (value as String).toUpperCase());
}
break;
case Keys.codecProfileId:
{
final profile = int.tryParse(value);
@ -260,18 +251,14 @@ class VideoMetadataFormatter {
profileString = Hevc.formatProfile(profile, level);
}
}
break;
}
case Codecs.aac:
profileString = AAC.formatProfile(profile);
break;
default:
profileString = profile.toString();
break;
}
save('Format Profile', profileString);
}
break;
}
case Keys.compatibleBrands:
final formattedBrands = RegExp(r'.{4}').allMatches(value).map((m) {
@ -279,52 +266,37 @@ class VideoMetadataFormatter {
return _formatBrand(brand);
}).join(', ');
save('Compatible Brands', formattedBrands);
break;
case Keys.creationTime:
save('Creation Time', _formatDate(value));
break;
case Keys.date:
if (value is String && value != '0') {
final charCount = value.length;
save(charCount == 4 ? 'Year' : 'Date', value);
}
break;
case Keys.duration:
save('Duration', _formatDuration(value));
break;
case Keys.durationMicros:
if (value != 0) save('Duration', formatPreciseDuration(Duration(microseconds: value)));
break;
case Keys.fpsDen:
save('Frame Rate', '${roundToPrecision(info[Keys.fpsNum] / info[Keys.fpsDen], decimals: 3).toString()} FPS');
break;
case Keys.frameCount:
save('Frame Count', value);
break;
case Keys.height:
save('Height', '$value pixels');
break;
case Keys.language:
if (value != 'und') save('Language', _formatLanguage(value));
break;
case Keys.location:
save('Location', _formatLocation(value));
break;
case Keys.majorBrand:
save('Major Brand', _formatBrand(value));
break;
case Keys.mediaFormat:
save('Format', (value as String).splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName));
break;
case Keys.mediaType:
save('Media Type', value);
break;
case Keys.minorVersion:
if (value != '0') save('Minor Version', value);
break;
case Keys.quicktimeLocationAccuracyHorizontal:
save('QuickTime Location Horizontal Accuracy', value);
break;
case Keys.quicktimeCreationDate:
case Keys.quicktimeLocationIso6709:
case Keys.quicktimeMake:
@ -334,37 +306,27 @@ class VideoMetadataFormatter {
break;
case Keys.rotate:
save('Rotation', '$value°');
break;
case Keys.sampleRate:
save('Sample Rate', _formatMetric(value, 'Hz'));
break;
case Keys.sarDen:
final sarNum = info[Keys.sarNum];
final sarDen = info[Keys.sarDen];
// skip common square pixels (1:1)
if (sarNum != sarDen) save('SAR', '$sarNum:$sarDen');
break;
case Keys.sourceOshash:
save('Source OSHash', value);
break;
case Keys.startMicros:
if (value != 0) save('Start', formatPreciseDuration(Duration(microseconds: value)));
break;
case Keys.statisticsWritingApp:
save('Stats Writing App', value);
break;
case Keys.statisticsWritingDateUtc:
save('Stats Writing Date', _formatDate(value));
break;
case Keys.track:
if (value != '0') save('Track', value);
break;
case Keys.width:
save('Width', '$value pixels');
break;
case Keys.xiaomiSlowMoment:
save('Xiaomi Slow Moment', value);
break;
default:
save(key.toSentenceCase(), value.toString());
}

View file

@ -28,43 +28,30 @@ class H264 {
switch (profileIndex) {
case profileBaseline:
profile = 'Baseline';
break;
case profileConstrainedBaseline:
profile = 'Constrained Baseline';
break;
case profileMain:
profile = 'Main';
break;
case profileExtended:
profile = 'Extended';
break;
case profileHigh:
profile = 'High';
break;
case profileHigh10:
profile = 'High 10';
break;
case profileHigh10Intra:
profile = 'High 10 Intra';
break;
case profileHigh422:
profile = 'High 4:2:2';
break;
case profileHigh422Intra:
profile = 'High 4:2:2 Intra';
break;
case profileHigh444:
profile = 'High 4:4:4';
break;
case profileHigh444Predictive:
profile = 'High 4:4:4 Predictive';
break;
case profileHigh444Intra:
profile = 'High 4:4:4 Intra';
break;
case profileCAVLC444:
profile = 'CAVLC 4:4:4';
break;
default:
return '$profileIndex';
}

View file

@ -9,16 +9,12 @@ class Hevc {
switch (profileIndex) {
case profileMain:
profile = 'Main';
break;
case profileMain10:
profile = 'Main 10';
break;
case profileMainStillPicture:
profile = 'Main Still Picture';
break;
case profileRExt:
profile = 'Format Range';
break;
default:
return '$profileIndex';
}

View file

@ -12,4 +12,4 @@ class PointsOfInterest {
LatLng(37.637861, 21.63),
LatLng(37.949722, 27.363889),
];
}
}

View file

@ -145,11 +145,9 @@ class Analyzer {
case AnalyzerState.stopping:
await _stopPlatformService();
_serviceStateNotifier.value = AnalyzerState.stopped;
break;
case AnalyzerState.stopped:
_controller?.stopSignal.value = true;
_stopUpdateTimer();
break;
}
}

View file

@ -96,25 +96,19 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
switch (command) {
case 'play':
event = const MediaCommandEvent(MediaCommand.play);
break;
case 'pause':
event = const MediaCommandEvent(MediaCommand.pause);
break;
case 'skip_to_next':
event = const MediaCommandEvent(MediaCommand.skipToNext);
break;
case 'skip_to_previous':
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
break;
case 'stop':
event = const MediaCommandEvent(MediaCommand.stop);
break;
case 'seek':
final position = fields['position'] as int?;
if (position != null) {
event = MediaSeekCommandEvent(MediaCommand.stop, position: position);
}
break;
}
if (event != null) {
_streamController.add(event);

View file

@ -80,7 +80,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) {
final value = root.getElement(name)?.text;
final value = root.getElement(name)?.innerText;
return value != null ? MapEntry(formatKey(name), value) : null;
}).whereNotNull(),
]);

View file

@ -74,15 +74,12 @@ class PlatformWindowService implements WindowService {
case Orientation.landscape:
// SCREEN_ORIENTATION_SENSOR_LANDSCAPE
orientationCode = 6;
break;
case Orientation.portrait:
// SCREEN_ORIENTATION_SENSOR_PORTRAIT
orientationCode = 7;
break;
default:
// SCREEN_ORIENTATION_UNSPECIFIED
orientationCode = -1;
break;
}
try {
await _platform.invokeMethod('requestOrientation', <String, dynamic>{

View file

@ -4,4 +4,4 @@ class AText {
static const separator = ' ${UniChars.bullet} ';
static const resolutionSeparator = ' ${UniChars.multiplicationSign} ';
static const valueNotAvailable = UniChars.emDash;
}
}

View file

@ -274,11 +274,9 @@ class DiffMatchPatch {
case Operation.insert:
count_insert++;
text_insert.write(diffs[pointer].text);
break;
case Operation.delete:
count_delete++;
text_delete.write(diffs[pointer].text);
break;
case Operation.equal:
// Upon reaching an equality, check for prior redundancies.
if (count_delete >= 1 && count_insert >= 1) {
@ -295,7 +293,6 @@ class DiffMatchPatch {
count_delete = 0;
text_delete.clear();
text_insert.clear();
break;
}
pointer++;
}
@ -1013,12 +1010,10 @@ class DiffMatchPatch {
count_insert++;
text_insert += diffs[pointer].text;
pointer++;
break;
case Operation.delete:
count_delete++;
text_delete += diffs[pointer].text;
pointer++;
break;
case Operation.equal:
// Upon reaching an equality, check for prior redundancies.
if (count_delete + count_insert > 1) {
@ -1068,7 +1063,6 @@ class DiffMatchPatch {
count_delete = 0;
text_delete = '';
text_insert = '';
break;
}
}
if (diffs.last.text.isEmpty) {
@ -1155,17 +1149,14 @@ class DiffMatchPatch {
html.write('<ins style="background:#e6ffe6;">');
html.write(text);
html.write('</ins>');
break;
case Operation.delete:
html.write('<del style="background:#ffe6e6;">');
html.write(text);
html.write('</del>');
break;
case Operation.equal:
html.write('<span>');
html.write(text);
html.write('</span>');
break;
}
}
return html.toString();
@ -1209,16 +1200,13 @@ class DiffMatchPatch {
switch (aDiff.operation) {
case Operation.insert:
insertions += aDiff.text.length;
break;
case Operation.delete:
deletions += aDiff.text.length;
break;
case Operation.equal:
// A deletion and an insertion is one substitution.
levenshtein += max(insertions, deletions);
insertions = 0;
deletions = 0;
break;
}
}
levenshtein += max(insertions, deletions);
@ -1239,17 +1227,14 @@ class DiffMatchPatch {
text.write('+');
text.write(Uri.encodeFull(aDiff.text));
text.write('\t');
break;
case Operation.delete:
text.write('-');
text.write(aDiff.text.length);
text.write('\t');
break;
case Operation.equal:
text.write('=');
text.write(aDiff.text.length);
text.write('\t');
break;
}
}
String delta = text.toString();
@ -1289,7 +1274,6 @@ class DiffMatchPatch {
throw ArgumentError('Illegal escape in diff_fromDelta: $param');
}
diffs.add(Diff(Operation.insert, param));
break;
case '-':
// Fall through.
case '=':
@ -1314,7 +1298,6 @@ class DiffMatchPatch {
} else {
diffs.add(Diff(Operation.delete, text));
}
break;
default:
// Anything else is an error.
throw ArgumentError('Invalid diff operation in diff_fromDelta: ${token[0]}');

View file

@ -26,7 +26,7 @@ class XMP {
static const nsXmp = XmpNamespaces.xmp;
// for `rdf:Description` node only
static bool _hasMeaningfulChildren(XmlNode node) => node.children.any((v) => v.nodeType != XmlNodeType.TEXT || v.text.trim().isNotEmpty);
static bool _hasMeaningfulChildren(XmlNode node) => node.children.any((v) => v.nodeType != XmlNodeType.TEXT || v.innerText.trim().isNotEmpty);
// for `rdf:Description` node only
static bool _hasMeaningfulAttributes(XmlNode description) {

View file

@ -79,10 +79,8 @@ Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
switch (settings.getWidgetDisplayedItem(widgetId)) {
case WidgetDisplayedItem.random:
entries.shuffle();
break;
case WidgetDisplayedItem.mostRecent:
entries.sort(AvesEntrySort.compareByDate);
break;
}
final entry = entries.firstOrNull;
if (entry != null) {

View file

@ -94,11 +94,12 @@ class _AppReferenceState extends State<AppReference> {
return FutureBuilder<PackageInfo>(
future: _packageInfoLoader,
builder: (context, snapshot) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
AvesLogo(
size: _appTitleStyle.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3,
size: _appTitleStyle.fontSize! * textScaleFactor * 1.3,
),
const SizedBox(width: 8),
Text(

View file

@ -157,7 +157,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
'Device: ${androidInfo.manufacturer} ${androidInfo.model}',
'Geocoder: ${device.hasGeocoder ? 'ready' : 'not available'}',
'Mobile services: ${mobileServices.isServiceAvailable ? 'ready' : 'not available'}',
'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}',
'System locales: ${WidgetsBinding.instance.platformDispatcher.locales.join(', ')}',
'Storage volumes: ${storageVolumes.map((v) => v.path).join(', ')}',
'Storage grants: ${storageGrants.join(', ')}',
'Error reporting: ${settings.isErrorReportingAllowed}',

View file

@ -1,7 +1,9 @@
import 'dart:developer' show Flow, Timeline;
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/intents.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide Flow;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
@ -209,10 +211,21 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
bool _loaded = false;
Future<void> _initLicenses() async {
int debugFlowId = -1;
assert(() {
final Flow flow = Flow.begin();
Timeline.timeSync('_initLicenses()', () {}, flow: flow);
debugFlowId = flow.id;
return true;
}());
for (final LicenseEntry license in widget.licenseEntries) {
if (!mounted) {
return;
}
assert(() {
Timeline.timeSync('_initLicenses()', () {}, flow: Flow.step(debugFlowId));
return true;
}());
final List<LicenseParagraph> paragraphs = await SchedulerBinding.instance.scheduleTask<List<LicenseParagraph>>(
license.paragraphs.toList,
Priority.animation,
@ -237,6 +250,7 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
),
));
} else {
assert(paragraph.indent >= 0);
_licenses.add(Padding(
padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent),
child: Text(paragraph.text),
@ -248,16 +262,21 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
setState(() {
_loaded = true;
});
assert(() {
Timeline.timeSync('Build scheduled', () {}, flow: Flow.end(debugFlowId));
return true;
}());
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData theme = Theme.of(context);
final String title = widget.packageName;
final String subtitle = localizations.licensesPackageDetailText(widget.licenseEntries.length);
const double pad = 24;
const EdgeInsets padding = EdgeInsets.only(left: pad, right: pad, bottom: pad);
final double pad = _getGutterSize(context);
final EdgeInsets padding = EdgeInsets.only(left: pad, right: pad, bottom: pad);
final List<Widget> listWidgets = <Widget>[
..._licenses,
if (!_loaded)
@ -274,9 +293,11 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
page = Scaffold(
appBar: AppBar(
title: _PackageLicensePageTitle(
title,
subtitle,
theme.primaryTextTheme,
title: title,
subtitle: subtitle,
theme: theme.useMaterial3 ? theme.textTheme : theme.primaryTextTheme,
titleTextStyle: theme.appBarTheme.titleTextStyle,
foregroundColor: theme.appBarTheme.foregroundColor,
),
),
body: Center(
@ -292,7 +313,11 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
// A Scrollbar is built-in below.
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: Scrollbar(
child: ListView(padding: padding, children: listWidgets),
child: ListView(
primary: true,
padding: padding,
children: listWidgets,
),
),
),
),
@ -308,7 +333,12 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
automaticallyImplyLeading: false,
pinned: true,
backgroundColor: theme.cardColor,
title: _PackageLicensePageTitle(title, subtitle, theme.textTheme),
title: _PackageLicensePageTitle(
title: title,
subtitle: subtitle,
theme: theme.textTheme,
titleTextStyle: theme.textTheme.titleLarge,
),
),
SliverPadding(
padding: padding,
@ -334,27 +364,36 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
}
class _PackageLicensePageTitle extends StatelessWidget {
const _PackageLicensePageTitle(
this.title,
this.subtitle,
this.theme,
);
const _PackageLicensePageTitle({
required this.title,
required this.subtitle,
required this.theme,
this.titleTextStyle,
this.foregroundColor,
});
final String title;
final String subtitle;
final TextTheme theme;
final TextStyle? titleTextStyle;
final Color? foregroundColor;
@override
Widget build(BuildContext context) {
final Color? color = Theme.of(context).appBarTheme.foregroundColor;
final TextStyle? effectiveTitleTextStyle = titleTextStyle ?? theme.titleLarge;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(title, style: theme.titleLarge?.copyWith(color: color)),
Text(subtitle, style: theme.titleSmall?.copyWith(color: color)),
Text(title, style: effectiveTitleTextStyle?.copyWith(color: foregroundColor)),
Text(subtitle, style: theme.titleSmall?.copyWith(color: foregroundColor)),
],
);
}
}
const int _materialGutterThreshold = 720;
const double _wideGutterSize = 24.0;
const double _narrowGutterSize = 12.0;
double _getGutterSize(BuildContext context) => MediaQuery.sizeOf(context).width >= _materialGutterThreshold ? _wideGutterSize : _narrowGutterSize;

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:aves/app_flavor.dart';
import 'package:aves/app_mode.dart';
@ -180,8 +179,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
super.initState();
EquatableConfig.stringify = true;
_appSetup = _setup();
// remember screen size to use it later, when `context` and `window` are no longer reliable
_screenSize = _getScreenSize();
_shouldUseBoldFontLoader = AccessibilityService.shouldUseBoldFont();
_dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette();
_subscriptions.add(_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChanged(event as String?)));
@ -206,6 +203,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
// remember screen size to use it later, when `context` and `window` are no longer reliable
_screenSize ??= _getScreenSize(context);
// place the settings provider above `MaterialApp`
// so it can be used during navigation transitions
return MultiProvider(
@ -266,38 +266,31 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// KEYCODE_ENTER, KEYCODE_BUTTON_A, KEYCODE_NUMPAD_ENTER
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: MediaQuery.fromWindow(
child: Builder(
builder: (context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
// disable accessible navigation, as it impacts snack bar action timer
// for all users of apps registered as accessibility services,
// even though they are not for accessibility purposes (like TalkBack is)
accessibleNavigation: false,
),
child: MaterialApp(
navigatorKey: _navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) => _decorateAppChild(
context: context,
initialized: initialized,
child: child,
),
onGenerateTitle: (context) => context.l10n.appName,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
useInheritedMediaQuery: true,
),
);
},
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
// disable accessible navigation, as it impacts snack bar action timer
// for all users of apps registered as accessibility services,
// even though they are not for accessibility purposes (like TalkBack is)
accessibleNavigation: false,
),
child: MaterialApp(
navigatorKey: _navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) => _decorateAppChild(
context: context,
initialized: initialized,
child: child,
),
onGenerateTitle: (context) => context.l10n.appName,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
),
),
);
@ -390,7 +383,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
case AppMode.pickSingleMediaExternal:
case AppMode.pickMultipleMediaExternal:
_saveTopEntries();
break;
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal:
@ -400,10 +392,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
case AppMode.view:
break;
}
break;
case AppLifecycleState.resumed:
RecentlyAddedFilter.updateNow();
break;
case AppLifecycleState.paused:
case AppLifecycleState.detached:
break;
@ -421,9 +411,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
Size? _getScreenSize() {
final physicalSize = window.physicalSize;
final ratio = window.devicePixelRatio;
Size? _getScreenSize(BuildContext context) {
final view = View.of(context);
final physicalSize = view.physicalSize;
final ratio = view.devicePixelRatio;
return physicalSize > Size.zero && ratio > 0 ? physicalSize / ratio : null;
}
@ -431,7 +422,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
void _saveTopEntries() {
if (!settings.initialized) return;
final screenSize = _screenSize ?? _getScreenSize();
final screenSize = _screenSize;
if (screenSize == null) return;
var tileExtent = settings.getTileExtent(CollectionPage.routeName);
@ -525,10 +516,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
case MaxBrightness.never:
case MaxBrightness.viewerOnly:
ScreenBrightness().resetScreenBrightness();
break;
case MaxBrightness.always:
ScreenBrightness().setScreenBrightness(1);
break;
}
}
@ -586,7 +575,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
: 'debug',
'has_mobile_services': mobileServices.isServiceAvailable,
'is_television': device.isTelevision,
'locales': WidgetsBinding.instance.window.locales.join(', '),
'locales': WidgetsBinding.instance.platformDispatcher.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
});
await reportService.log('Launch');

View file

@ -224,7 +224,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}
double get appBarContentHeight {
final textScaleFactor = context.read<MediaQueryData>().textScaleFactor;
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
double height = kToolbarHeight * textScaleFactor;
if (settings.useTvLayout) {
height += CaptionedButton.getTelevisionButtonHeight(context);
@ -511,16 +511,13 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
queryEnabled: context.read<Query>().enabled,
isMenuItem: true,
);
break;
case EntrySetAction.toggleFavourite:
child = FavouriteToggler(
entries: _getExpandedSelectedItems(selection),
isMenuItem: true,
);
break;
default:
child = MenuRow(text: action.getText(context), icon: action.getIcon());
break;
}
return PopupMenuItem(
key: _getActionKey(action),
@ -598,7 +595,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus();
void _updateStatusBarHeight() {
_statusBarHeight = context.read<MediaQueryData>().padding.top;
_statusBarHeight = MediaQuery.paddingOf(context).top;
_updateAppBarHeight();
}
@ -611,16 +608,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
// general
case EntrySetAction.configureView:
await _configureView();
break;
case EntrySetAction.select:
context.read<Selection<AvesEntry>>().select();
break;
case EntrySetAction.selectAll:
context.read<Selection<AvesEntry>>().addToSelection(collection.sortedEntries);
break;
case EntrySetAction.selectNone:
context.read<Selection<AvesEntry>>().clearSelection();
break;
// browsing
case EntrySetAction.searchCollection:
case EntrySetAction.toggleTitleSearch:
@ -650,7 +643,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
case EntrySetAction.editTags:
case EntrySetAction.removeMetadata:
_actionDelegate.onActionSelected(context, action);
break;
}
}

View file

@ -46,7 +46,6 @@ import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:intl/intl.dart';
@ -374,6 +373,8 @@ class _CollectionScaler extends StatelessWidget {
final tileSpacing = metrics.item1;
final horizontalPadding = metrics.item2;
final brightness = Theme.of(context).brightness;
final borderColor = DecoratedThumbnail.borderColor;
final borderWidth = DecoratedThumbnail.borderWidth(context);
return GridScaleGestureDetector<AvesEntry>(
scrollableKey: scrollableKey,
tileLayout: tileLayout,
@ -385,9 +386,9 @@ class _CollectionScaler extends StatelessWidget {
tileSize: tileSize,
spacing: tileSpacing,
horizontalPadding: horizontalPadding,
borderWidth: DecoratedThumbnail.borderWidth,
borderWidth: borderWidth,
borderRadius: Radius.zero,
color: DecoratedThumbnail.borderColor,
color: borderColor,
textDirection: Directionality.of(context),
),
child: child,
@ -404,8 +405,8 @@ class _CollectionScaler extends StatelessWidget {
decoration: BoxDecoration(
color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9),
border: Border.all(
color: DecoratedThumbnail.borderColor,
width: DecoratedThumbnail.borderWidth,
color: borderColor,
width: borderWidth,
),
),
),
@ -489,7 +490,6 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
}
});
}
break;
}
}
@ -573,7 +573,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
physics: collection.isEmpty
? const NeverScrollableScrollPhysics()
: SloppyScrollPhysics(
gestureSettings: context.select<MediaQueryData, DeviceGestureSettings>((mq) => mq.gestureSettings),
gestureSettings: MediaQuery.gestureSettingsOf(context),
parent: const AlwaysScrollableScrollPhysics(),
),
cacheExtent: context.select<TileExtentController, double>((controller) => controller.effectiveExtentMax),
@ -677,7 +677,6 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
switch (collection.sectionFactor) {
case EntryGroupFactor.album:
addAlbums(collection, sectionLayouts, crumbs);
break;
case EntryGroupFactor.month:
case EntryGroupFactor.day:
final firstKey = sectionLayouts.first.sectionKey;
@ -701,14 +700,11 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
});
}
}
break;
case EntryGroupFactor.none:
break;
}
break;
case EntrySortFactor.name:
addAlbums(collection, sectionLayouts, crumbs);
break;
case EntrySortFactor.rating:
case EntrySortFactor.size:
break;

View file

@ -173,79 +173,55 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
// browsing
case EntrySetAction.searchCollection:
_goToSearch(context);
break;
case EntrySetAction.toggleTitleSearch:
context.read<Query>().toggle();
break;
case EntrySetAction.addShortcut:
_addShortcut(context);
break;
// browsing or selecting
case EntrySetAction.map:
_goToMap(context);
break;
case EntrySetAction.slideshow:
_goToSlideshow(context);
break;
case EntrySetAction.stats:
_goToStats(context);
break;
case EntrySetAction.rescan:
_rescan(context);
break;
// selecting
case EntrySetAction.share:
_share(context);
break;
case EntrySetAction.delete:
case EntrySetAction.emptyBin:
_delete(context);
break;
case EntrySetAction.restore:
_move(context, moveType: MoveType.fromBin);
break;
case EntrySetAction.copy:
_move(context, moveType: MoveType.copy);
break;
case EntrySetAction.move:
_move(context, moveType: MoveType.move);
break;
case EntrySetAction.rename:
_rename(context);
break;
case EntrySetAction.convert:
_convert(context);
break;
case EntrySetAction.toggleFavourite:
_toggleFavourite(context);
break;
case EntrySetAction.rotateCCW:
_rotate(context, clockwise: false);
break;
case EntrySetAction.rotateCW:
_rotate(context, clockwise: true);
break;
case EntrySetAction.flip:
_flip(context);
break;
case EntrySetAction.editDate:
editDate(context);
break;
case EntrySetAction.editLocation:
_editLocation(context);
break;
case EntrySetAction.editTitleDescription:
_editTitleDescription(context);
break;
case EntrySetAction.editRating:
_editRating(context);
break;
case EntrySetAction.editTags:
_editTags(context);
break;
case EntrySetAction.removeMetadata:
_removeMetadata(context);
break;
}
}

View file

@ -57,7 +57,6 @@ class CollectionSectionHeader extends StatelessWidget {
case EntryGroupFactor.none:
break;
}
break;
case EntrySortFactor.name:
return _buildAlbumHeader(context);
case EntrySortFactor.rating:

View file

@ -78,7 +78,7 @@ class EntryListDetails extends StatelessWidget {
Widget _buildDateRow(BuildContext context, TextStyle style) {
final locale = context.l10n.localeName;
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
final use24hour = MediaQuery.alwaysUse24HourFormatOf(context);
final date = entry.bestDate;
final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable;

View file

@ -41,17 +41,13 @@ class InteractiveTile extends StatelessWidget {
} else {
OpenViewerNotification(entry).dispatch(context);
}
break;
case AppMode.pickSingleMediaExternal:
IntentService.submitPickedItems([entry.uri]);
break;
case AppMode.pickMultipleMediaExternal:
final selection = context.read<Selection<AvesEntry>>();
selection.toggleSelection(entry);
break;
case AppMode.pickMediaInternal:
Navigator.maybeOf(context)?.pop(entry);
break;
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickFilterInternal:
case AppMode.screenSaver:

View file

@ -52,7 +52,7 @@ class _EntryQueryBarState extends State<EntryQueryBar> {
@override
Widget build(BuildContext context) {
final textScaleFactor = context.select<MediaQueryData, double>((mq) => mq.textScaleFactor);
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
return Container(
height: EntryQueryBar.getPreferredHeight(textScaleFactor),
alignment: Alignment.topCenter,

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:ui';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/quick_chooser.dart';
@ -51,10 +50,15 @@ class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
@override
void initState() {
super.initState();
_selectedRowRect.value = Rect.fromLTWH(0, window.physicalSize.height * (reversed ? 1 : -1), 0, 0);
_registerWidget(widget);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_selectedRowRect.value = Rect.fromLTWH(0, MediaQuery.sizeOf(context).height * (reversed ? 1 : -1), 0, 0);
}
@override
void didUpdateWidget(covariant MenuQuickChooser<T> oldWidget) {
super.didUpdateWidget(oldWidget);

View file

@ -33,10 +33,8 @@ class QuickChooserRouteLayout extends SingleChildLayoutDelegate {
switch (menuPosition) {
case PopupMenuPosition.over:
y = triggerRect.top - childSize.height;
break;
case PopupMenuPosition.under:
y = size.height - triggerRect.bottom;
break;
}
double x = (triggerRect.left + (size.width - triggerRect.right) - childSize.width) / 2;
final wantedPosition = Offset(x, y);

View file

@ -309,17 +309,14 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
..remove(destinationAlbum)
..insert(0, destinationAlbum);
entriesByDestination[destinationAlbum] = entries;
break;
case MoveType.toBin:
entriesByDestination[AndroidFileUtils.trashDirPath] = entries;
break;
case MoveType.fromBin:
groupBy<AvesEntry, String?>(entries, (e) => e.directory).forEach((originAlbum, dirEntries) {
if (originAlbum != null) {
entriesByDestination[originAlbum] = dirEntries.toSet();
}
});
break;
}
await doQuickMove(

View file

@ -8,66 +8,82 @@ import 'package:flutter/material.dart';
// This overlay entry is not below a `Scaffold` (which is expected by `SnackBar`
// and `SnackBarAction`), and is not dismissed the same way.
// This adaptation assumes the `SnackBarBehavior.floating` behavior and no animation.
class OverlaySnackBar extends StatelessWidget {
class OverlaySnackBar extends StatefulWidget {
final Widget content;
final Widget? action;
final DismissDirection dismissDirection;
final VoidCallback onDismiss;
final Clip clipBehavior;
const OverlaySnackBar({
super.key,
required this.content,
required this.action,
required this.dismissDirection,
this.action,
this.dismissDirection = DismissDirection.down,
this.clipBehavior = Clip.hardEdge,
required this.onDismiss,
});
@override
State<OverlaySnackBar> createState() => _OverlaySnackBarState();
}
class _OverlaySnackBarState extends State<OverlaySnackBar> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final snackBarTheme = theme.snackBarTheme;
final isThemeDark = theme.brightness == Brightness.dark;
final buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
assert(debugCheckHasMediaQuery(context));
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
final bool isThemeDark = theme.brightness == Brightness.dark;
final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
final SnackBarThemeData defaults = theme.useMaterial3 ? _SnackbarDefaultsM3(context) : _SnackbarDefaultsM2(context);
final brightness = isThemeDark ? Brightness.light : Brightness.dark;
final themeBackgroundColor = isThemeDark ? colorScheme.onSurface : Color.alphaBlend(colorScheme.onSurface.withOpacity(0.80), colorScheme.surface);
final inverseTheme = theme.copyWith(
colorScheme: ColorScheme(
primary: colorScheme.onPrimary,
secondary: buttonColor,
surface: colorScheme.onSurface,
background: themeBackgroundColor,
error: colorScheme.onError,
onPrimary: colorScheme.primary,
onSecondary: colorScheme.secondary,
onSurface: colorScheme.surface,
onBackground: colorScheme.background,
onError: colorScheme.error,
brightness: brightness,
),
);
// SnackBar uses a theme that is the opposite brightness from
// the surrounding theme.
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
final contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.titleMedium;
// Invert the theme values for Material 2. Material 3 values are tokenized to pre-inverted values.
final ThemeData effectiveTheme = theme.useMaterial3
? theme
: theme.copyWith(
colorScheme: ColorScheme(
primary: colorScheme.onPrimary,
secondary: buttonColor,
surface: colorScheme.onSurface,
background: defaults.backgroundColor!,
error: colorScheme.onError,
onPrimary: colorScheme.primary,
onSecondary: colorScheme.secondary,
onSurface: colorScheme.surface,
onBackground: colorScheme.background,
onError: colorScheme.error,
brightness: brightness,
),
);
final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? defaults.contentTextStyle;
final horizontalPadding = FeedbackMixin.snackBarHorizontalPadding(snackBarTheme);
final padding = EdgeInsetsDirectional.only(start: horizontalPadding, end: action != null ? 0 : horizontalPadding);
final padding = EdgeInsetsDirectional.only(start: horizontalPadding, end: widget.action != null ? 0 : horizontalPadding);
const singleLineVerticalPadding = 14.0;
final EdgeInsets margin = snackBarTheme.insetPadding ?? defaults.insetPadding!;
Widget snackBar = Padding(
padding: padding,
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: action != null ? null : const EdgeInsets.symmetric(vertical: singleLineVerticalPadding),
padding: widget.action != null ? null : const EdgeInsets.symmetric(vertical: singleLineVerticalPadding),
child: DefaultTextStyle(
style: contentTextStyle!,
child: content,
child: widget.content,
),
),
),
if (action != null)
if (widget.action != null)
TextButtonTheme(
data: TextButtonThemeData(
style: TextButton.styleFrom(
@ -75,36 +91,28 @@ class OverlaySnackBar extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
),
),
child: action!,
child: widget.action!,
),
],
),
);
final elevation = snackBarTheme.elevation ?? 6.0;
final backgroundColor = snackBarTheme.backgroundColor ?? inverseTheme.colorScheme.background;
final shape = snackBarTheme.shape ?? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
final double elevation = snackBarTheme.elevation ?? defaults.elevation!;
final Color backgroundColor = snackBarTheme.backgroundColor ?? defaults.backgroundColor!;
final ShapeBorder? shape = snackBarTheme.shape ?? defaults.shape;
snackBar = Material(
shape: shape,
elevation: elevation,
color: backgroundColor,
child: Theme(
data: inverseTheme,
data: effectiveTheme,
child: snackBar,
),
);
const topMargin = 5.0;
const bottomMargin = 10.0;
const horizontalMargin = 15.0;
snackBar = Padding(
padding: const EdgeInsets.fromLTRB(
horizontalMargin,
topMargin,
horizontalMargin,
bottomMargin,
),
padding: margin,
child: snackBar,
);
@ -117,16 +125,138 @@ class OverlaySnackBar extends StatelessWidget {
snackBar = Semantics(
container: true,
liveRegion: true,
onDismiss: onDismiss,
onDismiss: widget.onDismiss,
child: Dismissible(
key: const Key('dismissible'),
direction: dismissDirection,
direction: widget.dismissDirection,
resizeDuration: null,
onDismissed: (direction) => onDismiss(),
onDismissed: (direction) => widget.onDismiss(),
child: snackBar,
),
);
return snackBar;
final Widget snackBarTransition = snackBar;
return Hero(
tag: '<SnackBar Hero tag - ${widget.content}>',
transitionOnUserGestures: true,
child: ClipRect(
clipBehavior: widget.clipBehavior,
child: snackBarTransition,
),
);
}
}
// Hand coded defaults based on Material Design 2.
class _SnackbarDefaultsM2 extends SnackBarThemeData {
_SnackbarDefaultsM2(BuildContext context)
: _theme = Theme.of(context),
_colors = Theme.of(context).colorScheme,
super(elevation: 6.0);
late final ThemeData _theme;
late final ColorScheme _colors;
@override
Color get backgroundColor => _theme.brightness == Brightness.light ? Color.alphaBlend(_colors.onSurface.withOpacity(0.80), _colors.surface) : _colors.onSurface;
@override
TextStyle? get contentTextStyle => ThemeData(brightness: _theme.brightness == Brightness.light ? Brightness.dark : Brightness.light).textTheme.titleMedium;
@override
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
@override
Color get actionTextColor => _colors.secondary;
@override
Color get disabledActionTextColor => _colors.onSurface.withOpacity(_theme.brightness == Brightness.light ? 0.38 : 0.3);
@override
ShapeBorder get shape => const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(4.0),
),
);
@override
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
@override
bool get showCloseIcon => false;
@override
Color get closeIconColor => _colors.onSurface;
@override
double get actionOverflowThreshold => 0.25;
}
// BEGIN GENERATED TOKEN PROPERTIES - Snackbar
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_162
class _SnackbarDefaultsM3 extends SnackBarThemeData {
_SnackbarDefaultsM3(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
@override
Color get backgroundColor => _colors.inverseSurface;
@override
Color get actionTextColor => MaterialStateColor.resolveWith((states) {
if (states.contains(MaterialState.disabled)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.pressed)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.hovered)) {
return _colors.inversePrimary;
}
if (states.contains(MaterialState.focused)) {
return _colors.inversePrimary;
}
return _colors.inversePrimary;
});
@override
Color get disabledActionTextColor => _colors.inversePrimary;
@override
TextStyle get contentTextStyle => Theme.of(context).textTheme.bodyMedium!.copyWith(
color: _colors.onInverseSurface,
);
@override
double get elevation => 6.0;
@override
ShapeBorder get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
@override
SnackBarBehavior get behavior => SnackBarBehavior.fixed;
@override
EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0);
@override
bool get showCloseIcon => false;
@override
Color? get closeIconColor => _colors.onInverseSurface;
@override
double get actionOverflowThreshold => 0.25;
}
// END GENERATED TOKEN PROPERTIES - Snackbar

View file

@ -35,7 +35,6 @@ mixin SizeAwareMixin {
case MoveType.copy:
case MoveType.export:
needed = selection.fold(0, sumSize);
break;
case MoveType.move:
case MoveType.toBin:
case MoveType.fromBin:
@ -46,7 +45,6 @@ mixin SizeAwareMixin {
// and we need at least as much space as the largest entry because individual entries are copied then deleted
final largestSingle = selection.fold<int>(0, (largest, entry) => max(largest, entry.sizeBytes ?? 0));
needed = max(fromOtherVolumes, largestSingle);
break;
}
final hasEnoughSpace = needed < free;

View file

@ -36,7 +36,6 @@ mixin VaultAwareMixin on FeedbackMixin {
await reportService.recordError(e, stack);
}
}
break;
case VaultLockType.pattern:
final pattern = await showDialog<String>(
context: context,
@ -46,7 +45,6 @@ mixin VaultAwareMixin on FeedbackMixin {
if (pattern != null) {
confirmed = pattern == await securityService.readValue(details.passKey);
}
break;
case VaultLockType.pin:
final pin = await showDialog<String>(
context: context,
@ -56,7 +54,6 @@ mixin VaultAwareMixin on FeedbackMixin {
if (pin != null) {
confirmed = pin == await securityService.readValue(details.passKey);
}
break;
case VaultLockType.password:
final password = await showDialog<String>(
context: context,
@ -66,7 +63,6 @@ mixin VaultAwareMixin on FeedbackMixin {
if (password != null) {
confirmed = password == await securityService.readValue(details.passKey);
}
break;
}
if (confirmed == null || !confirmed) return false;
@ -120,7 +116,6 @@ mixin VaultAwareMixin on FeedbackMixin {
await reportService.recordError(e, stack);
}
}
break;
case VaultLockType.pattern:
final pattern = await showDialog<String>(
context: context,
@ -130,7 +125,6 @@ mixin VaultAwareMixin on FeedbackMixin {
if (pattern != null) {
return await securityService.writeValue(details.passKey, pattern);
}
break;
case VaultLockType.pin:
final pin = await showDialog<String>(
context: context,
@ -140,7 +134,6 @@ mixin VaultAwareMixin on FeedbackMixin {
if (pin != null) {
return await securityService.writeValue(details.passKey, pin);
}
break;
case VaultLockType.password:
final password = await showDialog<String>(
context: context,
@ -150,7 +143,6 @@ mixin VaultAwareMixin on FeedbackMixin {
if (password != null) {
return await securityService.writeValue(details.passKey, password);
}
break;
}
return false;
}

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class InteractiveAppBarTitle extends StatelessWidget {
final GestureTapCallback? onTap;
@ -13,6 +12,7 @@ class InteractiveAppBarTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
return GestureDetector(
onTap: onTap,
// use a `Container` with a dummy color to make it expand
@ -20,7 +20,7 @@ class InteractiveAppBarTitle extends StatelessWidget {
child: Container(
alignment: AlignmentDirectional.centerStart,
color: Colors.transparent,
height: kToolbarHeight * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor),
height: kToolbarHeight * textScaleFactor,
child: child,
),
);

View file

@ -11,10 +11,11 @@ class FontSizeIconTheme extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final iconTheme = IconTheme.of(context);
return IconTheme(
data: iconTheme.copyWith(
size: iconTheme.size! * MediaQuery.textScaleFactorOf(context),
size: iconTheme.size! * textScaleFactor,
),
child: child,
);

View file

@ -22,7 +22,7 @@ class BottomGestureAreaProtector extends StatelessWidget {
left: 0,
right: 0,
bottom: 0,
height: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.bottom),
height: MediaQuery.systemGestureInsetsOf(context).bottom,
child: GestureDetector(
// absorb vertical gestures only
onVerticalDragDown: (details) {},
@ -42,7 +42,7 @@ class TopGestureAreaProtector extends StatelessWidget {
left: 0,
top: 0,
right: 0,
height: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.top),
height: MediaQuery.systemGestureInsetsOf(context).top,
child: GestureDetector(
// absorb vertical gestures only
onVerticalDragDown: (details) {},
@ -64,7 +64,7 @@ class SideGestureAreaProtector extends StatelessWidget {
textDirection: TextDirection.ltr,
children: [
SizedBox(
width: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.left),
width: MediaQuery.systemGestureInsetsOf(context).left,
child: GestureDetector(
// absorb horizontal gestures only
onHorizontalDragDown: (details) {},
@ -73,7 +73,7 @@ class SideGestureAreaProtector extends StatelessWidget {
),
const Spacer(),
SizedBox(
width: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.right),
width: MediaQuery.systemGestureInsetsOf(context).right,
child: GestureDetector(
// absorb horizontal gestures only
onHorizontalDragDown: (details) {},

View file

@ -54,11 +54,9 @@ class ReselectableRadioListTile<T> extends StatelessWidget {
case ListTileControlAffinity.platform:
leading = control;
trailing = secondary;
break;
case ListTileControlAffinity.trailing:
leading = secondary;
trailing = control;
break;
}
return MergeSemantics(
child: ListTileTheme.merge(

View file

@ -54,7 +54,7 @@ class MarkdownContainer extends StatelessWidget {
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
border: Border.all(color: Theme.of(context).dividerColor, width: AvesBorder.curvedBorderWidth),
border: Border.all(color: Theme.of(context).dividerColor, width: AvesBorder.curvedBorderWidth(context)),
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
constraints: BoxConstraints(maxWidth: useTvLayout ? double.infinity : mobileMaxWidth),

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AvesScaffold extends StatelessWidget {
final PreferredSizeWidget? appBar;
@ -26,7 +25,7 @@ class AvesScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
// prevent conflict between drawer drag gesture and Android navigation gestures
final drawerEnableOpenDragGesture = context.select<MediaQueryData, bool>((mq) => mq.systemGestureInsets.horizontal == 0);
final drawerEnableOpenDragGesture = MediaQuery.systemGestureInsetsOf(context).horizontal == 0;
return Scaffold(
appBar: appBar,

View file

@ -31,44 +31,51 @@ class OutlinedText extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO TLAD [subtitles] fix background area for mixed alphabetic-ideographic text
// as of Flutter v2.2.2, the area computed for `backgroundColor` has inconsistent height
// in case of mixed alphabetic-ideographic text. The painted boxes depends on the script.
// as of Flutter v3.10.0, the area computed for `backgroundColor` has inconsistent height
// in case of mixed alphabetic-ideographic text. The painted boxes depend on the script.
// Possible workarounds would be to use metrics from:
// - `TextPainter.getBoxesForSelection`
// - `Paragraph.getBoxesForRange`
// and paint the background at the bottom of the `Stack`
final hasOutline = outlineWidth > 0;
Widget? outline;
if (hasOutline) {
outline = Text.rich(
TextSpan(
children: textSpans.map(_toStrokeSpan).toList(),
),
textAlign: textAlign,
softWrap: softWrap,
overflow: overflow,
maxLines: maxLines,
);
if (outlineBlurSigma > 0) {
outline = ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: outlineBlurSigma,
sigmaY: outlineBlurSigma,
),
child: outline,
);
}
}
final fill = Text.rich(
TextSpan(
children: hasOutline ? textSpans.map(_toFillSpan).toList() : textSpans,
),
textAlign: textAlign,
softWrap: softWrap,
overflow: overflow,
maxLines: maxLines,
);
return Stack(
children: [
if (hasOutline)
ImageFiltered(
imageFilter: outlineBlurSigma > 0
? ImageFilter.blur(
sigmaX: outlineBlurSigma,
sigmaY: outlineBlurSigma,
)
: ImageFilter.matrix(
Matrix4.identity().storage,
),
child: Text.rich(
TextSpan(
children: textSpans.map(_toStrokeSpan).toList(),
),
textAlign: textAlign,
softWrap: softWrap,
overflow: overflow,
maxLines: maxLines,
),
),
Text.rich(
TextSpan(
children: hasOutline ? textSpans.map(_toFillSpan).toList() : textSpans,
),
textAlign: textAlign,
softWrap: softWrap,
overflow: overflow,
maxLines: maxLines,
),
if (outline != null) outline,
fill,
],
);
}
@ -89,6 +96,7 @@ class OutlinedText extends StatelessWidget {
children: span.children,
style: (span.style ?? const TextStyle()).copyWith(
backgroundColor: Colors.transparent,
shadows: [],
),
);
}

View file

@ -46,10 +46,11 @@ class _WheelSelectorState<T> extends State<WheelSelector<T>> {
@override
Widget build(BuildContext context) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
const background = Colors.transparent;
final foreground = DefaultTextStyle.of(context).style.color!;
final transitionDuration = context.select<DurationsData, Duration>((v) => v.formTransition);
final itemSize = Size.square(40 * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor));
final itemSize = Size.square(40 * textScaleFactor);
return FocusableActionDetector(
shortcuts: const {
@ -138,10 +139,8 @@ class _WheelSelectorState<T> extends State<WheelSelector<T>> {
switch (intent.type) {
case _ValueAdjustmentType.up:
delta = -1;
break;
case _ValueAdjustmentType.down:
delta = 1;
break;
}
final targetItem = _controller.selectedItem + delta;
final duration = context.read<DurationsData>().formTransition;

View file

@ -26,20 +26,47 @@ enum _ScaleState {
}
class _PointerPanZoomData {
_PointerPanZoomData({required this.focalPoint, required this.scale, required this.rotation});
_PointerPanZoomData.fromStartEvent(this.parent, PointerPanZoomStartEvent event)
: _position = event.position,
_pan = Offset.zero,
_scale = 1,
_rotation = 0;
Offset focalPoint;
double scale;
double rotation;
_PointerPanZoomData.fromUpdateEvent(this.parent, PointerPanZoomUpdateEvent event)
: _position = event.position,
_pan = event.pan,
_scale = event.scale,
_rotation = event.rotation;
final EagerScaleGestureRecognizer parent;
final Offset _position;
final Offset _pan;
final double _scale;
final double _rotation;
Offset get focalPoint {
if (parent.trackpadScrollCausesScale) {
return _position;
}
return _position + _pan;
}
double get scale {
if (parent.trackpadScrollCausesScale) {
return _scale * math.exp((_pan.dx * parent.trackpadScrollToScaleFactor.dx) + (_pan.dy * parent.trackpadScrollToScaleFactor.dy));
}
return _scale;
}
double get rotation => _rotation;
@override
String toString() => '_PointerPanZoomData(focalPoint: $focalPoint, scale: $scale, angle: $rotation)';
String toString() => '_PointerPanZoomData(parent: $parent, _position: $_position, _pan: $_pan, _scale: $_scale, _rotation: $_rotation)';
}
////////////////////////////////////////////////////////////////////////////////
bool _isFlingGesture(Velocity velocity) {
assert(velocity != null);
final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
}
@ -57,9 +84,7 @@ class _LineBetweenPointers {
this.pointerStartId = 0,
this.pointerEndLocation = Offset.zero,
this.pointerEndId = 1,
}) : assert(pointerStartLocation != null && pointerEndLocation != null),
assert(pointerStartId != null && pointerEndId != null),
assert(pointerStartId != pointerEndId);
}) : assert(pointerStartId != pointerEndId);
// The location and the id of the pointer that marks the start of the line.
final Offset pointerStartLocation;
@ -74,23 +99,22 @@ class _LineBetweenPointers {
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
/// calculates their focal point, indicated scale, and rotation. When a focal
/// pointer is established, the recognizer calls [onStart]. As the focal point,
/// scale, rotation change, the recognizer calls [onUpdate]. When the pointers
/// are no longer in contact with the screen, the recognizer calls [onEnd].
/// point is established, the recognizer calls [onStart]. As the focal point,
/// scale, and rotation change, the recognizer calls [onUpdate]. When the
/// pointers are no longer in contact with the screen, the recognizer calls
/// [onEnd].
class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
/// Create a gesture recognizer for interactions intended for scaling content.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
EagerScaleGestureRecognizer({
super.debugOwner,
@Deprecated(
'Migrate to supportedDevices. '
'This feature was deprecated after v2.3.0-1.0.pre.',
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
this.dragStartBehavior = DragStartBehavior.down,
}) : assert(dragStartBehavior != null);
this.trackpadScrollCausesScale = false,
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
});
/// Determines what point is used as the starting point in all calculations
/// involving this gesture.
@ -137,6 +161,26 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
Matrix4? _lastTransform;
/// {@template flutter.gestures.scale.trackpadScrollCausesScale}
/// Whether scrolling up/down on a trackpad should cause scaling instead of
/// panning.
///
/// Defaults to false.
/// {@endtemplate}
bool trackpadScrollCausesScale;
/// {@template flutter.gestures.scale.trackpadScrollToScaleFactor}
/// A factor to control the direction and magnitude of scale when converting
/// trackpad scrolling.
///
/// Incoming trackpad pan offsets will be divided by this factor to get scale
/// values. Increasing this offset will reduce the amount of scaling caused by
/// a fixed amount of trackpad scrolling.
///
/// Defaults to [kDefaultTrackpadScrollToScaleFactor].
/// {@endtemplate}
Offset trackpadScrollToScaleFactor;
late Offset _initialFocalPoint;
Offset? _currentFocalPoint;
late double _initialSpan;
@ -151,6 +195,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
final Map<int, Offset> _pointerLocations = <int, Offset>{};
final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
VelocityTracker? _scaleVelocityTracker;
late Offset _delta;
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
double _initialPanZoomScaleFactor = 1;
@ -271,15 +316,16 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_lastTransform = event.transform;
} else if (event is PointerPanZoomStartEvent) {
assert(_pointerPanZooms[event.pointer] == null);
_pointerPanZooms[event.pointer] = _PointerPanZoomData(focalPoint: event.position, scale: 1, rotation: 0);
_pointerPanZooms[event.pointer] = _PointerPanZoomData.fromStartEvent(this, event);
didChangeConfiguration = true;
shouldStartIfAccepted = true;
_lastTransform = event.transform;
} else if (event is PointerPanZoomUpdateEvent) {
assert(_pointerPanZooms[event.pointer] != null);
if (!event.synthesized) {
if (!event.synthesized && !trackpadScrollCausesScale) {
_velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
}
_pointerPanZooms[event.pointer] = _PointerPanZoomData(focalPoint: event.position + event.pan, scale: event.scale, rotation: event.rotation);
_pointerPanZooms[event.pointer] = _PointerPanZoomData.fromUpdateEvent(this, event);
_lastTransform = event.transform;
shouldStartIfAccepted = true;
} else if (event is PointerPanZoomEndEvent) {
@ -292,7 +338,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_update();
if (!didChangeConfiguration || _reconfigure(event.pointer)) {
_advanceStateMachine(shouldStartIfAccepted, event.kind);
_advanceStateMachine(shouldStartIfAccepted, event);
}
stopTrackingIfPointerNoLongerDown(event);
}
@ -403,18 +449,20 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) {
velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
}
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerCount)));
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
} else {
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount)));
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
}
}
_state = _ScaleState.accepted;
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
return false;
}
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
return true;
}
void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
void _advanceStateMachine(bool shouldStartIfAccepted, PointerEvent event) {
if (_state == _ScaleState.ready) {
_state = _ScaleState.possible;
}
@ -428,7 +476,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (_state == _ScaleState.possible) {
final double spanDelta = (_currentSpan - _initialSpan).abs();
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) {
if (spanDelta > computeScaleSlop(event.kind) || focalPointDelta > computePanSlop(event.kind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) {
resolve(GestureDisposition.accepted);
}
} else if (_state.index >= _ScaleState.accepted.index) {
@ -440,19 +488,22 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_dispatchOnStartCallbackIfNeeded();
}
if (_state == _ScaleState.started && onUpdate != null) {
invokeCallback<void>('onUpdate', () {
onUpdate!(ScaleUpdateDetails(
scale: _scaleFactor,
horizontalScale: _horizontalScaleFactor,
verticalScale: _verticalScaleFactor,
focalPoint: _currentFocalPoint!,
localFocalPoint: _localFocalPoint,
rotation: _computeRotationFactor(),
pointerCount: _pointerCount,
focalPointDelta: _delta,
));
});
if (_state == _ScaleState.started) {
_scaleVelocityTracker?.addPosition(event.timeStamp, Offset(_scaleFactor, 0));
if (onUpdate != null) {
invokeCallback<void>('onUpdate', () {
onUpdate!(ScaleUpdateDetails(
scale: _scaleFactor,
horizontalScale: _horizontalScaleFactor,
verticalScale: _verticalScaleFactor,
focalPoint: _currentFocalPoint!,
localFocalPoint: _localFocalPoint,
rotation: _computeRotationFactor(),
pointerCount: _pointerCount,
focalPointDelta: _delta,
));
});
}
}
}
@ -504,15 +555,12 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
switch (_state) {
case _ScaleState.possible:
resolve(GestureDisposition.rejected);
break;
case _ScaleState.ready:
assert(false); // We should have not seen a pointer yet
break;
case _ScaleState.accepted:
break;
case _ScaleState.started:
assert(false); // We should be in the accepted state when user is done
break;
}
_state = _ScaleState.ready;
}

View file

@ -14,11 +14,9 @@ class ScrollControllerAction extends CallbackAction<ScrollIntent> {
case AxisDirection.up:
case AxisDirection.left:
factor = -1;
break;
case AxisDirection.down:
case AxisDirection.right:
factor = 1;
break;
}
scrollController.animateTo(
scrollController.offset + factor * 150,

View file

@ -53,7 +53,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics {
// Scenario 3:
// If there's no velocity and we're already at where we intend to land,
// do nothing.
if (velocity.abs() < tolerance.velocity && (settlingPixels - metrics.pixels).abs() < tolerance.distance) {
if (velocity.abs() < toleranceFor(position).velocity && (settlingPixels - metrics.pixels).abs() < toleranceFor(position).distance) {
return null;
}
@ -66,7 +66,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics {
metrics.pixels,
settlingPixels,
velocity,
tolerance: tolerance,
tolerance: toleranceFor(position),
);
}
@ -77,7 +77,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics {
metrics.pixels,
settlingPixels,
velocity,
tolerance.velocity * velocity.sign,
toleranceFor(position).velocity * velocity.sign,
);
}
}

View file

@ -16,5 +16,4 @@ class SpringyScrollPhysics extends ScrollPhysics {
parent: buildParent(ancestor),
);
}
}

View file

@ -1,26 +1,22 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class AvesBorder {
static Color _borderColor(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? Colors.white30 : Colors.black26;
// directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery`
// 1 device pixel for straight lines is fine
static double get straightBorderWidth => 1 / window.devicePixelRatio;
static double straightBorderWidth(BuildContext context) => 1 / View.of(context).devicePixelRatio;
// 1 device pixel for curves is too thin
static double get curvedBorderWidth => window.devicePixelRatio > 2 ? 0.5 : 1.0;
static double curvedBorderWidth(BuildContext context) => View.of(context).devicePixelRatio > 2 ? 0.5 : 1.0;
static BorderSide straightSide(BuildContext context, {double? width}) => BorderSide(
color: _borderColor(context),
width: width ?? straightBorderWidth,
width: width ?? straightBorderWidth(context),
);
static BorderSide curvedSide(BuildContext context, {double? width}) => BorderSide(
color: _borderColor(context),
width: width ?? curvedBorderWidth,
width: width ?? curvedBorderWidth(context),
);
static Border border(BuildContext context, {double? width}) => Border.fromBorderSide(curvedSide(context, width: width));

View file

@ -2,6 +2,8 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart';
// adapted from Flutter `_ImageState` in `/widgets/image.dart`
// and `DecorationImagePainter` in `/painting/decoration_image.dart`
@ -13,6 +15,11 @@ class TransitionImage extends StatefulWidget {
final ImageProvider image;
final ValueListenable<double> animation;
final BoxFit thumbnailFit, viewerFit;
final ImageFrameBuilder? frameBuilder;
final ImageLoadingBuilder? loadingBuilder;
final ImageErrorWidgetBuilder? errorBuilder;
final String? semanticLabel;
final bool excludeFromSemantics;
final double? width, height;
final bool gaplessPlayback = false;
final Color? background;
@ -23,6 +30,11 @@ class TransitionImage extends StatefulWidget {
required this.animation,
required this.thumbnailFit,
required this.viewerFit,
this.frameBuilder,
this.loadingBuilder,
this.errorBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.background,
@ -32,16 +44,33 @@ class TransitionImage extends StatefulWidget {
State<TransitionImage> createState() => _TransitionImageState();
}
class _TransitionImageState extends State<TransitionImage> {
class _TransitionImageState extends State<TransitionImage> with WidgetsBindingObserver {
ImageStream? _imageStream;
ImageInfo? _imageInfo;
ImageChunkEvent? _loadingProgress;
bool _isListeningToStream = false;
int? _frameNumber;
bool _wasSynchronouslyLoaded = false;
late DisposableBuildContext<State<TransitionImage>> _scrollAwareContext;
Object? _lastException;
StackTrace? _lastStack;
ImageStreamCompleterHandle? _completerHandle;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_scrollAwareContext = DisposableBuildContext<State<TransitionImage>>(this);
}
@override
void dispose() {
assert(_imageStream != null);
WidgetsBinding.instance.removeObserver(this);
_stopListeningToStream();
_completerHandle?.dispose();
_scrollAwareContext.dispose();
_replaceImage(info: null);
super.dispose();
}
@ -52,21 +81,23 @@ class _TransitionImageState extends State<TransitionImage> {
if (TickerMode.of(context)) {
_listenToStream();
} else {
_stopListeningToStream();
_stopListeningToStream(keepStreamAlive: true);
}
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant TransitionImage oldWidget) {
void didUpdateWidget(TransitionImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (_isListeningToStream) {
if (_isListeningToStream && (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
final ImageStreamListener oldListener = _getListener();
_imageStream!.addListener(_getListener(recreateListener: true));
_imageStream!.removeListener(oldListener);
}
if (widget.image != oldWidget.image) _resolveImage();
if (widget.image != oldWidget.image) {
_resolveImage();
}
}
@override
@ -76,8 +107,11 @@ class _TransitionImageState extends State<TransitionImage> {
}
void _resolveImage() {
final provider = widget.image;
final newStream = provider.resolve(createLocalImageConfiguration(
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
context: _scrollAwareContext,
imageProvider: widget.image,
);
final ImageStream newStream = provider.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
));
@ -88,8 +122,26 @@ class _TransitionImageState extends State<TransitionImage> {
ImageStreamListener _getListener({bool recreateListener = false}) {
if (_imageStreamListener == null || recreateListener) {
_lastException = null;
_lastStack = null;
_imageStreamListener = ImageStreamListener(
_handleImageFrame,
onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
onError: widget.errorBuilder != null || kDebugMode
? (error, stackTrace) {
setState(() {
_lastException = error;
_lastStack = stackTrace;
});
assert(() {
if (widget.errorBuilder == null) {
// ignore: only_throw_errors, since we're just proxying the error.
throw error; // Ensures the error message is printed to the console.
}
return true;
}());
}
: null,
);
}
return _imageStreamListener!;
@ -98,15 +150,32 @@ class _TransitionImageState extends State<TransitionImage> {
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_replaceImage(info: imageInfo);
_loadingProgress = null;
_lastException = null;
_lastStack = null;
_frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
_wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
});
}
void _handleImageChunk(ImageChunkEvent event) {
assert(widget.loadingBuilder != null);
setState(() {
_loadingProgress = event;
_lastException = null;
_lastStack = null;
});
}
void _replaceImage({required ImageInfo? info}) {
_imageInfo?.dispose();
final ImageInfo? oldImageInfo = _imageInfo;
SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());
_imageInfo = info;
}
// Updates _imageStream to newStream, and moves the stream listener
// registration from the old stream to the new stream (if a listener was
// registered).
void _updateSourceStream(ImageStream newStream) {
if (_imageStream?.key == newStream.key) {
return;
@ -123,7 +192,9 @@ class _TransitionImageState extends State<TransitionImage> {
}
setState(() {
_loadingProgress = null;
_frameNumber = null;
_wasSynchronouslyLoaded = false;
});
_imageStream = newStream;
@ -138,22 +209,72 @@ class _TransitionImageState extends State<TransitionImage> {
}
_imageStream!.addListener(_getListener());
_completerHandle?.dispose();
_completerHandle = null;
_isListeningToStream = true;
}
void _stopListeningToStream() {
/// Stops listening to the image stream, if this state object has attached a
/// listener.
///
/// If the listener from this state is the last listener on the stream, the
/// stream will be disposed. To keep the stream alive, set `keepStreamAlive`
/// to true, which create [ImageStreamCompleterHandle] to keep the completer
/// alive and is compatible with the [TickerMode] being off.
void _stopListeningToStream({bool keepStreamAlive = false}) {
if (!_isListeningToStream) {
return;
}
if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) {
_completerHandle = _imageStream!.completer!.keepAlive();
}
_imageStream!.removeListener(_getListener());
_isListeningToStream = false;
}
Widget _debugBuildErrorWidget(BuildContext context, Object error) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
const Positioned.fill(
child: Placeholder(
color: Color(0xCF8D021F),
),
),
Padding(
padding: const EdgeInsets.all(4.0),
child: FittedBox(
child: Text(
'$error',
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
style: const TextStyle(
shadows: <Shadow>[
Shadow(blurRadius: 1.0),
],
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<double>(
if (_lastException != null) {
if (widget.errorBuilder != null) {
return widget.errorBuilder!(context, _lastException!, _lastStack);
}
if (kDebugMode) {
return _debugBuildErrorWidget(context, _lastException!);
}
}
Widget result = ValueListenableBuilder<double>(
valueListenable: widget.animation,
builder: (context, t, child) => CustomPaint(
painter: _TransitionImagePainter(
@ -166,6 +287,35 @@ class _TransitionImageState extends State<TransitionImage> {
),
),
);
if (!widget.excludeFromSemantics) {
result = Semantics(
container: widget.semanticLabel != null,
image: true,
label: widget.semanticLabel ?? '',
child: result,
);
}
if (widget.frameBuilder != null) {
result = widget.frameBuilder!(context, result, _frameNumber, _wasSynchronouslyLoaded);
}
if (widget.loadingBuilder != null) {
result = widget.loadingBuilder!(context, result, _loadingProgress);
}
return result;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(DiagnosticsProperty<ImageStream>('stream', _imageStream));
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
description.add(DiagnosticsProperty<ImageChunkEvent>('loadingProgress', _loadingProgress));
description.add(DiagnosticsProperty<int>('frameNumber', _frameNumber));
description.add(DiagnosticsProperty<bool>('wasSynchronouslyLoaded', _wasSynchronouslyLoaded));
}
}
@ -194,8 +344,11 @@ 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());
if (rect.isEmpty) {
return;
}
final outputSize = rect.size;
final inputSize = Size(image!.width.toDouble(), image!.height.toDouble());
final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size);
final viewerSizes = applyBoxFit(viewerFit, inputSize / scale, size);

View file

@ -44,24 +44,18 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
return (scrollableContext.findRenderObject() as RenderBox).size;
}
Orientation get _windowOrientation {
final size = WidgetsBinding.instance.window.physicalSize;
return size.width > size.height ? Orientation.landscape : Orientation.portrait;
}
final List<StreamSubscription> _subscriptions = [];
// grid section metrics before the app is laid out with the new orientation
late SectionedListLayout<T> _lastSectionedListLayout;
late Size _lastScrollableSize;
late Orientation _lastOrientation;
Orientation _lastOrientation = Orientation.portrait;
@override
void initState() {
super.initState();
final highlightInfo = context.read<HighlightInfo>();
_subscriptions.add(highlightInfo.eventBus.on<TrackEvent<T>>().listen(_trackItem));
_lastOrientation = _windowOrientation;
WidgetsBinding.instance.addObserver(this);
_saveLayoutMetrics();
}
@ -78,9 +72,10 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
@override
void didChangeMetrics() {
// the order of `WidgetsBindingObserver` metrics change notification is unreliable
// w.r.t. the `MediaQuery` update, and consequentially to this widget update:
// w.r.t. the `View` update, and consequentially to this widget update:
// `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update
final orientation = _windowOrientation;
final size = View.of(context).physicalSize;
final orientation = size.width > size.height ? Orientation.landscape : Orientation.portrait;
if (_lastOrientation != orientation) {
_lastOrientation = orientation;
_onLayoutChanged();
@ -138,7 +133,7 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
Future<void> _saveLayoutMetrics() async {
// use a delay to obtain current layout metrics
// so that we can handle window orientation change with the previous metrics,
// regardless of the `MediaQuery`/`WidgetsBindingObserver` order uncertainty
// regardless of the `View`/`WidgetsBindingObserver` order uncertainty
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {

View file

@ -57,7 +57,7 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
@override
Widget build(BuildContext context) {
final gestureSettings = context.select<MediaQueryData, DeviceGestureSettings>((mq) => mq.gestureSettings);
final gestureSettings = MediaQuery.gestureSettingsOf(context);
final child = GestureDetector(
// Horizontal/vertical drag gestures are interpreted as scaling
@ -116,11 +116,9 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
switch (tileLayout) {
case TileLayout.mosaic:
_startSize = Size.square(tileExtentController.extentNotifier.value);
break;
case TileLayout.grid:
case TileLayout.list:
_startSize = renderMetaData.size;
break;
}
_scaledSizeNotifier = ValueNotifier(_startSize!);
@ -143,7 +141,6 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
itemBuilder: widget.mosaicItemBuilder,
),
);
break;
case TileLayout.grid:
case TileLayout.list:
final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
@ -163,7 +160,6 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
),
),
);
break;
}
Overlay.of(scrollableContext).insert(_overlayEntry!);
}
@ -176,11 +172,9 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
case TileLayout.grid:
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
break;
case TileLayout.list:
final scaledHeight = (_startSize!.height * s).clamp(_extentMin!, _extentMax!);
_scaledSizeNotifier!.value = Size(_startSize!.width, scaledHeight);
break;
}
}
@ -200,10 +194,8 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
case TileLayout.mosaic:
case TileLayout.grid:
preferredExtent = _scaledSizeNotifier!.value.width;
break;
case TileLayout.list:
preferredExtent = _scaledSizeNotifier!.value.height;
break;
}
final newExtent = tileExtentController.setUserPreferredExtent(preferredExtent);
_scaledSizeNotifier = null;

View file

@ -5,7 +5,7 @@ class FixedExtentGridRow extends MultiChildRenderObjectWidget {
final double width, height, spacing;
final TextDirection textDirection;
FixedExtentGridRow({
const FixedExtentGridRow({
super.key,
required this.width,
required this.height,

View file

@ -49,7 +49,6 @@ class FixedExtentGridPainter extends CustomPainter {
1,
],
);
break;
case TileLayout.list:
chipSize = Size.square(tileSize.shortestSide);
final chipCenterToEdge = chipSize.width / 2;
@ -71,7 +70,6 @@ class FixedExtentGridPainter extends CustomPainter {
1,
],
);
break;
}
final strokePaint = Paint()
..style = PaintingStyle.stroke

View file

@ -4,7 +4,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FixedExtentScaleOverlay extends StatelessWidget {
final TileLayout tileLayout;
@ -105,7 +104,7 @@ class _OverlayBackgroundState extends State<_OverlayBackground> {
return _initialized
? BoxDecoration(
gradient: RadialGradient(
center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select<MediaQueryData, Size>((mq) => mq.size)),
center: FractionalOffset.fromOffsetAndSize(gradientCenter, MediaQuery.sizeOf(context)),
radius: 1,
colors: isDark
? const [

View file

@ -8,7 +8,7 @@ class MosaicGridRow extends MultiChildRenderObjectWidget {
final double spacing;
final TextDirection textDirection;
MosaicGridRow({
const MosaicGridRow({
super.key,
required this.rowLayout,
required this.spacing,

View file

@ -32,73 +32,68 @@ class AvesAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final useTvLayout = settings.useTvLayout;
return Selector<MediaQueryData, double>(
selector: (context, mq) => mq.padding.top,
builder: (context, mqPaddingTop, child) {
return SliverPersistentHeader(
floating: !useTvLayout,
pinned: pinned,
delegate: _SliverAppBarDelegate(
height: mqPaddingTop + appBarHeightForContentHeight(contentHeight),
child: child!,
),
);
},
child: DirectionalSafeArea(
start: !useTvLayout,
bottom: false,
child: AvesFloatingBar(
builder: (context, backgroundColor, child) => Material(
color: backgroundColor,
child: child,
),
child: Column(
children: [
SizedBox(
height: kToolbarHeight * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor),
child: Row(
children: [
leading != null
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Hero(
tag: leadingHeroTag,
flightShuttleBuilder: _flightShuttleBuilder,
transitionOnUserGestures: true,
child: FontSizeIconTheme(
child: leading!,
return SliverPersistentHeader(
floating: !useTvLayout,
pinned: pinned,
delegate: _SliverAppBarDelegate(
height: MediaQuery.paddingOf(context).top + appBarHeightForContentHeight(contentHeight),
child: DirectionalSafeArea(
start: !useTvLayout,
bottom: false,
child: AvesFloatingBar(
builder: (context, backgroundColor, child) => Material(
color: backgroundColor,
child: child,
),
child: Column(
children: [
SizedBox(
height: kToolbarHeight * textScaleFactor,
child: Row(
children: [
leading != null
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Hero(
tag: leadingHeroTag,
flightShuttleBuilder: _flightShuttleBuilder,
transitionOnUserGestures: true,
child: FontSizeIconTheme(
child: leading!,
),
),
),
)
: const SizedBox(width: 16),
Expanded(
child: DefaultTextStyle(
style: Theme.of(context).appBarTheme.titleTextStyle!,
child: Hero(
tag: titleHeroTag,
flightShuttleBuilder: _flightShuttleBuilder,
transitionOnUserGestures: true,
child: AnimatedSwitcher(
duration: context.read<DurationsData>().iconAnimation,
child: FontSizeIconTheme(
child: Row(
key: ValueKey(transitionKey),
children: [
Expanded(child: title),
...actions,
],
)
: const SizedBox(width: 16),
Expanded(
child: DefaultTextStyle(
style: Theme.of(context).appBarTheme.titleTextStyle!,
child: Hero(
tag: titleHeroTag,
flightShuttleBuilder: _flightShuttleBuilder,
transitionOnUserGestures: true,
child: AnimatedSwitcher(
duration: context.read<DurationsData>().iconAnimation,
child: FontSizeIconTheme(
child: Row(
key: ValueKey(transitionKey),
children: [
Expanded(child: title),
...actions,
],
),
),
),
),
),
),
),
],
],
),
),
),
if (bottom != null) bottom!,
],
if (bottom != null) bottom!,
],
),
),
),
),

View file

@ -31,12 +31,12 @@ enum HeroType { always, onTap, never }
@immutable
class AvesFilterDecoration {
final Widget widget;
final Radius radius;
final Widget widget;
const AvesFilterDecoration({
required this.widget,
required this.radius,
required this.widget,
});
BorderRadius get textBorderRadius => BorderRadius.vertical(bottom: radius);
@ -88,9 +88,9 @@ class AvesFilterChip extends StatefulWidget {
required double chipPadding,
required double rowPadding,
}) {
return context.select<MediaQueryData, double>((mq) {
return (mq.size.width - mq.padding.horizontal - chipPadding * minChipPerRow - rowPadding) / minChipPerRow;
});
final mqWidth = MediaQuery.sizeOf(context).width;
final mqHorizontalPadding = MediaQuery.paddingOf(context).horizontal;
return (mqWidth - mqHorizontalPadding - chipPadding * minChipPerRow - rowPadding) / minChipPerRow;
}
static Future<void> showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async {

View file

@ -37,7 +37,7 @@ class AvesLogo extends StatelessWidget {
radius: size / 2,
child: CircleAvatar(
backgroundColor: Colors.white,
radius: size / 2 - AvesBorder.curvedBorderWidth,
radius: size / 2 - AvesBorder.curvedBorderWidth(context),
child: Padding(
padding: EdgeInsets.only(top: size / 15),
child: child,

View file

@ -73,7 +73,7 @@ class _OverlayButtonState extends State<OverlayButton> {
builder: (context, focused, child) {
final border = AvesBorder.border(
context,
width: AvesBorder.curvedBorderWidth * (focused ? 3 : 1),
width: AvesBorder.curvedBorderWidth(context) * (focused ? 3 : 1),
);
return borderRadius != null
? BlurredRRect(

View file

@ -42,7 +42,6 @@ class MapButtonPanel extends StatelessWidget {
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
);
}
break;
case MapNavigationButton.map:
if (openMapPage != null) {
navigationButton = MapOverlayButton(
@ -51,7 +50,6 @@ class MapButtonPanel extends StatelessWidget {
tooltip: context.l10n.openMapPageTooltip,
);
}
break;
case MapNavigationButton.none:
break;
}

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/images.dart';
@ -35,6 +34,7 @@ class GeoMap extends StatefulWidget {
final AvesMapController? controller;
final Listenable? collectionListenable;
final List<AvesEntry> entries;
final Size availableSize;
final LatLng? initialCenter;
final ValueNotifier<bool> isAnimatingNotifier;
final ValueNotifier<LatLng?>? dotLocationNotifier;
@ -60,6 +60,7 @@ class GeoMap extends StatefulWidget {
this.controller,
this.collectionListenable,
required this.entries,
required this.availableSize,
this.initialCenter,
required this.isAnimatingNotifier,
this.dotLocationNotifier,
@ -133,6 +134,13 @@ class _GeoMapState extends State<GeoMap> {
@override
Widget build(BuildContext context) {
final devicePixelRatio = View.of(context).devicePixelRatio;
void onMarkerLongPress(GeoEntry<AvesEntry> geoEntry, LatLng tapLocation) => _onMarkerLongPress(
geoEntry: geoEntry,
tapLocation: tapLocation,
devicePixelRatio: devicePixelRatio,
);
return Selector<Settings, EntryMapStyle?>(
selector: (context, s) => s.mapStyle,
builder: (context, mapStyle, child) {
@ -143,6 +151,7 @@ class _GeoMapState extends State<GeoMap> {
buildThumbnailImage: (extent) => ThumbnailImage(
entry: key.entry,
extent: extent,
devicePixelRatio: devicePixelRatio,
progressive: !isHeavy,
),
);
@ -173,9 +182,8 @@ class _GeoMapState extends State<GeoMap> {
onUserZoomChange: widget.onUserZoomChange,
onMapTap: widget.onMapTap,
onMarkerTap: _onMarkerTap,
onMarkerLongPress: _onMarkerLongPress,
onMarkerLongPress: onMarkerLongPress,
);
break;
case EntryMapStyle.osmHot:
case EntryMapStyle.stamenToner:
case EntryMapStyle.stamenWatercolor:
@ -204,9 +212,8 @@ class _GeoMapState extends State<GeoMap> {
onUserZoomChange: widget.onUserZoomChange,
onMapTap: widget.onMapTap,
onMarkerTap: _onMarkerTap,
onMarkerLongPress: _onMarkerLongPress,
onMarkerLongPress: onMarkerLongPress,
);
break;
}
} else {
final overlay = Center(
@ -360,8 +367,8 @@ class _GeoMapState extends State<GeoMap> {
);
bounds = bounds.copyWith(zoom: max(minInitialZoom, bounds.zoom.floorToDouble()));
final availableSize = window.physicalSize / window.devicePixelRatio;
final neededSize = bounds.toDisplaySize();
final availableSize = widget.availableSize;
if (neededSize.width > availableSize.width || neededSize.height > availableSize.height) {
return _initBoundsForEntries(entries: entries, recentCount: (recentCount ?? 10000) ~/ 10);
}
@ -457,7 +464,11 @@ class _GeoMapState extends State<GeoMap> {
onTap(markerLocation, markerEntry);
}
Future<void> _onMarkerLongPress(GeoEntry<AvesEntry> geoEntry, LatLng tapLocation) async {
Future<void> _onMarkerLongPress({
required GeoEntry<AvesEntry> geoEntry,
required LatLng tapLocation,
required double devicePixelRatio,
}) async {
final onMarkerLongPress = widget.onMarkerLongPress;
if (onMarkerLongPress == null) return;
@ -478,6 +489,7 @@ class _GeoMapState extends State<GeoMap> {
buildThumbnailImage: (extent) => ThumbnailImage(
entry: markerEntry,
extent: extent,
devicePixelRatio: devicePixelRatio,
),
);
onMarkerLongPress(

View file

@ -82,7 +82,6 @@ class ScaleLayerWidget extends StatelessWidget {
// meters
distanceMeters = scaleMeters[scaleLevel];
displayDistance = distanceMeters >= metersInAKilometer ? '${(distanceMeters / metersInAKilometer).toStringAsFixed(0)} km' : '${distanceMeters.toStringAsFixed(0)} m';
break;
case UnitSystem.imperial:
if (scaleLevel < 15) {
// miles
@ -95,7 +94,6 @@ class ScaleLayerWidget extends StatelessWidget {
distanceMeters = distanceFeet * metersInAFoot;
displayDistance = '${distanceFeet.toStringAsFixed(0)} ft';
}
break;
}
final start = map.project(center);

View file

@ -1,7 +1,6 @@
import 'package:aves/model/device.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:provider/provider.dart';
const _tileLayerBackgroundColor = Colors.transparent;
@ -14,7 +13,7 @@ class OSMHotLayer extends StatelessWidget {
urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
subdomains: const ['a', 'b', 'c'],
backgroundColor: _tileLayerBackgroundColor,
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
retinaMode: MediaQuery.devicePixelRatioOf(context) > 1,
userAgentPackageName: device.userAgent,
);
}
@ -29,7 +28,7 @@ class StamenTonerLayer extends StatelessWidget {
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png',
subdomains: const ['a', 'b', 'c', 'd'],
backgroundColor: _tileLayerBackgroundColor,
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
retinaMode: MediaQuery.devicePixelRatioOf(context) > 1,
userAgentPackageName: device.userAgent,
);
}
@ -44,7 +43,7 @@ class StamenWatercolorLayer extends StatelessWidget {
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg',
subdomains: const ['a', 'b', 'c', 'd'],
backgroundColor: _tileLayerBackgroundColor,
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
retinaMode: MediaQuery.devicePixelRatioOf(context) > 1,
userAgentPackageName: device.userAgent,
);
}

View file

@ -26,13 +26,10 @@ class MapActionDelegate {
),
onSelection: (v) => settings.mapStyle = v,
);
break;
case MapAction.zoomIn:
controller?.zoomBy(1);
break;
case MapAction.zoomOut:
controller?.zoomBy(-1);
break;
}
}
}

View file

@ -114,13 +114,11 @@ class _SearchPageState extends State<SearchPage> {
key: const ValueKey<SearchBody>(SearchBody.suggestions),
child: widget.delegate.buildSuggestions(context),
);
break;
case SearchBody.results:
body = KeyedSubtree(
key: const ValueKey<SearchBody>(SearchBody.results),
child: widget.delegate.buildResults(context),
);
break;
case null:
break;
}

View file

@ -14,7 +14,8 @@ class DecoratedThumbnail extends StatelessWidget {
final Object? Function()? heroTagger;
static final Color borderColor = Colors.grey.shade700;
static final double borderWidth = AvesBorder.straightBorderWidth;
static double borderWidth(BuildContext context) => AvesBorder.straightBorderWidth(context);
const DecoratedThumbnail({
super.key,
@ -35,6 +36,7 @@ class DecoratedThumbnail extends StatelessWidget {
Widget child = ThumbnailImage(
entry: entry,
extent: tileExtent,
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
isMosaic: isMosaic,
cancellableNotifier: cancellableNotifier,
heroTag: heroTagger?.call(),
@ -64,7 +66,7 @@ class DecoratedThumbnail extends StatelessWidget {
foregroundDecoration: BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: borderColor,
width: borderWidth,
width: borderWidth(context),
)),
),
width: thumbnailWidth,

View file

@ -1,5 +1,4 @@
import 'dart:math';
import 'dart:ui';
import 'package:aves/image_providers/thumbnail_provider.dart';
import 'package:aves/model/entry/entry.dart';
@ -20,7 +19,7 @@ import 'package:provider/provider.dart';
class ThumbnailImage extends StatefulWidget {
final AvesEntry entry;
final double extent;
final double extent, devicePixelRatio;
final bool isMosaic, progressive;
final BoxFit? fit;
final bool showLoadingBackground;
@ -31,6 +30,7 @@ class ThumbnailImage extends StatefulWidget {
super.key,
required this.entry,
required this.extent,
required this.devicePixelRatio,
this.progressive = true,
this.isMosaic = false,
this.fit,
@ -57,7 +57,6 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
ImageInfo? _lastImageInfo;
Object? _lastException;
late final ImageStreamListener _streamListener;
late DisposableBuildContext<State<ThumbnailImage>> _scrollAwareContext;
AvesEntry get entry => widget.entry;
@ -69,7 +68,6 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
void initState() {
super.initState();
_streamListener = ImageStreamListener(_onImageLoad, onError: _onError);
_scrollAwareContext = DisposableBuildContext<State<ThumbnailImage>>(this);
_registerWidget(widget);
}
@ -85,7 +83,6 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
@override
void dispose() {
_unregisterWidget(widget);
_scrollAwareContext.dispose();
super.dispose();
}
@ -126,16 +123,10 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
_providers.addAll([
if (lowQuality != null)
_ConditionalImageProvider(
ScrollAwareImageProvider(
context: _scrollAwareContext,
imageProvider: lowQuality,
),
lowQuality,
),
_ConditionalImageProvider(
ScrollAwareImageProvider(
context: _scrollAwareContext,
imageProvider: highQuality,
),
highQuality,
_needSizedProvider,
),
]);
@ -176,8 +167,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
bool _needSizedProvider(ImageInfo? currentImageInfo) {
if (currentImageInfo == null) return true;
final currentImage = currentImageInfo.image;
// directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery`
final sizedThreshold = extent * window.devicePixelRatio;
final sizedThreshold = extent * widget.devicePixelRatio;
return sizedThreshold > min(currentImage.width, currentImage.height);
}

View file

@ -182,18 +182,15 @@ class _AppDebugPageState extends State<AppDebugPage> {
}, false);
await favourites.clear();
await favourites.add(source.visibleEntries);
break;
case AppDebugAction.prepScreenshotStats:
settings.changeFilterVisibility(settings.hiddenFilters, true);
settings.changeFilterVisibility({
PathFilter('/storage/emulated/0/Pictures/Dev'),
}, false);
break;
case AppDebugAction.prepScreenshotCountries:
settings.changeFilterVisibility({
LocationFilter(LocationLevel.country, 'Belgium;BE'),
}, false);
break;
case AppDebugAction.mediaStoreScanDir:
// scan files copied from test assets
// we do it via the app instead of broadcasting via ADB
@ -202,7 +199,6 @@ class _AppDebugPageState extends State<AppDebugPage> {
context: context,
builder: (context) => const MediaStoreScanDirDialog(),
);
break;
case AppDebugAction.greenScreen:
await Navigator.maybeOf(context)?.push(
MaterialPageRoute(
@ -212,7 +208,6 @@ class _AppDebugPageState extends State<AppDebugPage> {
),
),
);
break;
}
}
}

View file

@ -66,7 +66,7 @@ class DebugSettingsSection extends StatelessWidget {
'recentDestinationAlbums': toMultiline(settings.recentDestinationAlbums),
'recentTags': toMultiline(settings.recentTags),
'locale': '${settings.locale}',
'systemLocales': '${WidgetsBinding.instance.window.locales}',
'systemLocales': '${WidgetsBinding.instance.platformDispatcher.locales}',
'topEntryIds': '${settings.topEntryIds}',
},
),

View file

@ -8,7 +8,6 @@ import 'package:aves/widgets/dialogs/item_picker.dart';
import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'aves_dialog.dart';
@ -60,7 +59,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
return MediaQueryDataProvider(
child: Builder(
builder: (context) {
final shortestSide = context.select<MediaQueryData, double>((mq) => mq.size.shortestSide);
final shortestSide = MediaQuery.sizeOf(context).shortestSide;
final extent = (shortestSide / 3.0).clamp(60.0, 160.0);
return AvesDialog(
scrollableContent: [

View file

@ -72,16 +72,12 @@ void _skipConfirmation(ConfirmationDialog type) {
switch (type) {
case ConfirmationDialog.createVault:
settings.confirmCreateVault = false;
break;
case ConfirmationDialog.deleteForever:
settings.confirmDeleteForever = false;
break;
case ConfirmationDialog.moveToBin:
settings.confirmMoveToBin = false;
break;
case ConfirmationDialog.moveUndatedItems:
settings.confirmMoveUndatedItems = false;
break;
}
}

View file

@ -3,7 +3,6 @@ import 'dart:ui';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AvesDialog extends StatelessWidget {
static const confirmationRouteName = '/dialog/confirmation';
@ -104,7 +103,7 @@ class AvesDialog extends StatelessWidget {
// workaround because the dialog tries
// to size itself to the content intrinsic size,
// but the `ListView` viewport does not have one
width: context.select<MediaQueryData, double>((mq) => mq.size.width / 2),
width: MediaQuery.sizeOf(context).width / 2,
child: DecoratedBox(
decoration: contentDecoration(context),
child: child,

View file

@ -78,11 +78,9 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
final displaySize = entries.first.displaySize;
_widthController.text = '${displaySize.width.round()}';
_heightController.text = '${displaySize.height.round()}';
break;
case LengthUnit.percent:
_widthController.text = '100';
_heightController.text = '100';
break;
}
}
@ -149,10 +147,8 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
switch (_lengthUnit) {
case LengthUnit.px:
_heightController.text = '${(width / entries.first.displayAspectRatio).round()}';
break;
case LengthUnit.percent:
_heightController.text = '$width';
break;
}
} else {
_heightController.text = '';
@ -175,10 +171,8 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
switch (_lengthUnit) {
case LengthUnit.px:
_widthController.text = '${(height * entries.first.displayAspectRatio).round()}';
break;
case LengthUnit.percent:
_widthController.text = '$height';
break;
}
} else {
_widthController.text = '';

View file

@ -145,7 +145,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
Widget _buildSetCustomContent(BuildContext context) {
final l10n = context.l10n;
final locale = l10n.localeName;
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
final use24hour = MediaQuery.alwaysUse24HourFormatOf(context);
return Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
@ -179,7 +179,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
Widget _buildCopyItemContent(BuildContext context) {
final l10n = context.l10n;
final locale = l10n.localeName;
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
final use24hour = MediaQuery.alwaysUse24HourFormatOf(context);
return Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
@ -365,11 +365,9 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
case DateEditAction.copyItem:
case DateEditAction.extractFromTitle:
_isValidNotifier.value = true;
break;
case DateEditAction.shift:
case DateEditAction.remove:
_isValidNotifier.value = _fields.isNotEmpty;
break;
}
}

View file

@ -307,33 +307,26 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
switch (_action) {
case LocationEditAction.chooseOnMap:
_isValidNotifier.value = _mapCoordinates != null;
break;
case LocationEditAction.copyItem:
_isValidNotifier.value = _copyItemSource.hasGps;
break;
case LocationEditAction.setCustom:
_isValidNotifier.value = _parseLatLng() != null;
break;
case LocationEditAction.remove:
_isValidNotifier.value = true;
break;
}
}
void _submit(BuildContext context) {
final navigator = Navigator.maybeOf(context);
switch (_action) {
case LocationEditAction.chooseOnMap:
Navigator.maybeOf(context)?.pop(_mapCoordinates);
break;
navigator?.pop(_mapCoordinates);
case LocationEditAction.copyItem:
Navigator.maybeOf(context)?.pop(_copyItemSource.latLng);
break;
navigator?.pop(_copyItemSource.latLng);
case LocationEditAction.setCustom:
Navigator.maybeOf(context)?.pop(_parseLatLng());
break;
navigator?.pop(_parseLatLng());
case LocationEditAction.remove:
Navigator.maybeOf(context)?.pop(ExtraAvesEntryMetadataEdition.removalLocation);
break;
navigator?.pop(ExtraAvesEntryMetadataEdition.removalLocation);
}
}
}

View file

@ -32,11 +32,9 @@ class _EditEntryRatingDialogState extends State<EditEntryRatingDialog> {
case -1:
_action = _RatingAction.rejected;
_rating = 0;
break;
case 0:
_action = _RatingAction.unrated;
_rating = 0;
break;
default:
_action = _RatingAction.set;
_rating = entryRating;
@ -121,13 +119,10 @@ class _EditEntryRatingDialogState extends State<EditEntryRatingDialog> {
switch (_action) {
case _RatingAction.set:
entryRating = _rating;
break;
case _RatingAction.rejected:
entryRating = -1;
break;
case _RatingAction.unrated:
entryRating = 0;
break;
}
Navigator.maybeOf(context)?.pop(entryRating);
}

View file

@ -16,7 +16,6 @@ import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
class RenameEntrySetPage extends StatefulWidget {
static const routeName = '/rename_entry_set';
@ -60,6 +59,8 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final effectiveThumbnailExtent = max(thumbnailExtent, thumbnailExtent * textScaleFactor);
return AvesScaffold(
appBar: AppBar(
title: Text(l10n.renameEntrySetPageTitle),
@ -121,63 +122,58 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
),
),
Expanded(
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.textScaleFactor,
builder: (context, textScaleFactor, child) {
final effectiveThumbnailExtent = max(thumbnailExtent, thumbnailExtent * textScaleFactor);
return GridTheme(
extent: effectiveThumbnailExtent,
child: ListView.separated(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 12),
itemBuilder: (context, index) {
final entry = entries[index];
final sourceName = entry.filenameWithoutExtension ?? '';
return Row(
child: GridTheme(
extent: effectiveThumbnailExtent,
child: ListView.separated(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 12),
itemBuilder: (context, index) {
final entry = entries[index];
final sourceName = entry.filenameWithoutExtension ?? '';
return Row(
children: [
DecoratedThumbnail(
entry: entry,
tileExtent: effectiveThumbnailExtent,
selectable: false,
highlightable: false,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DecoratedThumbnail(
entry: entry,
tileExtent: effectiveThumbnailExtent,
selectable: false,
highlightable: false,
Text(
sourceName,
style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sourceName,
style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
const SizedBox(height: 4),
ValueListenableBuilder<NamingPattern>(
valueListenable: _namingPatternNotifier,
builder: (context, pattern, child) {
return Text(
pattern.apply(entry, index),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
);
},
),
],
),
const SizedBox(height: 4),
ValueListenableBuilder<NamingPattern>(
valueListenable: _namingPatternNotifier,
builder: (context, pattern, child) {
return Text(
pattern.apply(entry, index),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
);
},
),
],
);
},
separatorBuilder: (context, index) => const SizedBox(
height: CollectionGrid.fixedExtentLayoutSpacing,
),
),
itemCount: min(entryCount, previewMax),
),
],
);
}),
},
separatorBuilder: (context, index) => const SizedBox(
height: CollectionGrid.fixedExtentLayoutSpacing,
),
itemCount: min(entryCount, previewMax),
),
),
),
const Divider(height: 0),
Center(

Some files were not shown because too many files have changed in this diff Show more