upgraded Flutter to stable v3.10.0, agp 8, dart 3, use media query aspects
This commit is contained in:
parent
01d2e21369
commit
28973ec322
208 changed files with 1905 additions and 1988 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
|
||||
Subproject commit 84a1e904f44f9b0e9c4510138010edcc653163f8
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
]);
|
||||
|
|
|
@ -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>{
|
||||
|
|
|
@ -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]}');
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,6 @@ class CollectionSectionHeader extends StatelessWidget {
|
|||
case EntryGroupFactor.none:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EntrySortFactor.name:
|
||||
return _buildAlbumHeader(context);
|
||||
case EntrySortFactor.rating:
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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) {},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,5 +16,4 @@ class SpringyScrollPhysics extends ScrollPhysics {
|
|||
parent: buildParent(ancestor),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}',
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -57,7 +57,10 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
static const double itemPickerExtent = 46;
|
||||
static const double appPickerExtent = 32;
|
||||
|
||||
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
|
||||
double tabBarHeight(BuildContext context) {
|
||||
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||
return 64 * max(1, textScaleFactor);
|
||||
}
|
||||
|
||||
static const double tabIndicatorWeight = 2;
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pattern_lock/pattern_lock.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PatternDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/pattern';
|
||||
|
@ -33,7 +32,7 @@ class _PatternDialogState extends State<PatternDialog> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: SizedBox.square(
|
||||
dimension: context.select<MediaQueryData, double>((mq) => mq.size.shortestSide / 2),
|
||||
dimension: MediaQuery.sizeOf(context).shortestSide / 2,
|
||||
child: PatternLock(
|
||||
relativePadding: .4,
|
||||
selectedColor: colorScheme.secondary,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue