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
|
### Added
|
||||||
|
|
||||||
- option to set the Tags page as home
|
- option to set the Tags page as home
|
||||||
|
- support for animated PNG
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- remember whether to show the title filter when picking albums
|
- 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
|
## <a id="v1.8.6"></a>[v1.8.6] - 2023-04-30
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
|
@ -46,7 +48,7 @@ if (keystorePropertiesFile.exists()) {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'deckers.thibault.aves'
|
namespace 'deckers.thibault.aves'
|
||||||
compileSdkVersion 33
|
compileSdk 33
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
@ -60,10 +62,6 @@ android {
|
||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
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 {
|
flutter {
|
||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
@ -195,10 +202,10 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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.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.exifinterface:exifinterface:1.3.6'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
|
||||||
implementation 'androidx.media:media:1.6.0'
|
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"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="deckers.thibault.aves"
|
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
kotlin_version = '1.8.21'
|
kotlin_version = '1.8.21'
|
||||||
agp_version = '7.4.2'
|
agp_version = '8.0.1'
|
||||||
glide_version = '4.15.1'
|
glide_version = '4.15.1'
|
||||||
huawei_agconnect_version = '1.8.0.300'
|
huawei_agconnect_version = '1.8.0.300'
|
||||||
abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||||
|
|
|
@ -15,3 +15,6 @@ android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
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
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
@override
|
||||||
ImageStreamCompleter loadBuffer(AppIconImageKey key, DecoderBufferCallback decode) {
|
ImageStreamCompleter loadImage(AppIconImageKey key, ImageDecoderCallback decode) {
|
||||||
return MultiFrameImageStreamCompleter(
|
return MultiFrameImageStreamCompleter(
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
scale: key.scale,
|
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 {
|
try {
|
||||||
final bytes = await appService.getAppIcon(key.packageName, key.size);
|
final bytes = await appService.getAppIcon(key.packageName, key.size);
|
||||||
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
|
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
|
||||||
|
|
|
@ -18,7 +18,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadBuffer(RegionProviderKey key, DecoderBufferCallback decode) {
|
ImageStreamCompleter loadImage(RegionProviderKey key, ImageDecoderCallback decode) {
|
||||||
return MultiFrameImageStreamCompleter(
|
return MultiFrameImageStreamCompleter(
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
scale: 1.0,
|
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 uri = key.uri;
|
||||||
final mimeType = key.mimeType;
|
final mimeType = key.mimeType;
|
||||||
final pageId = key.pageId;
|
final pageId = key.pageId;
|
||||||
|
|
|
@ -19,7 +19,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadBuffer(ThumbnailProviderKey key, DecoderBufferCallback decode) {
|
ImageStreamCompleter loadImage(ThumbnailProviderKey key, ImageDecoderCallback decode) {
|
||||||
return MultiFrameImageStreamCompleter(
|
return MultiFrameImageStreamCompleter(
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
scale: 1.0,
|
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 uri = key.uri;
|
||||||
final mimeType = key.mimeType;
|
final mimeType = key.mimeType;
|
||||||
final pageId = key.pageId;
|
final pageId = key.pageId;
|
||||||
|
|
|
@ -32,7 +32,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadBuffer(UriImage key, DecoderBufferCallback decode) {
|
ImageStreamCompleter loadImage(UriImage key, ImageDecoderCallback decode) {
|
||||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||||
|
|
||||||
return MultiFrameImageStreamCompleter(
|
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);
|
assert(key == this);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -21,34 +21,24 @@ class MetadataDbUpgrader {
|
||||||
switch (oldVersion) {
|
switch (oldVersion) {
|
||||||
case 1:
|
case 1:
|
||||||
await _upgradeFrom1(db);
|
await _upgradeFrom1(db);
|
||||||
break;
|
|
||||||
case 2:
|
case 2:
|
||||||
await _upgradeFrom2(db);
|
await _upgradeFrom2(db);
|
||||||
break;
|
|
||||||
case 3:
|
case 3:
|
||||||
await _upgradeFrom3(db);
|
await _upgradeFrom3(db);
|
||||||
break;
|
|
||||||
case 4:
|
case 4:
|
||||||
await _upgradeFrom4(db);
|
await _upgradeFrom4(db);
|
||||||
break;
|
|
||||||
case 5:
|
case 5:
|
||||||
await _upgradeFrom5(db);
|
await _upgradeFrom5(db);
|
||||||
break;
|
|
||||||
case 6:
|
case 6:
|
||||||
await _upgradeFrom6(db);
|
await _upgradeFrom6(db);
|
||||||
break;
|
|
||||||
case 7:
|
case 7:
|
||||||
await _upgradeFrom7(db);
|
await _upgradeFrom7(db);
|
||||||
break;
|
|
||||||
case 8:
|
case 8:
|
||||||
await _upgradeFrom8(db);
|
await _upgradeFrom8(db);
|
||||||
break;
|
|
||||||
case 9:
|
case 9:
|
||||||
await _upgradeFrom9(db);
|
await _upgradeFrom9(db);
|
||||||
break;
|
|
||||||
case 10:
|
case 10:
|
||||||
await _upgradeFrom10(db);
|
await _upgradeFrom10(db);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
oldVersion++;
|
oldVersion++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ extension ExtraAvesEntryCatalog on AvesEntry {
|
||||||
if (isGeotiff && !hasGps) {
|
if (isGeotiff && !hasGps) {
|
||||||
final info = await metadataFetchService.getGeoTiffInfo(this);
|
final info = await metadataFetchService.getGeoTiffInfo(this);
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
final center = MappedGeoTiff(
|
final center = GeoTiffCoordinateConverter(
|
||||||
info: info,
|
info: info,
|
||||||
entry: this,
|
entry: this,
|
||||||
).center;
|
).center;
|
||||||
|
|
|
@ -52,7 +52,6 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
case DateEditAction.copyItem:
|
case DateEditAction.copyItem:
|
||||||
case DateEditAction.extractFromTitle:
|
case DateEditAction.extractFromTitle:
|
||||||
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
|
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
|
||||||
break;
|
|
||||||
case DateEditAction.shift:
|
case DateEditAction.shift:
|
||||||
final xmpDate = XMP.getString(descriptions, XmpAttributes.xmpCreateDate, namespace: XmpNamespaces.xmp);
|
final xmpDate = XMP.getString(descriptions, XmpAttributes.xmpCreateDate, namespace: XmpNamespaces.xmp);
|
||||||
if (xmpDate != null) {
|
if (xmpDate != null) {
|
||||||
|
@ -65,10 +64,8 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
reportService.recordError('failed to parse XMP date=$xmpDate', null);
|
reportService.recordError('failed to parse XMP date=$xmpDate', null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case DateEditAction.remove:
|
case DateEditAction.remove:
|
||||||
editCreateDateXmp(descriptions, null);
|
editCreateDateXmp(descriptions, null);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
@ -541,10 +538,8 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on FileSystemException catch (_) {}
|
} on FileSystemException catch (_) {}
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
date = await metadataFetchService.getDate(this, source.toMetadataField()!);
|
date = await metadataFetchService.getDate(this, source.toMetadataField()!);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
|
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
|
||||||
|
|
|
@ -78,7 +78,6 @@ class AlbumFilter extends CoveredCollectionFilter {
|
||||||
case AlbumType.app:
|
case AlbumType.app:
|
||||||
final appColor = colors.appColor(album);
|
final appColor = colors.appColor(album);
|
||||||
if (appColor != null) return appColor;
|
if (appColor != null) return appColor;
|
||||||
break;
|
|
||||||
case AlbumType.camera:
|
case AlbumType.camera:
|
||||||
return SynchronousFuture(colors.albumCamera);
|
return SynchronousFuture(colors.albumCamera);
|
||||||
case AlbumType.download:
|
case AlbumType.download:
|
||||||
|
|
|
@ -21,13 +21,10 @@ class AspectRatioFilter extends CollectionFilter {
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case QueryFilter.opEqual:
|
case QueryFilter.opEqual:
|
||||||
_test = (entry) => entry.displayAspectRatio == threshold;
|
_test = (entry) => entry.displayAspectRatio == threshold;
|
||||||
break;
|
|
||||||
case QueryFilter.opLower:
|
case QueryFilter.opLower:
|
||||||
_test = (entry) => entry.displayAspectRatio < threshold;
|
_test = (entry) => entry.displayAspectRatio < threshold;
|
||||||
break;
|
|
||||||
case QueryFilter.opGreater:
|
case QueryFilter.opGreater:
|
||||||
_test = (entry) => entry.displayAspectRatio > threshold;
|
_test = (entry) => entry.displayAspectRatio > threshold;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,13 +25,10 @@ class DateFilter extends CollectionFilter {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case DateLevel.y:
|
case DateLevel.y:
|
||||||
_test = (entry) => entry.bestDate?.isAtSameYearAs(_effectiveDate) ?? false;
|
_test = (entry) => entry.bestDate?.isAtSameYearAs(_effectiveDate) ?? false;
|
||||||
break;
|
|
||||||
case DateLevel.ym:
|
case DateLevel.ym:
|
||||||
_test = (entry) => entry.bestDate?.isAtSameMonthAs(_effectiveDate) ?? false;
|
_test = (entry) => entry.bestDate?.isAtSameMonthAs(_effectiveDate) ?? false;
|
||||||
break;
|
|
||||||
case DateLevel.ymd:
|
case DateLevel.ymd:
|
||||||
_test = (entry) => entry.bestDate?.isAtSameDayAs(_effectiveDate) ?? false;
|
_test = (entry) => entry.bestDate?.isAtSameDayAs(_effectiveDate) ?? false;
|
||||||
break;
|
|
||||||
case DateLevel.md:
|
case DateLevel.md:
|
||||||
final month = _effectiveDate.month;
|
final month = _effectiveDate.month;
|
||||||
final day = _effectiveDate.day;
|
final day = _effectiveDate.day;
|
||||||
|
@ -39,15 +36,12 @@ class DateFilter extends CollectionFilter {
|
||||||
final bestDate = entry.bestDate;
|
final bestDate = entry.bestDate;
|
||||||
return bestDate != null && bestDate.month == month && bestDate.day == day;
|
return bestDate != null && bestDate.month == month && bestDate.day == day;
|
||||||
};
|
};
|
||||||
break;
|
|
||||||
case DateLevel.m:
|
case DateLevel.m:
|
||||||
final month = _effectiveDate.month;
|
final month = _effectiveDate.month;
|
||||||
_test = (entry) => entry.bestDate?.month == month;
|
_test = (entry) => entry.bestDate?.month == month;
|
||||||
break;
|
|
||||||
case DateLevel.d:
|
case DateLevel.d:
|
||||||
final day = _effectiveDate.day;
|
final day = _effectiveDate.day;
|
||||||
_test = (entry) => entry.bestDate?.day == day;
|
_test = (entry) => entry.bestDate?.day == day;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,10 @@ class LocationFilter extends CoveredCollectionFilter {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case LocationLevel.country:
|
case LocationLevel.country:
|
||||||
_test = (entry) => entry.addressDetails?.countryCode == _code;
|
_test = (entry) => entry.addressDetails?.countryCode == _code;
|
||||||
break;
|
|
||||||
case LocationLevel.state:
|
case LocationLevel.state:
|
||||||
_test = (entry) => entry.addressDetails?.stateCode == _code;
|
_test = (entry) => entry.addressDetails?.stateCode == _code;
|
||||||
break;
|
|
||||||
case LocationLevel.place:
|
case LocationLevel.place:
|
||||||
_test = (entry) => entry.addressDetails?.place == _location;
|
_test = (entry) => entry.addressDetails?.place == _location;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +54,6 @@ class LocationFilter extends CoveredCollectionFilter {
|
||||||
if (_code != null) {
|
if (_code != null) {
|
||||||
location = _nameAndCode;
|
location = _nameAndCode;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case LocationLevel.place:
|
case LocationLevel.place:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,15 +26,12 @@ class MissingFilter extends CollectionFilter {
|
||||||
case _date:
|
case _date:
|
||||||
_test = (entry) => (entry.catalogMetadata?.dateMillis ?? 0) == 0;
|
_test = (entry) => (entry.catalogMetadata?.dateMillis ?? 0) == 0;
|
||||||
_icon = AIcons.dateUndated;
|
_icon = AIcons.dateUndated;
|
||||||
break;
|
|
||||||
case _fineAddress:
|
case _fineAddress:
|
||||||
_test = (entry) => entry.hasGps && !entry.hasFineAddress;
|
_test = (entry) => entry.hasGps && !entry.hasFineAddress;
|
||||||
_icon = AIcons.locationUnlocated;
|
_icon = AIcons.locationUnlocated;
|
||||||
break;
|
|
||||||
case _title:
|
case _title:
|
||||||
_test = (entry) => (entry.catalogMetadata?.xmpTitle ?? '').isEmpty;
|
_test = (entry) => (entry.catalogMetadata?.xmpTitle ?? '').isEmpty;
|
||||||
_icon = AIcons.descriptionUntitled;
|
_icon = AIcons.descriptionUntitled;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,13 +28,10 @@ class PlaceholderFilter extends CollectionFilter {
|
||||||
switch (placeholder) {
|
switch (placeholder) {
|
||||||
case _country:
|
case _country:
|
||||||
_icon = AIcons.country;
|
_icon = AIcons.country;
|
||||||
break;
|
|
||||||
case _state:
|
case _state:
|
||||||
_icon = AIcons.state;
|
_icon = AIcons.state;
|
||||||
break;
|
|
||||||
case _place:
|
case _place:
|
||||||
_icon = AIcons.place;
|
_icon = AIcons.place;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +71,6 @@ class PlaceholderFilter extends CollectionFilter {
|
||||||
case _place:
|
case _place:
|
||||||
return address.place;
|
return address.place;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,6 @@ class QueryFilter extends CollectionFilter {
|
||||||
if (op == opEqual) {
|
if (op == opEqual) {
|
||||||
return (entry) => entry.contentId == valueInt;
|
return (entry) => entry.contentId == valueInt;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case keyContentYear:
|
case keyContentYear:
|
||||||
if (valueInt == null) return null;
|
if (valueInt == null) return null;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
|
@ -128,7 +127,6 @@ class QueryFilter extends CollectionFilter {
|
||||||
case opGreater:
|
case opGreater:
|
||||||
return (entry) => (entry.bestDate?.year ?? 0) > valueInt;
|
return (entry) => (entry.bestDate?.year ?? 0) > valueInt;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case keyContentMonth:
|
case keyContentMonth:
|
||||||
if (valueInt == null) return null;
|
if (valueInt == null) return null;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
|
@ -139,7 +137,6 @@ class QueryFilter extends CollectionFilter {
|
||||||
case opGreater:
|
case opGreater:
|
||||||
return (entry) => (entry.bestDate?.month ?? 0) > valueInt;
|
return (entry) => (entry.bestDate?.month ?? 0) > valueInt;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case keyContentDay:
|
case keyContentDay:
|
||||||
if (valueInt == null) return null;
|
if (valueInt == null) return null;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
|
@ -150,7 +147,6 @@ class QueryFilter extends CollectionFilter {
|
||||||
case opGreater:
|
case opGreater:
|
||||||
return (entry) => (entry.bestDate?.day ?? 0) > valueInt;
|
return (entry) => (entry.bestDate?.day ?? 0) > valueInt;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case keyContentWidth:
|
case keyContentWidth:
|
||||||
if (valueInt == null) return null;
|
if (valueInt == null) return null;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
|
@ -161,7 +157,6 @@ class QueryFilter extends CollectionFilter {
|
||||||
case opGreater:
|
case opGreater:
|
||||||
return (entry) => entry.displaySize.width > valueInt;
|
return (entry) => entry.displaySize.width > valueInt;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case keyContentHeight:
|
case keyContentHeight:
|
||||||
if (valueInt == null) return null;
|
if (valueInt == null) return null;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
|
@ -172,7 +167,6 @@ class QueryFilter extends CollectionFilter {
|
||||||
case opGreater:
|
case opGreater:
|
||||||
return (entry) => entry.displaySize.height > valueInt;
|
return (entry) => entry.displaySize.height > valueInt;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case keyContentSize:
|
case keyContentSize:
|
||||||
match = _fileSizePattern.firstMatch(valueString);
|
match = _fileSizePattern.firstMatch(valueString);
|
||||||
if (match == null) return null;
|
if (match == null) return null;
|
||||||
|
@ -187,13 +181,10 @@ class QueryFilter extends CollectionFilter {
|
||||||
switch (multiplierString) {
|
switch (multiplierString) {
|
||||||
case 'K':
|
case 'K':
|
||||||
bytes *= kilo;
|
bytes *= kilo;
|
||||||
break;
|
|
||||||
case 'M':
|
case 'M':
|
||||||
bytes *= mega;
|
bytes *= mega;
|
||||||
break;
|
|
||||||
case 'G':
|
case 'G':
|
||||||
bytes *= giga;
|
bytes *= giga;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (op) {
|
switch (op) {
|
||||||
|
@ -204,7 +195,6 @@ class QueryFilter extends CollectionFilter {
|
||||||
case opGreater:
|
case opGreater:
|
||||||
return (entry) => (entry.sizeBytes ?? 0) > bytes;
|
return (entry) => (entry.sizeBytes ?? 0) > bytes;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -37,27 +37,21 @@ class TypeFilter extends CollectionFilter {
|
||||||
case _animated:
|
case _animated:
|
||||||
_test = (entry) => entry.isAnimated;
|
_test = (entry) => entry.isAnimated;
|
||||||
_icon = AIcons.animated;
|
_icon = AIcons.animated;
|
||||||
break;
|
|
||||||
case _geotiff:
|
case _geotiff:
|
||||||
_test = (entry) => entry.isGeotiff;
|
_test = (entry) => entry.isGeotiff;
|
||||||
_icon = AIcons.geo;
|
_icon = AIcons.geo;
|
||||||
break;
|
|
||||||
case _motionPhoto:
|
case _motionPhoto:
|
||||||
_test = (entry) => entry.isMotionPhoto;
|
_test = (entry) => entry.isMotionPhoto;
|
||||||
_icon = AIcons.motionPhoto;
|
_icon = AIcons.motionPhoto;
|
||||||
break;
|
|
||||||
case _panorama:
|
case _panorama:
|
||||||
_test = (entry) => entry.isImage && entry.is360;
|
_test = (entry) => entry.isImage && entry.is360;
|
||||||
_icon = AIcons.panorama;
|
_icon = AIcons.panorama;
|
||||||
break;
|
|
||||||
case _raw:
|
case _raw:
|
||||||
_test = (entry) => entry.isRaw;
|
_test = (entry) => entry.isRaw;
|
||||||
_icon = AIcons.raw;
|
_icon = AIcons.raw;
|
||||||
break;
|
|
||||||
case _sphericalVideo:
|
case _sphericalVideo:
|
||||||
_test = (entry) => entry.isVideo && entry.is360;
|
_test = (entry) => entry.isVideo && entry.is360;
|
||||||
_icon = AIcons.sphericalVideo;
|
_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/utils/math_utils.dart';
|
||||||
import 'package:aves_map/aves_map.dart';
|
import 'package:aves_map/aves_map.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/painting.dart';
|
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:proj4dart/proj4dart.dart' as proj4;
|
import 'package:proj4dart/proj4dart.dart' as proj4;
|
||||||
|
|
||||||
|
@ -42,19 +41,145 @@ class GeoTiffInfo extends Equatable {
|
||||||
|
|
||||||
class MappedGeoTiff with MapOverlay {
|
class MappedGeoTiff with MapOverlay {
|
||||||
final AvesEntry entry;
|
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();
|
late final GeoTiffCoordinateConverter _converter;
|
||||||
static final mapServiceHelper = MapServiceHelper(mapServiceTileSize);
|
late final int _mapServiceTileSize;
|
||||||
static final tileImagePaint = Paint();
|
late final MapServiceHelper _mapServiceHelper;
|
||||||
static final tileMissingPaint = Paint()
|
|
||||||
|
static final _tileImagePaint = Paint();
|
||||||
|
static final _tileMissingPaint = Paint()
|
||||||
..style = PaintingStyle.fill
|
..style = PaintingStyle.fill
|
||||||
..color = const Color(0xFF000000);
|
..color = const Color(0xFF000000);
|
||||||
|
|
||||||
MappedGeoTiff({
|
MappedGeoTiff({
|
||||||
required GeoTiffInfo info,
|
required GeoTiffInfo info,
|
||||||
required this.entry,
|
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;
|
pointToLatLng = (_) => null;
|
||||||
epsg3857ToPoint = (_) => 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 width => entry.width;
|
||||||
|
|
||||||
int get height => entry.height;
|
int get height => entry.height;
|
||||||
|
|
||||||
@override
|
|
||||||
bool get canOverlay => center != null;
|
|
||||||
|
|
||||||
LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round()));
|
LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round()));
|
||||||
|
|
||||||
@override
|
|
||||||
LatLng? get topLeft => pointToLatLng(const Point(0, 0));
|
LatLng? get topLeft => pointToLatLng(const Point(0, 0));
|
||||||
|
|
||||||
@override
|
|
||||||
LatLng? get bottomRight => pointToLatLng(Point(width, height));
|
LatLng? get bottomRight => pointToLatLng(Point(width, height));
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,10 +38,8 @@ class NamingPattern {
|
||||||
if (processorOptions != null) {
|
if (processorOptions != null) {
|
||||||
processors.add(DateNamingProcessor(processorOptions.trim()));
|
processors.add(DateNamingProcessor(processorOptions.trim()));
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case NameNamingProcessor.key:
|
case NameNamingProcessor.key:
|
||||||
processors.add(const NameNamingProcessor());
|
processors.add(const NameNamingProcessor());
|
||||||
break;
|
|
||||||
case CounterNamingProcessor.key:
|
case CounterNamingProcessor.key:
|
||||||
int? start, padding;
|
int? start, padding;
|
||||||
_applyProcessorOptions(processorOptions, (key, value) {
|
_applyProcessorOptions(processorOptions, (key, value) {
|
||||||
|
@ -50,18 +48,14 @@ class NamingPattern {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case CounterNamingProcessor.optionStart:
|
case CounterNamingProcessor.optionStart:
|
||||||
start = valueInt;
|
start = valueInt;
|
||||||
break;
|
|
||||||
case CounterNamingProcessor.optionPadding:
|
case CounterNamingProcessor.optionPadding:
|
||||||
padding = valueInt;
|
padding = valueInt;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding));
|
processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding));
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
debugPrint('unsupported naming processor: ${match.group(0)}');
|
debugPrint('unsupported naming processor: ${match.group(0)}');
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
index = end;
|
index = end;
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,13 +15,10 @@ extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case DisplayRefreshRateMode.auto:
|
case DisplayRefreshRateMode.auto:
|
||||||
await FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
|
await FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
|
||||||
break;
|
|
||||||
case DisplayRefreshRateMode.highest:
|
case DisplayRefreshRateMode.highest:
|
||||||
await FlutterDisplayMode.setHighRefreshRate();
|
await FlutterDisplayMode.setHighRefreshRate();
|
||||||
break;
|
|
||||||
case DisplayRefreshRateMode.lowest:
|
case DisplayRefreshRateMode.lowest:
|
||||||
await FlutterDisplayMode.setLowRefreshRate();
|
await FlutterDisplayMode.setLowRefreshRate();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -375,7 +375,7 @@ class Settings extends ChangeNotifier {
|
||||||
if (_locale != null) {
|
if (_locale != null) {
|
||||||
preferredLocales.add(_locale);
|
preferredLocales.add(_locale);
|
||||||
} else {
|
} else {
|
||||||
preferredLocales.addAll(WidgetsBinding.instance.window.locales);
|
preferredLocales.addAll(WidgetsBinding.instance.platformDispatcher.locales);
|
||||||
if (preferredLocales.isEmpty) {
|
if (preferredLocales.isEmpty) {
|
||||||
// the `window` locales may be empty in a window-less service context
|
// the `window` locales may be empty in a window-less service context
|
||||||
preferredLocales.addAll(_systemLocalesFallback);
|
preferredLocales.addAll(_systemLocalesFallback);
|
||||||
|
@ -1022,7 +1022,6 @@ class Settings extends ChangeNotifier {
|
||||||
if (value is num) {
|
if (value is num) {
|
||||||
isRotationLocked = value == 0;
|
isRotationLocked = value == 0;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case platformTransitionAnimationScaleKey:
|
case platformTransitionAnimationScaleKey:
|
||||||
if (value is num) {
|
if (value is num) {
|
||||||
areAnimationsRemoved = value == 0;
|
areAnimationsRemoved = value == 0;
|
||||||
|
@ -1080,7 +1079,6 @@ class Settings extends ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
debugPrint('failed to import key=$key, value=$newValue is not an int');
|
debugPrint('failed to import key=$key, value=$newValue is not an int');
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case subtitleFontSizeKey:
|
case subtitleFontSizeKey:
|
||||||
case infoMapZoomKey:
|
case infoMapZoomKey:
|
||||||
if (newValue is double) {
|
if (newValue is double) {
|
||||||
|
@ -1088,7 +1086,6 @@ class Settings extends ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
debugPrint('failed to import key=$key, value=$newValue is not a double');
|
debugPrint('failed to import key=$key, value=$newValue is not a double');
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case isInstalledAppAccessAllowedKey:
|
case isInstalledAppAccessAllowedKey:
|
||||||
case isErrorReportingAllowedKey:
|
case isErrorReportingAllowedKey:
|
||||||
case enableDynamicColorKey:
|
case enableDynamicColorKey:
|
||||||
|
@ -1144,7 +1141,6 @@ class Settings extends ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
debugPrint('failed to import key=$key, value=$newValue is not a bool');
|
debugPrint('failed to import key=$key, value=$newValue is not a bool');
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case localeKey:
|
case localeKey:
|
||||||
case displayRefreshRateModeKey:
|
case displayRefreshRateModeKey:
|
||||||
case themeBrightnessKey:
|
case themeBrightnessKey:
|
||||||
|
@ -1187,7 +1183,6 @@ class Settings extends ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
debugPrint('failed to import key=$key, value=$newValue is not a string');
|
debugPrint('failed to import key=$key, value=$newValue is not a string');
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case drawerTypeBookmarksKey:
|
case drawerTypeBookmarksKey:
|
||||||
case drawerAlbumBookmarksKey:
|
case drawerAlbumBookmarksKey:
|
||||||
case drawerPageBookmarksKey:
|
case drawerPageBookmarksKey:
|
||||||
|
@ -1203,7 +1198,6 @@ class Settings extends ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
debugPrint('failed to import key=$key, value=$newValue is not a list');
|
debugPrint('failed to import key=$key, value=$newValue is not a list');
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldValue != newValue) {
|
if (oldValue != newValue) {
|
||||||
|
|
|
@ -47,13 +47,10 @@ mixin AlbumMixin on SourceBase {
|
||||||
switch (androidFileUtils.getAlbumType(album)) {
|
switch (androidFileUtils.getAlbumType(album)) {
|
||||||
case AlbumType.regular:
|
case AlbumType.regular:
|
||||||
regularAlbums.add(album);
|
regularAlbums.add(album);
|
||||||
break;
|
|
||||||
case AlbumType.app:
|
case AlbumType.app:
|
||||||
appAlbums.add(album);
|
appAlbums.add(album);
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
specialAlbums.add(album);
|
specialAlbums.add(album);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) => MapEntry(
|
return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) => MapEntry(
|
||||||
|
|
|
@ -68,10 +68,8 @@ class CollectionLens with ChangeNotifier {
|
||||||
case MoveType.move:
|
case MoveType.move:
|
||||||
case MoveType.fromBin:
|
case MoveType.fromBin:
|
||||||
refresh();
|
refresh();
|
||||||
break;
|
|
||||||
case MoveType.toBin:
|
case MoveType.toBin:
|
||||||
_onEntryRemoved(e.entries);
|
_onEntryRemoved(e.entries);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
_subscriptions.add(sourceEvents.on<EntryRefreshedEvent>().listen((e) => refresh()));
|
_subscriptions.add(sourceEvents.on<EntryRefreshedEvent>().listen((e) => refresh()));
|
||||||
|
@ -213,16 +211,12 @@ class CollectionLens with ChangeNotifier {
|
||||||
switch (sortFactor) {
|
switch (sortFactor) {
|
||||||
case EntrySortFactor.date:
|
case EntrySortFactor.date:
|
||||||
_filteredSortedEntries.sort(AvesEntrySort.compareByDate);
|
_filteredSortedEntries.sort(AvesEntrySort.compareByDate);
|
||||||
break;
|
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
_filteredSortedEntries.sort(AvesEntrySort.compareByName);
|
_filteredSortedEntries.sort(AvesEntrySort.compareByName);
|
||||||
break;
|
|
||||||
case EntrySortFactor.rating:
|
case EntrySortFactor.rating:
|
||||||
_filteredSortedEntries.sort(AvesEntrySort.compareByRating);
|
_filteredSortedEntries.sort(AvesEntrySort.compareByRating);
|
||||||
break;
|
|
||||||
case EntrySortFactor.size:
|
case EntrySortFactor.size:
|
||||||
_filteredSortedEntries.sort(AvesEntrySort.compareBySize);
|
_filteredSortedEntries.sort(AvesEntrySort.compareBySize);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (sortReverse) {
|
if (sortReverse) {
|
||||||
_filteredSortedEntries = _filteredSortedEntries.reversed.toList();
|
_filteredSortedEntries = _filteredSortedEntries.reversed.toList();
|
||||||
|
@ -240,33 +234,25 @@ class CollectionLens with ChangeNotifier {
|
||||||
switch (sectionFactor) {
|
switch (sectionFactor) {
|
||||||
case EntryGroupFactor.album:
|
case EntryGroupFactor.album:
|
||||||
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
||||||
break;
|
|
||||||
case EntryGroupFactor.month:
|
case EntryGroupFactor.month:
|
||||||
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
|
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
|
||||||
break;
|
|
||||||
case EntryGroupFactor.day:
|
case EntryGroupFactor.day:
|
||||||
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
|
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
|
||||||
break;
|
|
||||||
case EntryGroupFactor.none:
|
case EntryGroupFactor.none:
|
||||||
sections = Map.fromEntries([
|
sections = Map.fromEntries([
|
||||||
MapEntry(const SectionKey(), _filteredSortedEntries),
|
MapEntry(const SectionKey(), _filteredSortedEntries),
|
||||||
]);
|
]);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
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!);
|
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);
|
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, compare);
|
||||||
break;
|
|
||||||
case EntrySortFactor.rating:
|
case EntrySortFactor.rating:
|
||||||
sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
|
sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
|
||||||
break;
|
|
||||||
case EntrySortFactor.size:
|
case EntrySortFactor.size:
|
||||||
sections = Map.fromEntries([
|
sections = Map.fromEntries([
|
||||||
MapEntry(const SectionKey(), _filteredSortedEntries),
|
MapEntry(const SectionKey(), _filteredSortedEntries),
|
||||||
]);
|
]);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sections = Map.unmodifiable(sections);
|
sections = Map.unmodifiable(sections);
|
||||||
|
|
|
@ -221,18 +221,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'contentId':
|
case 'contentId':
|
||||||
entry.contentId = newValue as int?;
|
entry.contentId = newValue as int?;
|
||||||
break;
|
|
||||||
case 'dateModifiedSecs':
|
case 'dateModifiedSecs':
|
||||||
// `dateModifiedSecs` changes when moving entries to another directory,
|
// `dateModifiedSecs` changes when moving entries to another directory,
|
||||||
// but it does not change when renaming the containing directory
|
// but it does not change when renaming the containing directory
|
||||||
entry.dateModifiedSecs = newValue as int?;
|
entry.dateModifiedSecs = newValue as int?;
|
||||||
break;
|
|
||||||
case 'path':
|
case 'path':
|
||||||
entry.path = newValue as String?;
|
entry.path = newValue as String?;
|
||||||
break;
|
|
||||||
case 'title':
|
case 'title':
|
||||||
entry.sourceTitle = newValue as String?;
|
entry.sourceTitle = newValue as String?;
|
||||||
break;
|
|
||||||
case 'trashed':
|
case 'trashed':
|
||||||
final trashed = newValue as bool;
|
final trashed = newValue as bool;
|
||||||
entry.trashed = trashed;
|
entry.trashed = trashed;
|
||||||
|
@ -243,13 +239,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
||||||
dateMillis: DateTime.now().millisecondsSinceEpoch,
|
dateMillis: DateTime.now().millisecondsSinceEpoch,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
break;
|
|
||||||
case 'uri':
|
case 'uri':
|
||||||
entry.uri = newValue as String;
|
entry.uri = newValue as String;
|
||||||
break;
|
|
||||||
case 'origin':
|
case 'origin':
|
||||||
entry.origin = newValue as int;
|
entry.origin = newValue as int;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (entry.trashed) {
|
if (entry.trashed) {
|
||||||
|
@ -371,16 +364,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
||||||
switch (moveType) {
|
switch (moveType) {
|
||||||
case MoveType.copy:
|
case MoveType.copy:
|
||||||
addEntries(movedEntries);
|
addEntries(movedEntries);
|
||||||
break;
|
|
||||||
case MoveType.move:
|
case MoveType.move:
|
||||||
case MoveType.export:
|
case MoveType.export:
|
||||||
cleanEmptyAlbums(fromAlbums.whereNotNull().toSet());
|
cleanEmptyAlbums(fromAlbums.whereNotNull().toSet());
|
||||||
addDirectories(albums: destinationAlbums);
|
addDirectories(albums: destinationAlbums);
|
||||||
break;
|
|
||||||
case MoveType.toBin:
|
case MoveType.toBin:
|
||||||
case MoveType.fromBin:
|
case MoveType.fromBin:
|
||||||
updateDerivedFilters(movedEntries);
|
updateDerivedFilters(movedEntries);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
invalidateAlbumFilterSummary(directories: fromAlbums);
|
invalidateAlbumFilterSummary(directories: fromAlbums);
|
||||||
_invalidate(entries: movedEntries);
|
_invalidate(entries: movedEntries);
|
||||||
|
|
|
@ -210,38 +210,29 @@ class VideoMetadataFormatter {
|
||||||
case Keys.androidCaptureFramerate:
|
case Keys.androidCaptureFramerate:
|
||||||
final captureFps = double.parse(value);
|
final captureFps = double.parse(value);
|
||||||
save('Capture Frame Rate', '${roundToPrecision(captureFps, decimals: 3).toString()} FPS');
|
save('Capture Frame Rate', '${roundToPrecision(captureFps, decimals: 3).toString()} FPS');
|
||||||
break;
|
|
||||||
case Keys.androidManufacturer:
|
case Keys.androidManufacturer:
|
||||||
save('Android Manufacturer', value);
|
save('Android Manufacturer', value);
|
||||||
break;
|
|
||||||
case Keys.androidModel:
|
case Keys.androidModel:
|
||||||
save('Android Model', value);
|
save('Android Model', value);
|
||||||
break;
|
|
||||||
case Keys.androidVersion:
|
case Keys.androidVersion:
|
||||||
save('Android Version', value);
|
save('Android Version', value);
|
||||||
break;
|
|
||||||
case Keys.bitrate:
|
case Keys.bitrate:
|
||||||
case Keys.bps:
|
case Keys.bps:
|
||||||
save('Bit Rate', _formatMetric(value, 'b/s'));
|
save('Bit Rate', _formatMetric(value, 'b/s'));
|
||||||
break;
|
|
||||||
case Keys.byteCount:
|
case Keys.byteCount:
|
||||||
save('Size', _formatFilesize(value));
|
save('Size', _formatFilesize(value));
|
||||||
break;
|
|
||||||
case Keys.channelLayout:
|
case Keys.channelLayout:
|
||||||
save('Channel Layout', _formatChannelLayout(value));
|
save('Channel Layout', _formatChannelLayout(value));
|
||||||
break;
|
|
||||||
case Keys.codecName:
|
case Keys.codecName:
|
||||||
if (value != 'none') {
|
if (value != 'none') {
|
||||||
save('Format', _formatCodecName(value));
|
save('Format', _formatCodecName(value));
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case Keys.codecPixelFormat:
|
case Keys.codecPixelFormat:
|
||||||
if (streamType == MediaStreamTypes.video) {
|
if (streamType == MediaStreamTypes.video) {
|
||||||
// this is just a short name used by FFmpeg
|
// this is just a short name used by FFmpeg
|
||||||
// user-friendly descriptions for related enums are defined in libavutil/pixfmt.h
|
// user-friendly descriptions for related enums are defined in libavutil/pixfmt.h
|
||||||
save('Pixel Format', (value as String).toUpperCase());
|
save('Pixel Format', (value as String).toUpperCase());
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case Keys.codecProfileId:
|
case Keys.codecProfileId:
|
||||||
{
|
{
|
||||||
final profile = int.tryParse(value);
|
final profile = int.tryParse(value);
|
||||||
|
@ -260,18 +251,14 @@ class VideoMetadataFormatter {
|
||||||
profileString = Hevc.formatProfile(profile, level);
|
profileString = Hevc.formatProfile(profile, level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case Codecs.aac:
|
case Codecs.aac:
|
||||||
profileString = AAC.formatProfile(profile);
|
profileString = AAC.formatProfile(profile);
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
profileString = profile.toString();
|
profileString = profile.toString();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
save('Format Profile', profileString);
|
save('Format Profile', profileString);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case Keys.compatibleBrands:
|
case Keys.compatibleBrands:
|
||||||
final formattedBrands = RegExp(r'.{4}').allMatches(value).map((m) {
|
final formattedBrands = RegExp(r'.{4}').allMatches(value).map((m) {
|
||||||
|
@ -279,52 +266,37 @@ class VideoMetadataFormatter {
|
||||||
return _formatBrand(brand);
|
return _formatBrand(brand);
|
||||||
}).join(', ');
|
}).join(', ');
|
||||||
save('Compatible Brands', formattedBrands);
|
save('Compatible Brands', formattedBrands);
|
||||||
break;
|
|
||||||
case Keys.creationTime:
|
case Keys.creationTime:
|
||||||
save('Creation Time', _formatDate(value));
|
save('Creation Time', _formatDate(value));
|
||||||
break;
|
|
||||||
case Keys.date:
|
case Keys.date:
|
||||||
if (value is String && value != '0') {
|
if (value is String && value != '0') {
|
||||||
final charCount = value.length;
|
final charCount = value.length;
|
||||||
save(charCount == 4 ? 'Year' : 'Date', value);
|
save(charCount == 4 ? 'Year' : 'Date', value);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case Keys.duration:
|
case Keys.duration:
|
||||||
save('Duration', _formatDuration(value));
|
save('Duration', _formatDuration(value));
|
||||||
break;
|
|
||||||
case Keys.durationMicros:
|
case Keys.durationMicros:
|
||||||
if (value != 0) save('Duration', formatPreciseDuration(Duration(microseconds: value)));
|
if (value != 0) save('Duration', formatPreciseDuration(Duration(microseconds: value)));
|
||||||
break;
|
|
||||||
case Keys.fpsDen:
|
case Keys.fpsDen:
|
||||||
save('Frame Rate', '${roundToPrecision(info[Keys.fpsNum] / info[Keys.fpsDen], decimals: 3).toString()} FPS');
|
save('Frame Rate', '${roundToPrecision(info[Keys.fpsNum] / info[Keys.fpsDen], decimals: 3).toString()} FPS');
|
||||||
break;
|
|
||||||
case Keys.frameCount:
|
case Keys.frameCount:
|
||||||
save('Frame Count', value);
|
save('Frame Count', value);
|
||||||
break;
|
|
||||||
case Keys.height:
|
case Keys.height:
|
||||||
save('Height', '$value pixels');
|
save('Height', '$value pixels');
|
||||||
break;
|
|
||||||
case Keys.language:
|
case Keys.language:
|
||||||
if (value != 'und') save('Language', _formatLanguage(value));
|
if (value != 'und') save('Language', _formatLanguage(value));
|
||||||
break;
|
|
||||||
case Keys.location:
|
case Keys.location:
|
||||||
save('Location', _formatLocation(value));
|
save('Location', _formatLocation(value));
|
||||||
break;
|
|
||||||
case Keys.majorBrand:
|
case Keys.majorBrand:
|
||||||
save('Major Brand', _formatBrand(value));
|
save('Major Brand', _formatBrand(value));
|
||||||
break;
|
|
||||||
case Keys.mediaFormat:
|
case Keys.mediaFormat:
|
||||||
save('Format', (value as String).splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName));
|
save('Format', (value as String).splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName));
|
||||||
break;
|
|
||||||
case Keys.mediaType:
|
case Keys.mediaType:
|
||||||
save('Media Type', value);
|
save('Media Type', value);
|
||||||
break;
|
|
||||||
case Keys.minorVersion:
|
case Keys.minorVersion:
|
||||||
if (value != '0') save('Minor Version', value);
|
if (value != '0') save('Minor Version', value);
|
||||||
break;
|
|
||||||
case Keys.quicktimeLocationAccuracyHorizontal:
|
case Keys.quicktimeLocationAccuracyHorizontal:
|
||||||
save('QuickTime Location Horizontal Accuracy', value);
|
save('QuickTime Location Horizontal Accuracy', value);
|
||||||
break;
|
|
||||||
case Keys.quicktimeCreationDate:
|
case Keys.quicktimeCreationDate:
|
||||||
case Keys.quicktimeLocationIso6709:
|
case Keys.quicktimeLocationIso6709:
|
||||||
case Keys.quicktimeMake:
|
case Keys.quicktimeMake:
|
||||||
|
@ -334,37 +306,27 @@ class VideoMetadataFormatter {
|
||||||
break;
|
break;
|
||||||
case Keys.rotate:
|
case Keys.rotate:
|
||||||
save('Rotation', '$value°');
|
save('Rotation', '$value°');
|
||||||
break;
|
|
||||||
case Keys.sampleRate:
|
case Keys.sampleRate:
|
||||||
save('Sample Rate', _formatMetric(value, 'Hz'));
|
save('Sample Rate', _formatMetric(value, 'Hz'));
|
||||||
break;
|
|
||||||
case Keys.sarDen:
|
case Keys.sarDen:
|
||||||
final sarNum = info[Keys.sarNum];
|
final sarNum = info[Keys.sarNum];
|
||||||
final sarDen = info[Keys.sarDen];
|
final sarDen = info[Keys.sarDen];
|
||||||
// skip common square pixels (1:1)
|
// skip common square pixels (1:1)
|
||||||
if (sarNum != sarDen) save('SAR', '$sarNum:$sarDen');
|
if (sarNum != sarDen) save('SAR', '$sarNum:$sarDen');
|
||||||
break;
|
|
||||||
case Keys.sourceOshash:
|
case Keys.sourceOshash:
|
||||||
save('Source OSHash', value);
|
save('Source OSHash', value);
|
||||||
break;
|
|
||||||
case Keys.startMicros:
|
case Keys.startMicros:
|
||||||
if (value != 0) save('Start', formatPreciseDuration(Duration(microseconds: value)));
|
if (value != 0) save('Start', formatPreciseDuration(Duration(microseconds: value)));
|
||||||
break;
|
|
||||||
case Keys.statisticsWritingApp:
|
case Keys.statisticsWritingApp:
|
||||||
save('Stats Writing App', value);
|
save('Stats Writing App', value);
|
||||||
break;
|
|
||||||
case Keys.statisticsWritingDateUtc:
|
case Keys.statisticsWritingDateUtc:
|
||||||
save('Stats Writing Date', _formatDate(value));
|
save('Stats Writing Date', _formatDate(value));
|
||||||
break;
|
|
||||||
case Keys.track:
|
case Keys.track:
|
||||||
if (value != '0') save('Track', value);
|
if (value != '0') save('Track', value);
|
||||||
break;
|
|
||||||
case Keys.width:
|
case Keys.width:
|
||||||
save('Width', '$value pixels');
|
save('Width', '$value pixels');
|
||||||
break;
|
|
||||||
case Keys.xiaomiSlowMoment:
|
case Keys.xiaomiSlowMoment:
|
||||||
save('Xiaomi Slow Moment', value);
|
save('Xiaomi Slow Moment', value);
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
save(key.toSentenceCase(), value.toString());
|
save(key.toSentenceCase(), value.toString());
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,43 +28,30 @@ class H264 {
|
||||||
switch (profileIndex) {
|
switch (profileIndex) {
|
||||||
case profileBaseline:
|
case profileBaseline:
|
||||||
profile = 'Baseline';
|
profile = 'Baseline';
|
||||||
break;
|
|
||||||
case profileConstrainedBaseline:
|
case profileConstrainedBaseline:
|
||||||
profile = 'Constrained Baseline';
|
profile = 'Constrained Baseline';
|
||||||
break;
|
|
||||||
case profileMain:
|
case profileMain:
|
||||||
profile = 'Main';
|
profile = 'Main';
|
||||||
break;
|
|
||||||
case profileExtended:
|
case profileExtended:
|
||||||
profile = 'Extended';
|
profile = 'Extended';
|
||||||
break;
|
|
||||||
case profileHigh:
|
case profileHigh:
|
||||||
profile = 'High';
|
profile = 'High';
|
||||||
break;
|
|
||||||
case profileHigh10:
|
case profileHigh10:
|
||||||
profile = 'High 10';
|
profile = 'High 10';
|
||||||
break;
|
|
||||||
case profileHigh10Intra:
|
case profileHigh10Intra:
|
||||||
profile = 'High 10 Intra';
|
profile = 'High 10 Intra';
|
||||||
break;
|
|
||||||
case profileHigh422:
|
case profileHigh422:
|
||||||
profile = 'High 4:2:2';
|
profile = 'High 4:2:2';
|
||||||
break;
|
|
||||||
case profileHigh422Intra:
|
case profileHigh422Intra:
|
||||||
profile = 'High 4:2:2 Intra';
|
profile = 'High 4:2:2 Intra';
|
||||||
break;
|
|
||||||
case profileHigh444:
|
case profileHigh444:
|
||||||
profile = 'High 4:4:4';
|
profile = 'High 4:4:4';
|
||||||
break;
|
|
||||||
case profileHigh444Predictive:
|
case profileHigh444Predictive:
|
||||||
profile = 'High 4:4:4 Predictive';
|
profile = 'High 4:4:4 Predictive';
|
||||||
break;
|
|
||||||
case profileHigh444Intra:
|
case profileHigh444Intra:
|
||||||
profile = 'High 4:4:4 Intra';
|
profile = 'High 4:4:4 Intra';
|
||||||
break;
|
|
||||||
case profileCAVLC444:
|
case profileCAVLC444:
|
||||||
profile = 'CAVLC 4:4:4';
|
profile = 'CAVLC 4:4:4';
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
return '$profileIndex';
|
return '$profileIndex';
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,16 +9,12 @@ class Hevc {
|
||||||
switch (profileIndex) {
|
switch (profileIndex) {
|
||||||
case profileMain:
|
case profileMain:
|
||||||
profile = 'Main';
|
profile = 'Main';
|
||||||
break;
|
|
||||||
case profileMain10:
|
case profileMain10:
|
||||||
profile = 'Main 10';
|
profile = 'Main 10';
|
||||||
break;
|
|
||||||
case profileMainStillPicture:
|
case profileMainStillPicture:
|
||||||
profile = 'Main Still Picture';
|
profile = 'Main Still Picture';
|
||||||
break;
|
|
||||||
case profileRExt:
|
case profileRExt:
|
||||||
profile = 'Format Range';
|
profile = 'Format Range';
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
return '$profileIndex';
|
return '$profileIndex';
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,4 @@ class PointsOfInterest {
|
||||||
LatLng(37.637861, 21.63),
|
LatLng(37.637861, 21.63),
|
||||||
LatLng(37.949722, 27.363889),
|
LatLng(37.949722, 27.363889),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,11 +145,9 @@ class Analyzer {
|
||||||
case AnalyzerState.stopping:
|
case AnalyzerState.stopping:
|
||||||
await _stopPlatformService();
|
await _stopPlatformService();
|
||||||
_serviceStateNotifier.value = AnalyzerState.stopped;
|
_serviceStateNotifier.value = AnalyzerState.stopped;
|
||||||
break;
|
|
||||||
case AnalyzerState.stopped:
|
case AnalyzerState.stopped:
|
||||||
_controller?.stopSignal.value = true;
|
_controller?.stopSignal.value = true;
|
||||||
_stopUpdateTimer();
|
_stopUpdateTimer();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,25 +96,19 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'play':
|
case 'play':
|
||||||
event = const MediaCommandEvent(MediaCommand.play);
|
event = const MediaCommandEvent(MediaCommand.play);
|
||||||
break;
|
|
||||||
case 'pause':
|
case 'pause':
|
||||||
event = const MediaCommandEvent(MediaCommand.pause);
|
event = const MediaCommandEvent(MediaCommand.pause);
|
||||||
break;
|
|
||||||
case 'skip_to_next':
|
case 'skip_to_next':
|
||||||
event = const MediaCommandEvent(MediaCommand.skipToNext);
|
event = const MediaCommandEvent(MediaCommand.skipToNext);
|
||||||
break;
|
|
||||||
case 'skip_to_previous':
|
case 'skip_to_previous':
|
||||||
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
|
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
|
||||||
break;
|
|
||||||
case 'stop':
|
case 'stop':
|
||||||
event = const MediaCommandEvent(MediaCommand.stop);
|
event = const MediaCommandEvent(MediaCommand.stop);
|
||||||
break;
|
|
||||||
case 'seek':
|
case 'seek':
|
||||||
final position = fields['position'] as int?;
|
final position = fields['position'] as int?;
|
||||||
if (position != null) {
|
if (position != null) {
|
||||||
event = MediaSeekCommandEvent(MediaCommand.stop, position: position);
|
event = MediaSeekCommandEvent(MediaCommand.stop, position: position);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
_streamController.add(event);
|
_streamController.add(event);
|
||||||
|
|
|
@ -80,7 +80,7 @@ class SvgMetadataService {
|
||||||
final docDir = Map.fromEntries([
|
final docDir = Map.fromEntries([
|
||||||
...root.attributes.where((a) => _attributes.contains(a.name.qualified)).map((a) => MapEntry(formatKey(a.name.qualified), a.value)),
|
...root.attributes.where((a) => _attributes.contains(a.name.qualified)).map((a) => MapEntry(formatKey(a.name.qualified), a.value)),
|
||||||
..._textElements.map((name) {
|
..._textElements.map((name) {
|
||||||
final value = root.getElement(name)?.text;
|
final value = root.getElement(name)?.innerText;
|
||||||
return value != null ? MapEntry(formatKey(name), value) : null;
|
return value != null ? MapEntry(formatKey(name), value) : null;
|
||||||
}).whereNotNull(),
|
}).whereNotNull(),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -74,15 +74,12 @@ class PlatformWindowService implements WindowService {
|
||||||
case Orientation.landscape:
|
case Orientation.landscape:
|
||||||
// SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
// SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
orientationCode = 6;
|
orientationCode = 6;
|
||||||
break;
|
|
||||||
case Orientation.portrait:
|
case Orientation.portrait:
|
||||||
// SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
// SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
orientationCode = 7;
|
orientationCode = 7;
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
// SCREEN_ORIENTATION_UNSPECIFIED
|
// SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
orientationCode = -1;
|
orientationCode = -1;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await _platform.invokeMethod('requestOrientation', <String, dynamic>{
|
await _platform.invokeMethod('requestOrientation', <String, dynamic>{
|
||||||
|
|
|
@ -4,4 +4,4 @@ class AText {
|
||||||
static const separator = ' ${UniChars.bullet} ';
|
static const separator = ' ${UniChars.bullet} ';
|
||||||
static const resolutionSeparator = ' ${UniChars.multiplicationSign} ';
|
static const resolutionSeparator = ' ${UniChars.multiplicationSign} ';
|
||||||
static const valueNotAvailable = UniChars.emDash;
|
static const valueNotAvailable = UniChars.emDash;
|
||||||
}
|
}
|
||||||
|
|
|
@ -274,11 +274,9 @@ class DiffMatchPatch {
|
||||||
case Operation.insert:
|
case Operation.insert:
|
||||||
count_insert++;
|
count_insert++;
|
||||||
text_insert.write(diffs[pointer].text);
|
text_insert.write(diffs[pointer].text);
|
||||||
break;
|
|
||||||
case Operation.delete:
|
case Operation.delete:
|
||||||
count_delete++;
|
count_delete++;
|
||||||
text_delete.write(diffs[pointer].text);
|
text_delete.write(diffs[pointer].text);
|
||||||
break;
|
|
||||||
case Operation.equal:
|
case Operation.equal:
|
||||||
// Upon reaching an equality, check for prior redundancies.
|
// Upon reaching an equality, check for prior redundancies.
|
||||||
if (count_delete >= 1 && count_insert >= 1) {
|
if (count_delete >= 1 && count_insert >= 1) {
|
||||||
|
@ -295,7 +293,6 @@ class DiffMatchPatch {
|
||||||
count_delete = 0;
|
count_delete = 0;
|
||||||
text_delete.clear();
|
text_delete.clear();
|
||||||
text_insert.clear();
|
text_insert.clear();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
pointer++;
|
pointer++;
|
||||||
}
|
}
|
||||||
|
@ -1013,12 +1010,10 @@ class DiffMatchPatch {
|
||||||
count_insert++;
|
count_insert++;
|
||||||
text_insert += diffs[pointer].text;
|
text_insert += diffs[pointer].text;
|
||||||
pointer++;
|
pointer++;
|
||||||
break;
|
|
||||||
case Operation.delete:
|
case Operation.delete:
|
||||||
count_delete++;
|
count_delete++;
|
||||||
text_delete += diffs[pointer].text;
|
text_delete += diffs[pointer].text;
|
||||||
pointer++;
|
pointer++;
|
||||||
break;
|
|
||||||
case Operation.equal:
|
case Operation.equal:
|
||||||
// Upon reaching an equality, check for prior redundancies.
|
// Upon reaching an equality, check for prior redundancies.
|
||||||
if (count_delete + count_insert > 1) {
|
if (count_delete + count_insert > 1) {
|
||||||
|
@ -1068,7 +1063,6 @@ class DiffMatchPatch {
|
||||||
count_delete = 0;
|
count_delete = 0;
|
||||||
text_delete = '';
|
text_delete = '';
|
||||||
text_insert = '';
|
text_insert = '';
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (diffs.last.text.isEmpty) {
|
if (diffs.last.text.isEmpty) {
|
||||||
|
@ -1155,17 +1149,14 @@ class DiffMatchPatch {
|
||||||
html.write('<ins style="background:#e6ffe6;">');
|
html.write('<ins style="background:#e6ffe6;">');
|
||||||
html.write(text);
|
html.write(text);
|
||||||
html.write('</ins>');
|
html.write('</ins>');
|
||||||
break;
|
|
||||||
case Operation.delete:
|
case Operation.delete:
|
||||||
html.write('<del style="background:#ffe6e6;">');
|
html.write('<del style="background:#ffe6e6;">');
|
||||||
html.write(text);
|
html.write(text);
|
||||||
html.write('</del>');
|
html.write('</del>');
|
||||||
break;
|
|
||||||
case Operation.equal:
|
case Operation.equal:
|
||||||
html.write('<span>');
|
html.write('<span>');
|
||||||
html.write(text);
|
html.write(text);
|
||||||
html.write('</span>');
|
html.write('</span>');
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html.toString();
|
return html.toString();
|
||||||
|
@ -1209,16 +1200,13 @@ class DiffMatchPatch {
|
||||||
switch (aDiff.operation) {
|
switch (aDiff.operation) {
|
||||||
case Operation.insert:
|
case Operation.insert:
|
||||||
insertions += aDiff.text.length;
|
insertions += aDiff.text.length;
|
||||||
break;
|
|
||||||
case Operation.delete:
|
case Operation.delete:
|
||||||
deletions += aDiff.text.length;
|
deletions += aDiff.text.length;
|
||||||
break;
|
|
||||||
case Operation.equal:
|
case Operation.equal:
|
||||||
// A deletion and an insertion is one substitution.
|
// A deletion and an insertion is one substitution.
|
||||||
levenshtein += max(insertions, deletions);
|
levenshtein += max(insertions, deletions);
|
||||||
insertions = 0;
|
insertions = 0;
|
||||||
deletions = 0;
|
deletions = 0;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
levenshtein += max(insertions, deletions);
|
levenshtein += max(insertions, deletions);
|
||||||
|
@ -1239,17 +1227,14 @@ class DiffMatchPatch {
|
||||||
text.write('+');
|
text.write('+');
|
||||||
text.write(Uri.encodeFull(aDiff.text));
|
text.write(Uri.encodeFull(aDiff.text));
|
||||||
text.write('\t');
|
text.write('\t');
|
||||||
break;
|
|
||||||
case Operation.delete:
|
case Operation.delete:
|
||||||
text.write('-');
|
text.write('-');
|
||||||
text.write(aDiff.text.length);
|
text.write(aDiff.text.length);
|
||||||
text.write('\t');
|
text.write('\t');
|
||||||
break;
|
|
||||||
case Operation.equal:
|
case Operation.equal:
|
||||||
text.write('=');
|
text.write('=');
|
||||||
text.write(aDiff.text.length);
|
text.write(aDiff.text.length);
|
||||||
text.write('\t');
|
text.write('\t');
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String delta = text.toString();
|
String delta = text.toString();
|
||||||
|
@ -1289,7 +1274,6 @@ class DiffMatchPatch {
|
||||||
throw ArgumentError('Illegal escape in diff_fromDelta: $param');
|
throw ArgumentError('Illegal escape in diff_fromDelta: $param');
|
||||||
}
|
}
|
||||||
diffs.add(Diff(Operation.insert, param));
|
diffs.add(Diff(Operation.insert, param));
|
||||||
break;
|
|
||||||
case '-':
|
case '-':
|
||||||
// Fall through.
|
// Fall through.
|
||||||
case '=':
|
case '=':
|
||||||
|
@ -1314,7 +1298,6 @@ class DiffMatchPatch {
|
||||||
} else {
|
} else {
|
||||||
diffs.add(Diff(Operation.delete, text));
|
diffs.add(Diff(Operation.delete, text));
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
// Anything else is an error.
|
// Anything else is an error.
|
||||||
throw ArgumentError('Invalid diff operation in diff_fromDelta: ${token[0]}');
|
throw ArgumentError('Invalid diff operation in diff_fromDelta: ${token[0]}');
|
||||||
|
|
|
@ -26,7 +26,7 @@ class XMP {
|
||||||
static const nsXmp = XmpNamespaces.xmp;
|
static const nsXmp = XmpNamespaces.xmp;
|
||||||
|
|
||||||
// for `rdf:Description` node only
|
// 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
|
// for `rdf:Description` node only
|
||||||
static bool _hasMeaningfulAttributes(XmlNode description) {
|
static bool _hasMeaningfulAttributes(XmlNode description) {
|
||||||
|
|
|
@ -79,10 +79,8 @@ Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
|
||||||
switch (settings.getWidgetDisplayedItem(widgetId)) {
|
switch (settings.getWidgetDisplayedItem(widgetId)) {
|
||||||
case WidgetDisplayedItem.random:
|
case WidgetDisplayedItem.random:
|
||||||
entries.shuffle();
|
entries.shuffle();
|
||||||
break;
|
|
||||||
case WidgetDisplayedItem.mostRecent:
|
case WidgetDisplayedItem.mostRecent:
|
||||||
entries.sort(AvesEntrySort.compareByDate);
|
entries.sort(AvesEntrySort.compareByDate);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
final entry = entries.firstOrNull;
|
final entry = entries.firstOrNull;
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
|
|
|
@ -94,11 +94,12 @@ class _AppReferenceState extends State<AppReference> {
|
||||||
return FutureBuilder<PackageInfo>(
|
return FutureBuilder<PackageInfo>(
|
||||||
future: _packageInfoLoader,
|
future: _packageInfoLoader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
AvesLogo(
|
AvesLogo(
|
||||||
size: _appTitleStyle.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3,
|
size: _appTitleStyle.fontSize! * textScaleFactor * 1.3,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
|
|
|
@ -157,7 +157,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
||||||
'Device: ${androidInfo.manufacturer} ${androidInfo.model}',
|
'Device: ${androidInfo.manufacturer} ${androidInfo.model}',
|
||||||
'Geocoder: ${device.hasGeocoder ? 'ready' : 'not available'}',
|
'Geocoder: ${device.hasGeocoder ? 'ready' : 'not available'}',
|
||||||
'Mobile services: ${mobileServices.isServiceAvailable ? '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 volumes: ${storageVolumes.map((v) => v.path).join(', ')}',
|
||||||
'Storage grants: ${storageGrants.join(', ')}',
|
'Storage grants: ${storageGrants.join(', ')}',
|
||||||
'Error reporting: ${settings.isErrorReportingAllowed}',
|
'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/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/intents.dart';
|
import 'package:aves/widgets/common/behaviour/intents.dart';
|
||||||
import 'package:flutter/foundation.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/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
@ -209,10 +211,21 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
|
||||||
bool _loaded = false;
|
bool _loaded = false;
|
||||||
|
|
||||||
Future<void> _initLicenses() async {
|
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) {
|
for (final LicenseEntry license in widget.licenseEntries) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
assert(() {
|
||||||
|
Timeline.timeSync('_initLicenses()', () {}, flow: Flow.step(debugFlowId));
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
final List<LicenseParagraph> paragraphs = await SchedulerBinding.instance.scheduleTask<List<LicenseParagraph>>(
|
final List<LicenseParagraph> paragraphs = await SchedulerBinding.instance.scheduleTask<List<LicenseParagraph>>(
|
||||||
license.paragraphs.toList,
|
license.paragraphs.toList,
|
||||||
Priority.animation,
|
Priority.animation,
|
||||||
|
@ -237,6 +250,7 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
|
assert(paragraph.indent >= 0);
|
||||||
_licenses.add(Padding(
|
_licenses.add(Padding(
|
||||||
padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent),
|
padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent),
|
||||||
child: Text(paragraph.text),
|
child: Text(paragraph.text),
|
||||||
|
@ -248,16 +262,21 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
});
|
});
|
||||||
|
assert(() {
|
||||||
|
Timeline.timeSync('Build scheduled', () {}, flow: Flow.end(debugFlowId));
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMaterialLocalizations(context));
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
final String title = widget.packageName;
|
final String title = widget.packageName;
|
||||||
final String subtitle = localizations.licensesPackageDetailText(widget.licenseEntries.length);
|
final String subtitle = localizations.licensesPackageDetailText(widget.licenseEntries.length);
|
||||||
const double pad = 24;
|
final double pad = _getGutterSize(context);
|
||||||
const EdgeInsets padding = EdgeInsets.only(left: pad, right: pad, bottom: pad);
|
final EdgeInsets padding = EdgeInsets.only(left: pad, right: pad, bottom: pad);
|
||||||
final List<Widget> listWidgets = <Widget>[
|
final List<Widget> listWidgets = <Widget>[
|
||||||
..._licenses,
|
..._licenses,
|
||||||
if (!_loaded)
|
if (!_loaded)
|
||||||
|
@ -274,9 +293,11 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
|
||||||
page = Scaffold(
|
page = Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _PackageLicensePageTitle(
|
title: _PackageLicensePageTitle(
|
||||||
title,
|
title: title,
|
||||||
subtitle,
|
subtitle: subtitle,
|
||||||
theme.primaryTextTheme,
|
theme: theme.useMaterial3 ? theme.textTheme : theme.primaryTextTheme,
|
||||||
|
titleTextStyle: theme.appBarTheme.titleTextStyle,
|
||||||
|
foregroundColor: theme.appBarTheme.foregroundColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
|
@ -292,7 +313,11 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
|
||||||
// A Scrollbar is built-in below.
|
// A Scrollbar is built-in below.
|
||||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
child: Scrollbar(
|
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,
|
automaticallyImplyLeading: false,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
backgroundColor: theme.cardColor,
|
backgroundColor: theme.cardColor,
|
||||||
title: _PackageLicensePageTitle(title, subtitle, theme.textTheme),
|
title: _PackageLicensePageTitle(
|
||||||
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
|
theme: theme.textTheme,
|
||||||
|
titleTextStyle: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
|
@ -334,27 +364,36 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PackageLicensePageTitle extends StatelessWidget {
|
class _PackageLicensePageTitle extends StatelessWidget {
|
||||||
const _PackageLicensePageTitle(
|
const _PackageLicensePageTitle({
|
||||||
this.title,
|
required this.title,
|
||||||
this.subtitle,
|
required this.subtitle,
|
||||||
this.theme,
|
required this.theme,
|
||||||
);
|
this.titleTextStyle,
|
||||||
|
this.foregroundColor,
|
||||||
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final TextTheme theme;
|
final TextTheme theme;
|
||||||
|
final TextStyle? titleTextStyle;
|
||||||
|
final Color? foregroundColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Color? color = Theme.of(context).appBarTheme.foregroundColor;
|
final TextStyle? effectiveTitleTextStyle = titleTextStyle ?? theme.titleLarge;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(title, style: theme.titleLarge?.copyWith(color: color)),
|
Text(title, style: effectiveTitleTextStyle?.copyWith(color: foregroundColor)),
|
||||||
Text(subtitle, style: theme.titleSmall?.copyWith(color: color)),
|
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:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/app_flavor.dart';
|
import 'package:aves/app_flavor.dart';
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
|
@ -180,8 +179,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
super.initState();
|
super.initState();
|
||||||
EquatableConfig.stringify = true;
|
EquatableConfig.stringify = true;
|
||||||
_appSetup = _setup();
|
_appSetup = _setup();
|
||||||
// remember screen size to use it later, when `context` and `window` are no longer reliable
|
|
||||||
_screenSize = _getScreenSize();
|
|
||||||
_shouldUseBoldFontLoader = AccessibilityService.shouldUseBoldFont();
|
_shouldUseBoldFontLoader = AccessibilityService.shouldUseBoldFont();
|
||||||
_dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette();
|
_dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette();
|
||||||
_subscriptions.add(_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChanged(event as String?)));
|
_subscriptions.add(_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChanged(event as String?)));
|
||||||
|
@ -206,6 +203,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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`
|
// place the settings provider above `MaterialApp`
|
||||||
// so it can be used during navigation transitions
|
// so it can be used during navigation transitions
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
|
@ -266,38 +266,31 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
// KEYCODE_ENTER, KEYCODE_BUTTON_A, KEYCODE_NUMPAD_ENTER
|
// KEYCODE_ENTER, KEYCODE_BUTTON_A, KEYCODE_NUMPAD_ENTER
|
||||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||||
},
|
},
|
||||||
child: MediaQuery.fromWindow(
|
child: MediaQuery(
|
||||||
child: Builder(
|
data: MediaQuery.of(context).copyWith(
|
||||||
builder: (context) {
|
// disable accessible navigation, as it impacts snack bar action timer
|
||||||
return MediaQuery(
|
// for all users of apps registered as accessibility services,
|
||||||
data: MediaQuery.of(context).copyWith(
|
// even though they are not for accessibility purposes (like TalkBack is)
|
||||||
// disable accessible navigation, as it impacts snack bar action timer
|
accessibleNavigation: false,
|
||||||
// for all users of apps registered as accessibility services,
|
),
|
||||||
// even though they are not for accessibility purposes (like TalkBack is)
|
child: MaterialApp(
|
||||||
accessibleNavigation: false,
|
navigatorKey: _navigatorKey,
|
||||||
),
|
home: home,
|
||||||
child: MaterialApp(
|
navigatorObservers: _navigatorObservers,
|
||||||
navigatorKey: _navigatorKey,
|
builder: (context, child) => _decorateAppChild(
|
||||||
home: home,
|
context: context,
|
||||||
navigatorObservers: _navigatorObservers,
|
initialized: initialized,
|
||||||
builder: (context, child) => _decorateAppChild(
|
child: child,
|
||||||
context: context,
|
),
|
||||||
initialized: initialized,
|
onGenerateTitle: (context) => context.l10n.appName,
|
||||||
child: child,
|
theme: lightTheme,
|
||||||
),
|
darkTheme: darkTheme,
|
||||||
onGenerateTitle: (context) => context.l10n.appName,
|
themeMode: themeBrightness.appThemeMode,
|
||||||
theme: lightTheme,
|
locale: settingsLocale,
|
||||||
darkTheme: darkTheme,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
themeMode: themeBrightness.appThemeMode,
|
supportedLocales: AvesApp.supportedLocales,
|
||||||
locale: settingsLocale,
|
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
scrollBehavior: StretchMaterialScrollBehavior(),
|
||||||
supportedLocales: AvesApp.supportedLocales,
|
|
||||||
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
|
|
||||||
scrollBehavior: StretchMaterialScrollBehavior(),
|
|
||||||
useInheritedMediaQuery: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -390,7 +383,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
case AppMode.pickSingleMediaExternal:
|
case AppMode.pickSingleMediaExternal:
|
||||||
case AppMode.pickMultipleMediaExternal:
|
case AppMode.pickMultipleMediaExternal:
|
||||||
_saveTopEntries();
|
_saveTopEntries();
|
||||||
break;
|
|
||||||
case AppMode.pickCollectionFiltersExternal:
|
case AppMode.pickCollectionFiltersExternal:
|
||||||
case AppMode.pickMediaInternal:
|
case AppMode.pickMediaInternal:
|
||||||
case AppMode.pickFilterInternal:
|
case AppMode.pickFilterInternal:
|
||||||
|
@ -400,10 +392,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
case AppMode.view:
|
case AppMode.view:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
RecentlyAddedFilter.updateNow();
|
RecentlyAddedFilter.updateNow();
|
||||||
break;
|
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
case AppLifecycleState.detached:
|
case AppLifecycleState.detached:
|
||||||
break;
|
break;
|
||||||
|
@ -421,9 +411,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
|
|
||||||
Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
|
Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
|
||||||
|
|
||||||
Size? _getScreenSize() {
|
Size? _getScreenSize(BuildContext context) {
|
||||||
final physicalSize = window.physicalSize;
|
final view = View.of(context);
|
||||||
final ratio = window.devicePixelRatio;
|
final physicalSize = view.physicalSize;
|
||||||
|
final ratio = view.devicePixelRatio;
|
||||||
return physicalSize > Size.zero && ratio > 0 ? physicalSize / ratio : null;
|
return physicalSize > Size.zero && ratio > 0 ? physicalSize / ratio : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -431,7 +422,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
void _saveTopEntries() {
|
void _saveTopEntries() {
|
||||||
if (!settings.initialized) return;
|
if (!settings.initialized) return;
|
||||||
|
|
||||||
final screenSize = _screenSize ?? _getScreenSize();
|
final screenSize = _screenSize;
|
||||||
if (screenSize == null) return;
|
if (screenSize == null) return;
|
||||||
|
|
||||||
var tileExtent = settings.getTileExtent(CollectionPage.routeName);
|
var tileExtent = settings.getTileExtent(CollectionPage.routeName);
|
||||||
|
@ -525,10 +516,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
case MaxBrightness.never:
|
case MaxBrightness.never:
|
||||||
case MaxBrightness.viewerOnly:
|
case MaxBrightness.viewerOnly:
|
||||||
ScreenBrightness().resetScreenBrightness();
|
ScreenBrightness().resetScreenBrightness();
|
||||||
break;
|
|
||||||
case MaxBrightness.always:
|
case MaxBrightness.always:
|
||||||
ScreenBrightness().setScreenBrightness(1);
|
ScreenBrightness().setScreenBrightness(1);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,7 +575,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
: 'debug',
|
: 'debug',
|
||||||
'has_mobile_services': mobileServices.isServiceAvailable,
|
'has_mobile_services': mobileServices.isServiceAvailable,
|
||||||
'is_television': device.isTelevision,
|
'is_television': device.isTelevision,
|
||||||
'locales': WidgetsBinding.instance.window.locales.join(', '),
|
'locales': WidgetsBinding.instance.platformDispatcher.locales.join(', '),
|
||||||
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
|
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
|
||||||
});
|
});
|
||||||
await reportService.log('Launch');
|
await reportService.log('Launch');
|
||||||
|
|
|
@ -224,7 +224,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
}
|
}
|
||||||
|
|
||||||
double get appBarContentHeight {
|
double get appBarContentHeight {
|
||||||
final textScaleFactor = context.read<MediaQueryData>().textScaleFactor;
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
double height = kToolbarHeight * textScaleFactor;
|
double height = kToolbarHeight * textScaleFactor;
|
||||||
if (settings.useTvLayout) {
|
if (settings.useTvLayout) {
|
||||||
height += CaptionedButton.getTelevisionButtonHeight(context);
|
height += CaptionedButton.getTelevisionButtonHeight(context);
|
||||||
|
@ -511,16 +511,13 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
queryEnabled: context.read<Query>().enabled,
|
queryEnabled: context.read<Query>().enabled,
|
||||||
isMenuItem: true,
|
isMenuItem: true,
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case EntrySetAction.toggleFavourite:
|
case EntrySetAction.toggleFavourite:
|
||||||
child = FavouriteToggler(
|
child = FavouriteToggler(
|
||||||
entries: _getExpandedSelectedItems(selection),
|
entries: _getExpandedSelectedItems(selection),
|
||||||
isMenuItem: true,
|
isMenuItem: true,
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
child = MenuRow(text: action.getText(context), icon: action.getIcon());
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return PopupMenuItem(
|
return PopupMenuItem(
|
||||||
key: _getActionKey(action),
|
key: _getActionKey(action),
|
||||||
|
@ -598,7 +595,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus();
|
void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus();
|
||||||
|
|
||||||
void _updateStatusBarHeight() {
|
void _updateStatusBarHeight() {
|
||||||
_statusBarHeight = context.read<MediaQueryData>().padding.top;
|
_statusBarHeight = MediaQuery.paddingOf(context).top;
|
||||||
_updateAppBarHeight();
|
_updateAppBarHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,16 +608,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
// general
|
// general
|
||||||
case EntrySetAction.configureView:
|
case EntrySetAction.configureView:
|
||||||
await _configureView();
|
await _configureView();
|
||||||
break;
|
|
||||||
case EntrySetAction.select:
|
case EntrySetAction.select:
|
||||||
context.read<Selection<AvesEntry>>().select();
|
context.read<Selection<AvesEntry>>().select();
|
||||||
break;
|
|
||||||
case EntrySetAction.selectAll:
|
case EntrySetAction.selectAll:
|
||||||
context.read<Selection<AvesEntry>>().addToSelection(collection.sortedEntries);
|
context.read<Selection<AvesEntry>>().addToSelection(collection.sortedEntries);
|
||||||
break;
|
|
||||||
case EntrySetAction.selectNone:
|
case EntrySetAction.selectNone:
|
||||||
context.read<Selection<AvesEntry>>().clearSelection();
|
context.read<Selection<AvesEntry>>().clearSelection();
|
||||||
break;
|
|
||||||
// browsing
|
// browsing
|
||||||
case EntrySetAction.searchCollection:
|
case EntrySetAction.searchCollection:
|
||||||
case EntrySetAction.toggleTitleSearch:
|
case EntrySetAction.toggleTitleSearch:
|
||||||
|
@ -650,7 +643,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
case EntrySetAction.editTags:
|
case EntrySetAction.editTags:
|
||||||
case EntrySetAction.removeMetadata:
|
case EntrySetAction.removeMetadata:
|
||||||
_actionDelegate.onActionSelected(context, action);
|
_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/widgets/viewer/entry_viewer_page.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
@ -374,6 +373,8 @@ class _CollectionScaler extends StatelessWidget {
|
||||||
final tileSpacing = metrics.item1;
|
final tileSpacing = metrics.item1;
|
||||||
final horizontalPadding = metrics.item2;
|
final horizontalPadding = metrics.item2;
|
||||||
final brightness = Theme.of(context).brightness;
|
final brightness = Theme.of(context).brightness;
|
||||||
|
final borderColor = DecoratedThumbnail.borderColor;
|
||||||
|
final borderWidth = DecoratedThumbnail.borderWidth(context);
|
||||||
return GridScaleGestureDetector<AvesEntry>(
|
return GridScaleGestureDetector<AvesEntry>(
|
||||||
scrollableKey: scrollableKey,
|
scrollableKey: scrollableKey,
|
||||||
tileLayout: tileLayout,
|
tileLayout: tileLayout,
|
||||||
|
@ -385,9 +386,9 @@ class _CollectionScaler extends StatelessWidget {
|
||||||
tileSize: tileSize,
|
tileSize: tileSize,
|
||||||
spacing: tileSpacing,
|
spacing: tileSpacing,
|
||||||
horizontalPadding: horizontalPadding,
|
horizontalPadding: horizontalPadding,
|
||||||
borderWidth: DecoratedThumbnail.borderWidth,
|
borderWidth: borderWidth,
|
||||||
borderRadius: Radius.zero,
|
borderRadius: Radius.zero,
|
||||||
color: DecoratedThumbnail.borderColor,
|
color: borderColor,
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -404,8 +405,8 @@ class _CollectionScaler extends StatelessWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9),
|
color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: DecoratedThumbnail.borderColor,
|
color: borderColor,
|
||||||
width: DecoratedThumbnail.borderWidth,
|
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
|
physics: collection.isEmpty
|
||||||
? const NeverScrollableScrollPhysics()
|
? const NeverScrollableScrollPhysics()
|
||||||
: SloppyScrollPhysics(
|
: SloppyScrollPhysics(
|
||||||
gestureSettings: context.select<MediaQueryData, DeviceGestureSettings>((mq) => mq.gestureSettings),
|
gestureSettings: MediaQuery.gestureSettingsOf(context),
|
||||||
parent: const AlwaysScrollableScrollPhysics(),
|
parent: const AlwaysScrollableScrollPhysics(),
|
||||||
),
|
),
|
||||||
cacheExtent: context.select<TileExtentController, double>((controller) => controller.effectiveExtentMax),
|
cacheExtent: context.select<TileExtentController, double>((controller) => controller.effectiveExtentMax),
|
||||||
|
@ -677,7 +677,6 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
|
||||||
switch (collection.sectionFactor) {
|
switch (collection.sectionFactor) {
|
||||||
case EntryGroupFactor.album:
|
case EntryGroupFactor.album:
|
||||||
addAlbums(collection, sectionLayouts, crumbs);
|
addAlbums(collection, sectionLayouts, crumbs);
|
||||||
break;
|
|
||||||
case EntryGroupFactor.month:
|
case EntryGroupFactor.month:
|
||||||
case EntryGroupFactor.day:
|
case EntryGroupFactor.day:
|
||||||
final firstKey = sectionLayouts.first.sectionKey;
|
final firstKey = sectionLayouts.first.sectionKey;
|
||||||
|
@ -701,14 +700,11 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case EntryGroupFactor.none:
|
case EntryGroupFactor.none:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
addAlbums(collection, sectionLayouts, crumbs);
|
addAlbums(collection, sectionLayouts, crumbs);
|
||||||
break;
|
|
||||||
case EntrySortFactor.rating:
|
case EntrySortFactor.rating:
|
||||||
case EntrySortFactor.size:
|
case EntrySortFactor.size:
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -173,79 +173,55 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
// browsing
|
// browsing
|
||||||
case EntrySetAction.searchCollection:
|
case EntrySetAction.searchCollection:
|
||||||
_goToSearch(context);
|
_goToSearch(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.toggleTitleSearch:
|
case EntrySetAction.toggleTitleSearch:
|
||||||
context.read<Query>().toggle();
|
context.read<Query>().toggle();
|
||||||
break;
|
|
||||||
case EntrySetAction.addShortcut:
|
case EntrySetAction.addShortcut:
|
||||||
_addShortcut(context);
|
_addShortcut(context);
|
||||||
break;
|
|
||||||
// browsing or selecting
|
// browsing or selecting
|
||||||
case EntrySetAction.map:
|
case EntrySetAction.map:
|
||||||
_goToMap(context);
|
_goToMap(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.slideshow:
|
case EntrySetAction.slideshow:
|
||||||
_goToSlideshow(context);
|
_goToSlideshow(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.stats:
|
case EntrySetAction.stats:
|
||||||
_goToStats(context);
|
_goToStats(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.rescan:
|
case EntrySetAction.rescan:
|
||||||
_rescan(context);
|
_rescan(context);
|
||||||
break;
|
|
||||||
// selecting
|
// selecting
|
||||||
case EntrySetAction.share:
|
case EntrySetAction.share:
|
||||||
_share(context);
|
_share(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.delete:
|
case EntrySetAction.delete:
|
||||||
case EntrySetAction.emptyBin:
|
case EntrySetAction.emptyBin:
|
||||||
_delete(context);
|
_delete(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.restore:
|
case EntrySetAction.restore:
|
||||||
_move(context, moveType: MoveType.fromBin);
|
_move(context, moveType: MoveType.fromBin);
|
||||||
break;
|
|
||||||
case EntrySetAction.copy:
|
case EntrySetAction.copy:
|
||||||
_move(context, moveType: MoveType.copy);
|
_move(context, moveType: MoveType.copy);
|
||||||
break;
|
|
||||||
case EntrySetAction.move:
|
case EntrySetAction.move:
|
||||||
_move(context, moveType: MoveType.move);
|
_move(context, moveType: MoveType.move);
|
||||||
break;
|
|
||||||
case EntrySetAction.rename:
|
case EntrySetAction.rename:
|
||||||
_rename(context);
|
_rename(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.convert:
|
case EntrySetAction.convert:
|
||||||
_convert(context);
|
_convert(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.toggleFavourite:
|
case EntrySetAction.toggleFavourite:
|
||||||
_toggleFavourite(context);
|
_toggleFavourite(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.rotateCCW:
|
case EntrySetAction.rotateCCW:
|
||||||
_rotate(context, clockwise: false);
|
_rotate(context, clockwise: false);
|
||||||
break;
|
|
||||||
case EntrySetAction.rotateCW:
|
case EntrySetAction.rotateCW:
|
||||||
_rotate(context, clockwise: true);
|
_rotate(context, clockwise: true);
|
||||||
break;
|
|
||||||
case EntrySetAction.flip:
|
case EntrySetAction.flip:
|
||||||
_flip(context);
|
_flip(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.editDate:
|
case EntrySetAction.editDate:
|
||||||
editDate(context);
|
editDate(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.editLocation:
|
case EntrySetAction.editLocation:
|
||||||
_editLocation(context);
|
_editLocation(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.editTitleDescription:
|
case EntrySetAction.editTitleDescription:
|
||||||
_editTitleDescription(context);
|
_editTitleDescription(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.editRating:
|
case EntrySetAction.editRating:
|
||||||
_editRating(context);
|
_editRating(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.editTags:
|
case EntrySetAction.editTags:
|
||||||
_editTags(context);
|
_editTags(context);
|
||||||
break;
|
|
||||||
case EntrySetAction.removeMetadata:
|
case EntrySetAction.removeMetadata:
|
||||||
_removeMetadata(context);
|
_removeMetadata(context);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,6 @@ class CollectionSectionHeader extends StatelessWidget {
|
||||||
case EntryGroupFactor.none:
|
case EntryGroupFactor.none:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
return _buildAlbumHeader(context);
|
return _buildAlbumHeader(context);
|
||||||
case EntrySortFactor.rating:
|
case EntrySortFactor.rating:
|
||||||
|
|
|
@ -78,7 +78,7 @@ class EntryListDetails extends StatelessWidget {
|
||||||
|
|
||||||
Widget _buildDateRow(BuildContext context, TextStyle style) {
|
Widget _buildDateRow(BuildContext context, TextStyle style) {
|
||||||
final locale = context.l10n.localeName;
|
final locale = context.l10n.localeName;
|
||||||
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
final use24hour = MediaQuery.alwaysUse24HourFormatOf(context);
|
||||||
final date = entry.bestDate;
|
final date = entry.bestDate;
|
||||||
final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable;
|
final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable;
|
||||||
|
|
||||||
|
|
|
@ -41,17 +41,13 @@ class InteractiveTile extends StatelessWidget {
|
||||||
} else {
|
} else {
|
||||||
OpenViewerNotification(entry).dispatch(context);
|
OpenViewerNotification(entry).dispatch(context);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case AppMode.pickSingleMediaExternal:
|
case AppMode.pickSingleMediaExternal:
|
||||||
IntentService.submitPickedItems([entry.uri]);
|
IntentService.submitPickedItems([entry.uri]);
|
||||||
break;
|
|
||||||
case AppMode.pickMultipleMediaExternal:
|
case AppMode.pickMultipleMediaExternal:
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
selection.toggleSelection(entry);
|
selection.toggleSelection(entry);
|
||||||
break;
|
|
||||||
case AppMode.pickMediaInternal:
|
case AppMode.pickMediaInternal:
|
||||||
Navigator.maybeOf(context)?.pop(entry);
|
Navigator.maybeOf(context)?.pop(entry);
|
||||||
break;
|
|
||||||
case AppMode.pickCollectionFiltersExternal:
|
case AppMode.pickCollectionFiltersExternal:
|
||||||
case AppMode.pickFilterInternal:
|
case AppMode.pickFilterInternal:
|
||||||
case AppMode.screenSaver:
|
case AppMode.screenSaver:
|
||||||
|
|
|
@ -52,7 +52,7 @@ class _EntryQueryBarState extends State<EntryQueryBar> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final textScaleFactor = context.select<MediaQueryData, double>((mq) => mq.textScaleFactor);
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
return Container(
|
return Container(
|
||||||
height: EntryQueryBar.getPreferredHeight(textScaleFactor),
|
height: EntryQueryBar.getPreferredHeight(textScaleFactor),
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/action_controls/quick_choosers/common/quick_chooser.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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedRowRect.value = Rect.fromLTWH(0, window.physicalSize.height * (reversed ? 1 : -1), 0, 0);
|
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_selectedRowRect.value = Rect.fromLTWH(0, MediaQuery.sizeOf(context).height * (reversed ? 1 : -1), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant MenuQuickChooser<T> oldWidget) {
|
void didUpdateWidget(covariant MenuQuickChooser<T> oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
|
@ -33,10 +33,8 @@ class QuickChooserRouteLayout extends SingleChildLayoutDelegate {
|
||||||
switch (menuPosition) {
|
switch (menuPosition) {
|
||||||
case PopupMenuPosition.over:
|
case PopupMenuPosition.over:
|
||||||
y = triggerRect.top - childSize.height;
|
y = triggerRect.top - childSize.height;
|
||||||
break;
|
|
||||||
case PopupMenuPosition.under:
|
case PopupMenuPosition.under:
|
||||||
y = size.height - triggerRect.bottom;
|
y = size.height - triggerRect.bottom;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
double x = (triggerRect.left + (size.width - triggerRect.right) - childSize.width) / 2;
|
double x = (triggerRect.left + (size.width - triggerRect.right) - childSize.width) / 2;
|
||||||
final wantedPosition = Offset(x, y);
|
final wantedPosition = Offset(x, y);
|
||||||
|
|
|
@ -309,17 +309,14 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||||
..remove(destinationAlbum)
|
..remove(destinationAlbum)
|
||||||
..insert(0, destinationAlbum);
|
..insert(0, destinationAlbum);
|
||||||
entriesByDestination[destinationAlbum] = entries;
|
entriesByDestination[destinationAlbum] = entries;
|
||||||
break;
|
|
||||||
case MoveType.toBin:
|
case MoveType.toBin:
|
||||||
entriesByDestination[AndroidFileUtils.trashDirPath] = entries;
|
entriesByDestination[AndroidFileUtils.trashDirPath] = entries;
|
||||||
break;
|
|
||||||
case MoveType.fromBin:
|
case MoveType.fromBin:
|
||||||
groupBy<AvesEntry, String?>(entries, (e) => e.directory).forEach((originAlbum, dirEntries) {
|
groupBy<AvesEntry, String?>(entries, (e) => e.directory).forEach((originAlbum, dirEntries) {
|
||||||
if (originAlbum != null) {
|
if (originAlbum != null) {
|
||||||
entriesByDestination[originAlbum] = dirEntries.toSet();
|
entriesByDestination[originAlbum] = dirEntries.toSet();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await doQuickMove(
|
await doQuickMove(
|
||||||
|
|
|
@ -8,66 +8,82 @@ import 'package:flutter/material.dart';
|
||||||
// This overlay entry is not below a `Scaffold` (which is expected by `SnackBar`
|
// This overlay entry is not below a `Scaffold` (which is expected by `SnackBar`
|
||||||
// and `SnackBarAction`), and is not dismissed the same way.
|
// and `SnackBarAction`), and is not dismissed the same way.
|
||||||
// This adaptation assumes the `SnackBarBehavior.floating` behavior and no animation.
|
// This adaptation assumes the `SnackBarBehavior.floating` behavior and no animation.
|
||||||
class OverlaySnackBar extends StatelessWidget {
|
class OverlaySnackBar extends StatefulWidget {
|
||||||
final Widget content;
|
final Widget content;
|
||||||
final Widget? action;
|
final Widget? action;
|
||||||
final DismissDirection dismissDirection;
|
final DismissDirection dismissDirection;
|
||||||
final VoidCallback onDismiss;
|
final VoidCallback onDismiss;
|
||||||
|
final Clip clipBehavior;
|
||||||
|
|
||||||
const OverlaySnackBar({
|
const OverlaySnackBar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.action,
|
this.action,
|
||||||
required this.dismissDirection,
|
this.dismissDirection = DismissDirection.down,
|
||||||
|
this.clipBehavior = Clip.hardEdge,
|
||||||
required this.onDismiss,
|
required this.onDismiss,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OverlaySnackBar> createState() => _OverlaySnackBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverlaySnackBarState extends State<OverlaySnackBar> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
assert(debugCheckHasMediaQuery(context));
|
||||||
final colorScheme = theme.colorScheme;
|
final ThemeData theme = Theme.of(context);
|
||||||
final snackBarTheme = theme.snackBarTheme;
|
final ColorScheme colorScheme = theme.colorScheme;
|
||||||
final isThemeDark = theme.brightness == Brightness.dark;
|
final SnackBarThemeData snackBarTheme = theme.snackBarTheme;
|
||||||
final buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary;
|
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;
|
// SnackBar uses a theme that is the opposite brightness from
|
||||||
final themeBackgroundColor = isThemeDark ? colorScheme.onSurface : Color.alphaBlend(colorScheme.onSurface.withOpacity(0.80), colorScheme.surface);
|
// the surrounding theme.
|
||||||
final inverseTheme = theme.copyWith(
|
final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark;
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
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 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;
|
const singleLineVerticalPadding = 14.0;
|
||||||
|
|
||||||
|
final EdgeInsets margin = snackBarTheme.insetPadding ?? defaults.insetPadding!;
|
||||||
|
|
||||||
Widget snackBar = Padding(
|
Widget snackBar = Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: action != null ? null : const EdgeInsets.symmetric(vertical: singleLineVerticalPadding),
|
padding: widget.action != null ? null : const EdgeInsets.symmetric(vertical: singleLineVerticalPadding),
|
||||||
child: DefaultTextStyle(
|
child: DefaultTextStyle(
|
||||||
style: contentTextStyle!,
|
style: contentTextStyle!,
|
||||||
child: content,
|
child: widget.content,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (action != null)
|
if (widget.action != null)
|
||||||
TextButtonTheme(
|
TextButtonTheme(
|
||||||
data: TextButtonThemeData(
|
data: TextButtonThemeData(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
|
@ -75,36 +91,28 @@ class OverlaySnackBar extends StatelessWidget {
|
||||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: action!,
|
child: widget.action!,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final elevation = snackBarTheme.elevation ?? 6.0;
|
final double elevation = snackBarTheme.elevation ?? defaults.elevation!;
|
||||||
final backgroundColor = snackBarTheme.backgroundColor ?? inverseTheme.colorScheme.background;
|
final Color backgroundColor = snackBarTheme.backgroundColor ?? defaults.backgroundColor!;
|
||||||
final shape = snackBarTheme.shape ?? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
|
final ShapeBorder? shape = snackBarTheme.shape ?? defaults.shape;
|
||||||
|
|
||||||
snackBar = Material(
|
snackBar = Material(
|
||||||
shape: shape,
|
shape: shape,
|
||||||
elevation: elevation,
|
elevation: elevation,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Theme(
|
child: Theme(
|
||||||
data: inverseTheme,
|
data: effectiveTheme,
|
||||||
child: snackBar,
|
child: snackBar,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const topMargin = 5.0;
|
|
||||||
const bottomMargin = 10.0;
|
|
||||||
const horizontalMargin = 15.0;
|
|
||||||
snackBar = Padding(
|
snackBar = Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: margin,
|
||||||
horizontalMargin,
|
|
||||||
topMargin,
|
|
||||||
horizontalMargin,
|
|
||||||
bottomMargin,
|
|
||||||
),
|
|
||||||
child: snackBar,
|
child: snackBar,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -117,16 +125,138 @@ class OverlaySnackBar extends StatelessWidget {
|
||||||
snackBar = Semantics(
|
snackBar = Semantics(
|
||||||
container: true,
|
container: true,
|
||||||
liveRegion: true,
|
liveRegion: true,
|
||||||
onDismiss: onDismiss,
|
onDismiss: widget.onDismiss,
|
||||||
child: Dismissible(
|
child: Dismissible(
|
||||||
key: const Key('dismissible'),
|
key: const Key('dismissible'),
|
||||||
direction: dismissDirection,
|
direction: widget.dismissDirection,
|
||||||
resizeDuration: null,
|
resizeDuration: null,
|
||||||
onDismissed: (direction) => onDismiss(),
|
onDismissed: (direction) => widget.onDismiss(),
|
||||||
child: snackBar,
|
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.copy:
|
||||||
case MoveType.export:
|
case MoveType.export:
|
||||||
needed = selection.fold(0, sumSize);
|
needed = selection.fold(0, sumSize);
|
||||||
break;
|
|
||||||
case MoveType.move:
|
case MoveType.move:
|
||||||
case MoveType.toBin:
|
case MoveType.toBin:
|
||||||
case MoveType.fromBin:
|
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
|
// 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));
|
final largestSingle = selection.fold<int>(0, (largest, entry) => max(largest, entry.sizeBytes ?? 0));
|
||||||
needed = max(fromOtherVolumes, largestSingle);
|
needed = max(fromOtherVolumes, largestSingle);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasEnoughSpace = needed < free;
|
final hasEnoughSpace = needed < free;
|
||||||
|
|
|
@ -36,7 +36,6 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case VaultLockType.pattern:
|
case VaultLockType.pattern:
|
||||||
final pattern = await showDialog<String>(
|
final pattern = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -46,7 +45,6 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
if (pattern != null) {
|
if (pattern != null) {
|
||||||
confirmed = pattern == await securityService.readValue(details.passKey);
|
confirmed = pattern == await securityService.readValue(details.passKey);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case VaultLockType.pin:
|
case VaultLockType.pin:
|
||||||
final pin = await showDialog<String>(
|
final pin = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -56,7 +54,6 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
confirmed = pin == await securityService.readValue(details.passKey);
|
confirmed = pin == await securityService.readValue(details.passKey);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case VaultLockType.password:
|
case VaultLockType.password:
|
||||||
final password = await showDialog<String>(
|
final password = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -66,7 +63,6 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
confirmed = password == await securityService.readValue(details.passKey);
|
confirmed = password == await securityService.readValue(details.passKey);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirmed == null || !confirmed) return false;
|
if (confirmed == null || !confirmed) return false;
|
||||||
|
@ -120,7 +116,6 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case VaultLockType.pattern:
|
case VaultLockType.pattern:
|
||||||
final pattern = await showDialog<String>(
|
final pattern = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -130,7 +125,6 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
if (pattern != null) {
|
if (pattern != null) {
|
||||||
return await securityService.writeValue(details.passKey, pattern);
|
return await securityService.writeValue(details.passKey, pattern);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case VaultLockType.pin:
|
case VaultLockType.pin:
|
||||||
final pin = await showDialog<String>(
|
final pin = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -140,7 +134,6 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
return await securityService.writeValue(details.passKey, pin);
|
return await securityService.writeValue(details.passKey, pin);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case VaultLockType.password:
|
case VaultLockType.password:
|
||||||
final password = await showDialog<String>(
|
final password = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -150,7 +143,6 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
return await securityService.writeValue(details.passKey, password);
|
return await securityService.writeValue(details.passKey, password);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class InteractiveAppBarTitle extends StatelessWidget {
|
class InteractiveAppBarTitle extends StatelessWidget {
|
||||||
final GestureTapCallback? onTap;
|
final GestureTapCallback? onTap;
|
||||||
|
@ -13,6 +12,7 @@ class InteractiveAppBarTitle extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
// use a `Container` with a dummy color to make it expand
|
// use a `Container` with a dummy color to make it expand
|
||||||
|
@ -20,7 +20,7 @@ class InteractiveAppBarTitle extends StatelessWidget {
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
height: kToolbarHeight * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor),
|
height: kToolbarHeight * textScaleFactor,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,10 +11,11 @@ class FontSizeIconTheme extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
final iconTheme = IconTheme.of(context);
|
final iconTheme = IconTheme.of(context);
|
||||||
return IconTheme(
|
return IconTheme(
|
||||||
data: iconTheme.copyWith(
|
data: iconTheme.copyWith(
|
||||||
size: iconTheme.size! * MediaQuery.textScaleFactorOf(context),
|
size: iconTheme.size! * textScaleFactor,
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,7 +22,7 @@ class BottomGestureAreaProtector extends StatelessWidget {
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
height: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.bottom),
|
height: MediaQuery.systemGestureInsetsOf(context).bottom,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// absorb vertical gestures only
|
// absorb vertical gestures only
|
||||||
onVerticalDragDown: (details) {},
|
onVerticalDragDown: (details) {},
|
||||||
|
@ -42,7 +42,7 @@ class TopGestureAreaProtector extends StatelessWidget {
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.top),
|
height: MediaQuery.systemGestureInsetsOf(context).top,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// absorb vertical gestures only
|
// absorb vertical gestures only
|
||||||
onVerticalDragDown: (details) {},
|
onVerticalDragDown: (details) {},
|
||||||
|
@ -64,7 +64,7 @@ class SideGestureAreaProtector extends StatelessWidget {
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.left),
|
width: MediaQuery.systemGestureInsetsOf(context).left,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// absorb horizontal gestures only
|
// absorb horizontal gestures only
|
||||||
onHorizontalDragDown: (details) {},
|
onHorizontalDragDown: (details) {},
|
||||||
|
@ -73,7 +73,7 @@ class SideGestureAreaProtector extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: context.select<MediaQueryData, double>((mq) => mq.systemGestureInsets.right),
|
width: MediaQuery.systemGestureInsetsOf(context).right,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// absorb horizontal gestures only
|
// absorb horizontal gestures only
|
||||||
onHorizontalDragDown: (details) {},
|
onHorizontalDragDown: (details) {},
|
||||||
|
|
|
@ -54,11 +54,9 @@ class ReselectableRadioListTile<T> extends StatelessWidget {
|
||||||
case ListTileControlAffinity.platform:
|
case ListTileControlAffinity.platform:
|
||||||
leading = control;
|
leading = control;
|
||||||
trailing = secondary;
|
trailing = secondary;
|
||||||
break;
|
|
||||||
case ListTileControlAffinity.trailing:
|
case ListTileControlAffinity.trailing:
|
||||||
leading = secondary;
|
leading = secondary;
|
||||||
trailing = control;
|
trailing = control;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return MergeSemantics(
|
return MergeSemantics(
|
||||||
child: ListTileTheme.merge(
|
child: ListTileTheme.merge(
|
||||||
|
|
|
@ -54,7 +54,7 @@ class MarkdownContainer extends StatelessWidget {
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).canvasColor,
|
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)),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints(maxWidth: useTvLayout ? double.infinity : mobileMaxWidth),
|
constraints: BoxConstraints(maxWidth: useTvLayout ? double.infinity : mobileMaxWidth),
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class AvesScaffold extends StatelessWidget {
|
class AvesScaffold extends StatelessWidget {
|
||||||
final PreferredSizeWidget? appBar;
|
final PreferredSizeWidget? appBar;
|
||||||
|
@ -26,7 +25,7 @@ class AvesScaffold extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// prevent conflict between drawer drag gesture and Android navigation gestures
|
// 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(
|
return Scaffold(
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
|
|
|
@ -31,44 +31,51 @@ class OutlinedText extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// TODO TLAD [subtitles] fix background area for mixed alphabetic-ideographic text
|
// 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
|
// as of Flutter v3.10.0, the area computed for `backgroundColor` has inconsistent height
|
||||||
// in case of mixed alphabetic-ideographic text. The painted boxes depends on the script.
|
// in case of mixed alphabetic-ideographic text. The painted boxes depend on the script.
|
||||||
// Possible workarounds would be to use metrics from:
|
// Possible workarounds would be to use metrics from:
|
||||||
// - `TextPainter.getBoxesForSelection`
|
// - `TextPainter.getBoxesForSelection`
|
||||||
// - `Paragraph.getBoxesForRange`
|
// - `Paragraph.getBoxesForRange`
|
||||||
// and paint the background at the bottom of the `Stack`
|
// and paint the background at the bottom of the `Stack`
|
||||||
|
|
||||||
final hasOutline = outlineWidth > 0;
|
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(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
if (hasOutline)
|
if (outline != null) outline,
|
||||||
ImageFiltered(
|
fill,
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -89,6 +96,7 @@ class OutlinedText extends StatelessWidget {
|
||||||
children: span.children,
|
children: span.children,
|
||||||
style: (span.style ?? const TextStyle()).copyWith(
|
style: (span.style ?? const TextStyle()).copyWith(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
shadows: [],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,11 @@ class _WheelSelectorState<T> extends State<WheelSelector<T>> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
const background = Colors.transparent;
|
const background = Colors.transparent;
|
||||||
final foreground = DefaultTextStyle.of(context).style.color!;
|
final foreground = DefaultTextStyle.of(context).style.color!;
|
||||||
final transitionDuration = context.select<DurationsData, Duration>((v) => v.formTransition);
|
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(
|
return FocusableActionDetector(
|
||||||
shortcuts: const {
|
shortcuts: const {
|
||||||
|
@ -138,10 +139,8 @@ class _WheelSelectorState<T> extends State<WheelSelector<T>> {
|
||||||
switch (intent.type) {
|
switch (intent.type) {
|
||||||
case _ValueAdjustmentType.up:
|
case _ValueAdjustmentType.up:
|
||||||
delta = -1;
|
delta = -1;
|
||||||
break;
|
|
||||||
case _ValueAdjustmentType.down:
|
case _ValueAdjustmentType.down:
|
||||||
delta = 1;
|
delta = 1;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
final targetItem = _controller.selectedItem + delta;
|
final targetItem = _controller.selectedItem + delta;
|
||||||
final duration = context.read<DurationsData>().formTransition;
|
final duration = context.read<DurationsData>().formTransition;
|
||||||
|
|
|
@ -26,20 +26,47 @@ enum _ScaleState {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PointerPanZoomData {
|
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;
|
_PointerPanZoomData.fromUpdateEvent(this.parent, PointerPanZoomUpdateEvent event)
|
||||||
double scale;
|
: _position = event.position,
|
||||||
double rotation;
|
_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
|
@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) {
|
bool _isFlingGesture(Velocity velocity) {
|
||||||
assert(velocity != null);
|
|
||||||
final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
|
final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
|
||||||
return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
|
return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
|
||||||
}
|
}
|
||||||
|
@ -57,9 +84,7 @@ class _LineBetweenPointers {
|
||||||
this.pointerStartId = 0,
|
this.pointerStartId = 0,
|
||||||
this.pointerEndLocation = Offset.zero,
|
this.pointerEndLocation = Offset.zero,
|
||||||
this.pointerEndId = 1,
|
this.pointerEndId = 1,
|
||||||
}) : assert(pointerStartLocation != null && pointerEndLocation != null),
|
}) : assert(pointerStartId != pointerEndId);
|
||||||
assert(pointerStartId != null && pointerEndId != null),
|
|
||||||
assert(pointerStartId != pointerEndId);
|
|
||||||
|
|
||||||
// The location and the id of the pointer that marks the start of the line.
|
// The location and the id of the pointer that marks the start of the line.
|
||||||
final Offset pointerStartLocation;
|
final Offset pointerStartLocation;
|
||||||
|
@ -74,23 +99,22 @@ class _LineBetweenPointers {
|
||||||
///
|
///
|
||||||
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
|
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
|
||||||
/// calculates their focal point, indicated scale, and rotation. When a focal
|
/// calculates their focal point, indicated scale, and rotation. When a focal
|
||||||
/// pointer is established, the recognizer calls [onStart]. As the focal point,
|
/// point is established, the recognizer calls [onStart]. As the focal point,
|
||||||
/// scale, rotation change, the recognizer calls [onUpdate]. When the pointers
|
/// scale, and rotation change, the recognizer calls [onUpdate]. When the
|
||||||
/// are no longer in contact with the screen, the recognizer calls [onEnd].
|
/// pointers are no longer in contact with the screen, the recognizer calls
|
||||||
|
/// [onEnd].
|
||||||
class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
/// Create a gesture recognizer for interactions intended for scaling content.
|
/// Create a gesture recognizer for interactions intended for scaling content.
|
||||||
///
|
///
|
||||||
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
|
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
|
||||||
EagerScaleGestureRecognizer({
|
EagerScaleGestureRecognizer({
|
||||||
super.debugOwner,
|
super.debugOwner,
|
||||||
@Deprecated(
|
|
||||||
'Migrate to supportedDevices. '
|
|
||||||
'This feature was deprecated after v2.3.0-1.0.pre.',
|
|
||||||
)
|
|
||||||
super.kind,
|
|
||||||
super.supportedDevices,
|
super.supportedDevices,
|
||||||
|
super.allowedButtonsFilter,
|
||||||
this.dragStartBehavior = DragStartBehavior.down,
|
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
|
/// Determines what point is used as the starting point in all calculations
|
||||||
/// involving this gesture.
|
/// involving this gesture.
|
||||||
|
@ -137,6 +161,26 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
|
|
||||||
Matrix4? _lastTransform;
|
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;
|
late Offset _initialFocalPoint;
|
||||||
Offset? _currentFocalPoint;
|
Offset? _currentFocalPoint;
|
||||||
late double _initialSpan;
|
late double _initialSpan;
|
||||||
|
@ -151,6 +195,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
final Map<int, Offset> _pointerLocations = <int, Offset>{};
|
final Map<int, Offset> _pointerLocations = <int, Offset>{};
|
||||||
final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
|
final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
|
||||||
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
||||||
|
VelocityTracker? _scaleVelocityTracker;
|
||||||
late Offset _delta;
|
late Offset _delta;
|
||||||
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
|
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
|
||||||
double _initialPanZoomScaleFactor = 1;
|
double _initialPanZoomScaleFactor = 1;
|
||||||
|
@ -271,15 +316,16 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
_lastTransform = event.transform;
|
_lastTransform = event.transform;
|
||||||
} else if (event is PointerPanZoomStartEvent) {
|
} else if (event is PointerPanZoomStartEvent) {
|
||||||
assert(_pointerPanZooms[event.pointer] == null);
|
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;
|
didChangeConfiguration = true;
|
||||||
shouldStartIfAccepted = true;
|
shouldStartIfAccepted = true;
|
||||||
|
_lastTransform = event.transform;
|
||||||
} else if (event is PointerPanZoomUpdateEvent) {
|
} else if (event is PointerPanZoomUpdateEvent) {
|
||||||
assert(_pointerPanZooms[event.pointer] != null);
|
assert(_pointerPanZooms[event.pointer] != null);
|
||||||
if (!event.synthesized) {
|
if (!event.synthesized && !trackpadScrollCausesScale) {
|
||||||
_velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
|
_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;
|
_lastTransform = event.transform;
|
||||||
shouldStartIfAccepted = true;
|
shouldStartIfAccepted = true;
|
||||||
} else if (event is PointerPanZoomEndEvent) {
|
} else if (event is PointerPanZoomEndEvent) {
|
||||||
|
@ -292,7 +338,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
_update();
|
_update();
|
||||||
|
|
||||||
if (!didChangeConfiguration || _reconfigure(event.pointer)) {
|
if (!didChangeConfiguration || _reconfigure(event.pointer)) {
|
||||||
_advanceStateMachine(shouldStartIfAccepted, event.kind);
|
_advanceStateMachine(shouldStartIfAccepted, event);
|
||||||
}
|
}
|
||||||
stopTrackingIfPointerNoLongerDown(event);
|
stopTrackingIfPointerNoLongerDown(event);
|
||||||
}
|
}
|
||||||
|
@ -403,18 +449,20 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) {
|
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) {
|
||||||
velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * 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 {
|
} else {
|
||||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount)));
|
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_state = _ScaleState.accepted;
|
_state = _ScaleState.accepted;
|
||||||
|
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
_scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
|
void _advanceStateMachine(bool shouldStartIfAccepted, PointerEvent event) {
|
||||||
if (_state == _ScaleState.ready) {
|
if (_state == _ScaleState.ready) {
|
||||||
_state = _ScaleState.possible;
|
_state = _ScaleState.possible;
|
||||||
}
|
}
|
||||||
|
@ -428,7 +476,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
if (_state == _ScaleState.possible) {
|
if (_state == _ScaleState.possible) {
|
||||||
final double spanDelta = (_currentSpan - _initialSpan).abs();
|
final double spanDelta = (_currentSpan - _initialSpan).abs();
|
||||||
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
|
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);
|
resolve(GestureDisposition.accepted);
|
||||||
}
|
}
|
||||||
} else if (_state.index >= _ScaleState.accepted.index) {
|
} else if (_state.index >= _ScaleState.accepted.index) {
|
||||||
|
@ -440,19 +488,22 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
_dispatchOnStartCallbackIfNeeded();
|
_dispatchOnStartCallbackIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_state == _ScaleState.started && onUpdate != null) {
|
if (_state == _ScaleState.started) {
|
||||||
invokeCallback<void>('onUpdate', () {
|
_scaleVelocityTracker?.addPosition(event.timeStamp, Offset(_scaleFactor, 0));
|
||||||
onUpdate!(ScaleUpdateDetails(
|
if (onUpdate != null) {
|
||||||
scale: _scaleFactor,
|
invokeCallback<void>('onUpdate', () {
|
||||||
horizontalScale: _horizontalScaleFactor,
|
onUpdate!(ScaleUpdateDetails(
|
||||||
verticalScale: _verticalScaleFactor,
|
scale: _scaleFactor,
|
||||||
focalPoint: _currentFocalPoint!,
|
horizontalScale: _horizontalScaleFactor,
|
||||||
localFocalPoint: _localFocalPoint,
|
verticalScale: _verticalScaleFactor,
|
||||||
rotation: _computeRotationFactor(),
|
focalPoint: _currentFocalPoint!,
|
||||||
pointerCount: _pointerCount,
|
localFocalPoint: _localFocalPoint,
|
||||||
focalPointDelta: _delta,
|
rotation: _computeRotationFactor(),
|
||||||
));
|
pointerCount: _pointerCount,
|
||||||
});
|
focalPointDelta: _delta,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -504,15 +555,12 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
switch (_state) {
|
switch (_state) {
|
||||||
case _ScaleState.possible:
|
case _ScaleState.possible:
|
||||||
resolve(GestureDisposition.rejected);
|
resolve(GestureDisposition.rejected);
|
||||||
break;
|
|
||||||
case _ScaleState.ready:
|
case _ScaleState.ready:
|
||||||
assert(false); // We should have not seen a pointer yet
|
assert(false); // We should have not seen a pointer yet
|
||||||
break;
|
|
||||||
case _ScaleState.accepted:
|
case _ScaleState.accepted:
|
||||||
break;
|
break;
|
||||||
case _ScaleState.started:
|
case _ScaleState.started:
|
||||||
assert(false); // We should be in the accepted state when user is done
|
assert(false); // We should be in the accepted state when user is done
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
_state = _ScaleState.ready;
|
_state = _ScaleState.ready;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,9 @@ class ScrollControllerAction extends CallbackAction<ScrollIntent> {
|
||||||
case AxisDirection.up:
|
case AxisDirection.up:
|
||||||
case AxisDirection.left:
|
case AxisDirection.left:
|
||||||
factor = -1;
|
factor = -1;
|
||||||
break;
|
|
||||||
case AxisDirection.down:
|
case AxisDirection.down:
|
||||||
case AxisDirection.right:
|
case AxisDirection.right:
|
||||||
factor = 1;
|
factor = 1;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
scrollController.animateTo(
|
scrollController.animateTo(
|
||||||
scrollController.offset + factor * 150,
|
scrollController.offset + factor * 150,
|
||||||
|
|
|
@ -53,7 +53,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics {
|
||||||
// Scenario 3:
|
// Scenario 3:
|
||||||
// If there's no velocity and we're already at where we intend to land,
|
// If there's no velocity and we're already at where we intend to land,
|
||||||
// do nothing.
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics {
|
||||||
metrics.pixels,
|
metrics.pixels,
|
||||||
settlingPixels,
|
settlingPixels,
|
||||||
velocity,
|
velocity,
|
||||||
tolerance: tolerance,
|
tolerance: toleranceFor(position),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics {
|
||||||
metrics.pixels,
|
metrics.pixels,
|
||||||
settlingPixels,
|
settlingPixels,
|
||||||
velocity,
|
velocity,
|
||||||
tolerance.velocity * velocity.sign,
|
toleranceFor(position).velocity * velocity.sign,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,5 +16,4 @@ class SpringyScrollPhysics extends ScrollPhysics {
|
||||||
parent: buildParent(ancestor),
|
parent: buildParent(ancestor),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AvesBorder {
|
class AvesBorder {
|
||||||
static Color _borderColor(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? Colors.white30 : Colors.black26;
|
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
|
// 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
|
// 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(
|
static BorderSide straightSide(BuildContext context, {double? width}) => BorderSide(
|
||||||
color: _borderColor(context),
|
color: _borderColor(context),
|
||||||
width: width ?? straightBorderWidth,
|
width: width ?? straightBorderWidth(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
static BorderSide curvedSide(BuildContext context, {double? width}) => BorderSide(
|
static BorderSide curvedSide(BuildContext context, {double? width}) => BorderSide(
|
||||||
color: _borderColor(context),
|
color: _borderColor(context),
|
||||||
width: width ?? curvedBorderWidth,
|
width: width ?? curvedBorderWidth(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
static Border border(BuildContext context, {double? width}) => Border.fromBorderSide(curvedSide(context, width: width));
|
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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/semantics.dart';
|
||||||
|
|
||||||
// adapted from Flutter `_ImageState` in `/widgets/image.dart`
|
// adapted from Flutter `_ImageState` in `/widgets/image.dart`
|
||||||
// and `DecorationImagePainter` in `/painting/decoration_image.dart`
|
// and `DecorationImagePainter` in `/painting/decoration_image.dart`
|
||||||
|
@ -13,6 +15,11 @@ class TransitionImage extends StatefulWidget {
|
||||||
final ImageProvider image;
|
final ImageProvider image;
|
||||||
final ValueListenable<double> animation;
|
final ValueListenable<double> animation;
|
||||||
final BoxFit thumbnailFit, viewerFit;
|
final BoxFit thumbnailFit, viewerFit;
|
||||||
|
final ImageFrameBuilder? frameBuilder;
|
||||||
|
final ImageLoadingBuilder? loadingBuilder;
|
||||||
|
final ImageErrorWidgetBuilder? errorBuilder;
|
||||||
|
final String? semanticLabel;
|
||||||
|
final bool excludeFromSemantics;
|
||||||
final double? width, height;
|
final double? width, height;
|
||||||
final bool gaplessPlayback = false;
|
final bool gaplessPlayback = false;
|
||||||
final Color? background;
|
final Color? background;
|
||||||
|
@ -23,6 +30,11 @@ class TransitionImage extends StatefulWidget {
|
||||||
required this.animation,
|
required this.animation,
|
||||||
required this.thumbnailFit,
|
required this.thumbnailFit,
|
||||||
required this.viewerFit,
|
required this.viewerFit,
|
||||||
|
this.frameBuilder,
|
||||||
|
this.loadingBuilder,
|
||||||
|
this.errorBuilder,
|
||||||
|
this.semanticLabel,
|
||||||
|
this.excludeFromSemantics = false,
|
||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.background,
|
this.background,
|
||||||
|
@ -32,16 +44,33 @@ class TransitionImage extends StatefulWidget {
|
||||||
State<TransitionImage> createState() => _TransitionImageState();
|
State<TransitionImage> createState() => _TransitionImageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TransitionImageState extends State<TransitionImage> {
|
class _TransitionImageState extends State<TransitionImage> with WidgetsBindingObserver {
|
||||||
ImageStream? _imageStream;
|
ImageStream? _imageStream;
|
||||||
ImageInfo? _imageInfo;
|
ImageInfo? _imageInfo;
|
||||||
|
ImageChunkEvent? _loadingProgress;
|
||||||
bool _isListeningToStream = false;
|
bool _isListeningToStream = false;
|
||||||
int? _frameNumber;
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
assert(_imageStream != null);
|
assert(_imageStream != null);
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_stopListeningToStream();
|
_stopListeningToStream();
|
||||||
|
_completerHandle?.dispose();
|
||||||
|
_scrollAwareContext.dispose();
|
||||||
|
_replaceImage(info: null);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,21 +81,23 @@ class _TransitionImageState extends State<TransitionImage> {
|
||||||
if (TickerMode.of(context)) {
|
if (TickerMode.of(context)) {
|
||||||
_listenToStream();
|
_listenToStream();
|
||||||
} else {
|
} else {
|
||||||
_stopListeningToStream();
|
_stopListeningToStream(keepStreamAlive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant TransitionImage oldWidget) {
|
void didUpdateWidget(TransitionImage oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (_isListeningToStream) {
|
if (_isListeningToStream && (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
|
||||||
final ImageStreamListener oldListener = _getListener();
|
final ImageStreamListener oldListener = _getListener();
|
||||||
_imageStream!.addListener(_getListener(recreateListener: true));
|
_imageStream!.addListener(_getListener(recreateListener: true));
|
||||||
_imageStream!.removeListener(oldListener);
|
_imageStream!.removeListener(oldListener);
|
||||||
}
|
}
|
||||||
if (widget.image != oldWidget.image) _resolveImage();
|
if (widget.image != oldWidget.image) {
|
||||||
|
_resolveImage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -76,8 +107,11 @@ class _TransitionImageState extends State<TransitionImage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resolveImage() {
|
void _resolveImage() {
|
||||||
final provider = widget.image;
|
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
|
||||||
final newStream = provider.resolve(createLocalImageConfiguration(
|
context: _scrollAwareContext,
|
||||||
|
imageProvider: widget.image,
|
||||||
|
);
|
||||||
|
final ImageStream newStream = provider.resolve(createLocalImageConfiguration(
|
||||||
context,
|
context,
|
||||||
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
|
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
|
||||||
));
|
));
|
||||||
|
@ -88,8 +122,26 @@ class _TransitionImageState extends State<TransitionImage> {
|
||||||
|
|
||||||
ImageStreamListener _getListener({bool recreateListener = false}) {
|
ImageStreamListener _getListener({bool recreateListener = false}) {
|
||||||
if (_imageStreamListener == null || recreateListener) {
|
if (_imageStreamListener == null || recreateListener) {
|
||||||
|
_lastException = null;
|
||||||
|
_lastStack = null;
|
||||||
_imageStreamListener = ImageStreamListener(
|
_imageStreamListener = ImageStreamListener(
|
||||||
_handleImageFrame,
|
_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!;
|
return _imageStreamListener!;
|
||||||
|
@ -98,15 +150,32 @@ class _TransitionImageState extends State<TransitionImage> {
|
||||||
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
|
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_replaceImage(info: imageInfo);
|
_replaceImage(info: imageInfo);
|
||||||
|
_loadingProgress = null;
|
||||||
|
_lastException = null;
|
||||||
|
_lastStack = null;
|
||||||
_frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
|
_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}) {
|
void _replaceImage({required ImageInfo? info}) {
|
||||||
_imageInfo?.dispose();
|
final ImageInfo? oldImageInfo = _imageInfo;
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());
|
||||||
_imageInfo = info;
|
_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) {
|
void _updateSourceStream(ImageStream newStream) {
|
||||||
if (_imageStream?.key == newStream.key) {
|
if (_imageStream?.key == newStream.key) {
|
||||||
return;
|
return;
|
||||||
|
@ -123,7 +192,9 @@ class _TransitionImageState extends State<TransitionImage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_loadingProgress = null;
|
||||||
_frameNumber = null;
|
_frameNumber = null;
|
||||||
|
_wasSynchronouslyLoaded = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
_imageStream = newStream;
|
_imageStream = newStream;
|
||||||
|
@ -138,22 +209,72 @@ class _TransitionImageState extends State<TransitionImage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_imageStream!.addListener(_getListener());
|
_imageStream!.addListener(_getListener());
|
||||||
|
_completerHandle?.dispose();
|
||||||
|
_completerHandle = null;
|
||||||
|
|
||||||
_isListeningToStream = true;
|
_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) {
|
if (!_isListeningToStream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) {
|
||||||
|
_completerHandle = _imageStream!.completer!.keepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
_imageStream!.removeListener(_getListener());
|
_imageStream!.removeListener(_getListener());
|
||||||
_isListeningToStream = false;
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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,
|
valueListenable: widget.animation,
|
||||||
builder: (context, t, child) => CustomPaint(
|
builder: (context, t, child) => CustomPaint(
|
||||||
painter: _TransitionImagePainter(
|
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;
|
const alignment = Alignment.center;
|
||||||
|
|
||||||
final rect = ui.Rect.fromLTWH(0, 0, size.width, size.height);
|
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 outputSize = rect.size;
|
||||||
|
final inputSize = Size(image!.width.toDouble(), image!.height.toDouble());
|
||||||
|
|
||||||
final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size);
|
final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size);
|
||||||
final viewerSizes = applyBoxFit(viewerFit, 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;
|
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 = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
|
||||||
// grid section metrics before the app is laid out with the new orientation
|
// grid section metrics before the app is laid out with the new orientation
|
||||||
late SectionedListLayout<T> _lastSectionedListLayout;
|
late SectionedListLayout<T> _lastSectionedListLayout;
|
||||||
late Size _lastScrollableSize;
|
late Size _lastScrollableSize;
|
||||||
late Orientation _lastOrientation;
|
Orientation _lastOrientation = Orientation.portrait;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final highlightInfo = context.read<HighlightInfo>();
|
final highlightInfo = context.read<HighlightInfo>();
|
||||||
_subscriptions.add(highlightInfo.eventBus.on<TrackEvent<T>>().listen(_trackItem));
|
_subscriptions.add(highlightInfo.eventBus.on<TrackEvent<T>>().listen(_trackItem));
|
||||||
_lastOrientation = _windowOrientation;
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_saveLayoutMetrics();
|
_saveLayoutMetrics();
|
||||||
}
|
}
|
||||||
|
@ -78,9 +72,10 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
|
||||||
@override
|
@override
|
||||||
void didChangeMetrics() {
|
void didChangeMetrics() {
|
||||||
// the order of `WidgetsBindingObserver` metrics change notification is unreliable
|
// 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
|
// `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) {
|
if (_lastOrientation != orientation) {
|
||||||
_lastOrientation = orientation;
|
_lastOrientation = orientation;
|
||||||
_onLayoutChanged();
|
_onLayoutChanged();
|
||||||
|
@ -138,7 +133,7 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
|
||||||
Future<void> _saveLayoutMetrics() async {
|
Future<void> _saveLayoutMetrics() async {
|
||||||
// use a delay to obtain current layout metrics
|
// use a delay to obtain current layout metrics
|
||||||
// so that we can handle window orientation change with the previous 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));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
|
@ -57,7 +57,7 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final gestureSettings = context.select<MediaQueryData, DeviceGestureSettings>((mq) => mq.gestureSettings);
|
final gestureSettings = MediaQuery.gestureSettingsOf(context);
|
||||||
|
|
||||||
final child = GestureDetector(
|
final child = GestureDetector(
|
||||||
// Horizontal/vertical drag gestures are interpreted as scaling
|
// Horizontal/vertical drag gestures are interpreted as scaling
|
||||||
|
@ -116,11 +116,9 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
switch (tileLayout) {
|
switch (tileLayout) {
|
||||||
case TileLayout.mosaic:
|
case TileLayout.mosaic:
|
||||||
_startSize = Size.square(tileExtentController.extentNotifier.value);
|
_startSize = Size.square(tileExtentController.extentNotifier.value);
|
||||||
break;
|
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
case TileLayout.list:
|
case TileLayout.list:
|
||||||
_startSize = renderMetaData.size;
|
_startSize = renderMetaData.size;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
_scaledSizeNotifier = ValueNotifier(_startSize!);
|
_scaledSizeNotifier = ValueNotifier(_startSize!);
|
||||||
|
|
||||||
|
@ -143,7 +141,6 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
itemBuilder: widget.mosaicItemBuilder,
|
itemBuilder: widget.mosaicItemBuilder,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
case TileLayout.list:
|
case TileLayout.list:
|
||||||
final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
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!);
|
Overlay.of(scrollableContext).insert(_overlayEntry!);
|
||||||
}
|
}
|
||||||
|
@ -176,11 +172,9 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
|
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
|
||||||
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
|
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
|
||||||
break;
|
|
||||||
case TileLayout.list:
|
case TileLayout.list:
|
||||||
final scaledHeight = (_startSize!.height * s).clamp(_extentMin!, _extentMax!);
|
final scaledHeight = (_startSize!.height * s).clamp(_extentMin!, _extentMax!);
|
||||||
_scaledSizeNotifier!.value = Size(_startSize!.width, scaledHeight);
|
_scaledSizeNotifier!.value = Size(_startSize!.width, scaledHeight);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,10 +194,8 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
case TileLayout.mosaic:
|
case TileLayout.mosaic:
|
||||||
case TileLayout.grid:
|
case TileLayout.grid:
|
||||||
preferredExtent = _scaledSizeNotifier!.value.width;
|
preferredExtent = _scaledSizeNotifier!.value.width;
|
||||||
break;
|
|
||||||
case TileLayout.list:
|
case TileLayout.list:
|
||||||
preferredExtent = _scaledSizeNotifier!.value.height;
|
preferredExtent = _scaledSizeNotifier!.value.height;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
final newExtent = tileExtentController.setUserPreferredExtent(preferredExtent);
|
final newExtent = tileExtentController.setUserPreferredExtent(preferredExtent);
|
||||||
_scaledSizeNotifier = null;
|
_scaledSizeNotifier = null;
|
||||||
|
|
|
@ -5,7 +5,7 @@ class FixedExtentGridRow extends MultiChildRenderObjectWidget {
|
||||||
final double width, height, spacing;
|
final double width, height, spacing;
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
|
|
||||||
FixedExtentGridRow({
|
const FixedExtentGridRow({
|
||||||
super.key,
|
super.key,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
|
|
|
@ -49,7 +49,6 @@ class FixedExtentGridPainter extends CustomPainter {
|
||||||
1,
|
1,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case TileLayout.list:
|
case TileLayout.list:
|
||||||
chipSize = Size.square(tileSize.shortestSide);
|
chipSize = Size.square(tileSize.shortestSide);
|
||||||
final chipCenterToEdge = chipSize.width / 2;
|
final chipCenterToEdge = chipSize.width / 2;
|
||||||
|
@ -71,7 +70,6 @@ class FixedExtentGridPainter extends CustomPainter {
|
||||||
1,
|
1,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
final strokePaint = Paint()
|
final strokePaint = Paint()
|
||||||
..style = PaintingStyle.stroke
|
..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/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class FixedExtentScaleOverlay extends StatelessWidget {
|
class FixedExtentScaleOverlay extends StatelessWidget {
|
||||||
final TileLayout tileLayout;
|
final TileLayout tileLayout;
|
||||||
|
@ -105,7 +104,7 @@ class _OverlayBackgroundState extends State<_OverlayBackground> {
|
||||||
return _initialized
|
return _initialized
|
||||||
? BoxDecoration(
|
? BoxDecoration(
|
||||||
gradient: RadialGradient(
|
gradient: RadialGradient(
|
||||||
center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select<MediaQueryData, Size>((mq) => mq.size)),
|
center: FractionalOffset.fromOffsetAndSize(gradientCenter, MediaQuery.sizeOf(context)),
|
||||||
radius: 1,
|
radius: 1,
|
||||||
colors: isDark
|
colors: isDark
|
||||||
? const [
|
? const [
|
||||||
|
|
|
@ -8,7 +8,7 @@ class MosaicGridRow extends MultiChildRenderObjectWidget {
|
||||||
final double spacing;
|
final double spacing;
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
|
|
||||||
MosaicGridRow({
|
const MosaicGridRow({
|
||||||
super.key,
|
super.key,
|
||||||
required this.rowLayout,
|
required this.rowLayout,
|
||||||
required this.spacing,
|
required this.spacing,
|
||||||
|
|
|
@ -32,73 +32,68 @@ class AvesAppBar extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
final useTvLayout = settings.useTvLayout;
|
final useTvLayout = settings.useTvLayout;
|
||||||
return Selector<MediaQueryData, double>(
|
return SliverPersistentHeader(
|
||||||
selector: (context, mq) => mq.padding.top,
|
floating: !useTvLayout,
|
||||||
builder: (context, mqPaddingTop, child) {
|
pinned: pinned,
|
||||||
return SliverPersistentHeader(
|
delegate: _SliverAppBarDelegate(
|
||||||
floating: !useTvLayout,
|
height: MediaQuery.paddingOf(context).top + appBarHeightForContentHeight(contentHeight),
|
||||||
pinned: pinned,
|
child: DirectionalSafeArea(
|
||||||
delegate: _SliverAppBarDelegate(
|
start: !useTvLayout,
|
||||||
height: mqPaddingTop + appBarHeightForContentHeight(contentHeight),
|
bottom: false,
|
||||||
child: child!,
|
child: AvesFloatingBar(
|
||||||
),
|
builder: (context, backgroundColor, child) => Material(
|
||||||
);
|
color: backgroundColor,
|
||||||
},
|
child: child,
|
||||||
child: DirectionalSafeArea(
|
),
|
||||||
start: !useTvLayout,
|
child: Column(
|
||||||
bottom: false,
|
children: [
|
||||||
child: AvesFloatingBar(
|
SizedBox(
|
||||||
builder: (context, backgroundColor, child) => Material(
|
height: kToolbarHeight * textScaleFactor,
|
||||||
color: backgroundColor,
|
child: Row(
|
||||||
child: child,
|
children: [
|
||||||
),
|
leading != null
|
||||||
child: Column(
|
? Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
SizedBox(
|
child: Hero(
|
||||||
height: kToolbarHeight * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor),
|
tag: leadingHeroTag,
|
||||||
child: Row(
|
flightShuttleBuilder: _flightShuttleBuilder,
|
||||||
children: [
|
transitionOnUserGestures: true,
|
||||||
leading != null
|
child: FontSizeIconTheme(
|
||||||
? Padding(
|
child: leading!,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
),
|
||||||
child: Hero(
|
|
||||||
tag: leadingHeroTag,
|
|
||||||
flightShuttleBuilder: _flightShuttleBuilder,
|
|
||||||
transitionOnUserGestures: true,
|
|
||||||
child: FontSizeIconTheme(
|
|
||||||
child: leading!,
|
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
: const SizedBox(width: 16),
|
||||||
: const SizedBox(width: 16),
|
Expanded(
|
||||||
Expanded(
|
child: DefaultTextStyle(
|
||||||
child: DefaultTextStyle(
|
style: Theme.of(context).appBarTheme.titleTextStyle!,
|
||||||
style: Theme.of(context).appBarTheme.titleTextStyle!,
|
child: Hero(
|
||||||
child: Hero(
|
tag: titleHeroTag,
|
||||||
tag: titleHeroTag,
|
flightShuttleBuilder: _flightShuttleBuilder,
|
||||||
flightShuttleBuilder: _flightShuttleBuilder,
|
transitionOnUserGestures: true,
|
||||||
transitionOnUserGestures: true,
|
child: AnimatedSwitcher(
|
||||||
child: AnimatedSwitcher(
|
duration: context.read<DurationsData>().iconAnimation,
|
||||||
duration: context.read<DurationsData>().iconAnimation,
|
child: FontSizeIconTheme(
|
||||||
child: FontSizeIconTheme(
|
child: Row(
|
||||||
child: Row(
|
key: ValueKey(transitionKey),
|
||||||
key: ValueKey(transitionKey),
|
children: [
|
||||||
children: [
|
Expanded(child: title),
|
||||||
Expanded(child: title),
|
...actions,
|
||||||
...actions,
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
if (bottom != null) bottom!,
|
||||||
if (bottom != null) bottom!,
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -31,12 +31,12 @@ enum HeroType { always, onTap, never }
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class AvesFilterDecoration {
|
class AvesFilterDecoration {
|
||||||
final Widget widget;
|
|
||||||
final Radius radius;
|
final Radius radius;
|
||||||
|
final Widget widget;
|
||||||
|
|
||||||
const AvesFilterDecoration({
|
const AvesFilterDecoration({
|
||||||
required this.widget,
|
|
||||||
required this.radius,
|
required this.radius,
|
||||||
|
required this.widget,
|
||||||
});
|
});
|
||||||
|
|
||||||
BorderRadius get textBorderRadius => BorderRadius.vertical(bottom: radius);
|
BorderRadius get textBorderRadius => BorderRadius.vertical(bottom: radius);
|
||||||
|
@ -88,9 +88,9 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
required double chipPadding,
|
required double chipPadding,
|
||||||
required double rowPadding,
|
required double rowPadding,
|
||||||
}) {
|
}) {
|
||||||
return context.select<MediaQueryData, double>((mq) {
|
final mqWidth = MediaQuery.sizeOf(context).width;
|
||||||
return (mq.size.width - mq.padding.horizontal - chipPadding * minChipPerRow - rowPadding) / minChipPerRow;
|
final mqHorizontalPadding = MediaQuery.paddingOf(context).horizontal;
|
||||||
});
|
return (mqWidth - mqHorizontalPadding - chipPadding * minChipPerRow - rowPadding) / minChipPerRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async {
|
static Future<void> showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async {
|
||||||
|
|
|
@ -37,7 +37,7 @@ class AvesLogo extends StatelessWidget {
|
||||||
radius: size / 2,
|
radius: size / 2,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
radius: size / 2 - AvesBorder.curvedBorderWidth,
|
radius: size / 2 - AvesBorder.curvedBorderWidth(context),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(top: size / 15),
|
padding: EdgeInsets.only(top: size / 15),
|
||||||
child: child,
|
child: child,
|
||||||
|
|
|
@ -73,7 +73,7 @@ class _OverlayButtonState extends State<OverlayButton> {
|
||||||
builder: (context, focused, child) {
|
builder: (context, focused, child) {
|
||||||
final border = AvesBorder.border(
|
final border = AvesBorder.border(
|
||||||
context,
|
context,
|
||||||
width: AvesBorder.curvedBorderWidth * (focused ? 3 : 1),
|
width: AvesBorder.curvedBorderWidth(context) * (focused ? 3 : 1),
|
||||||
);
|
);
|
||||||
return borderRadius != null
|
return borderRadius != null
|
||||||
? BlurredRRect(
|
? BlurredRRect(
|
||||||
|
|
|
@ -42,7 +42,6 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case MapNavigationButton.map:
|
case MapNavigationButton.map:
|
||||||
if (openMapPage != null) {
|
if (openMapPage != null) {
|
||||||
navigationButton = MapOverlayButton(
|
navigationButton = MapOverlayButton(
|
||||||
|
@ -51,7 +50,6 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
tooltip: context.l10n.openMapPageTooltip,
|
tooltip: context.l10n.openMapPageTooltip,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case MapNavigationButton.none:
|
case MapNavigationButton.none:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/images.dart';
|
import 'package:aves/model/entry/extensions/images.dart';
|
||||||
|
@ -35,6 +34,7 @@ class GeoMap extends StatefulWidget {
|
||||||
final AvesMapController? controller;
|
final AvesMapController? controller;
|
||||||
final Listenable? collectionListenable;
|
final Listenable? collectionListenable;
|
||||||
final List<AvesEntry> entries;
|
final List<AvesEntry> entries;
|
||||||
|
final Size availableSize;
|
||||||
final LatLng? initialCenter;
|
final LatLng? initialCenter;
|
||||||
final ValueNotifier<bool> isAnimatingNotifier;
|
final ValueNotifier<bool> isAnimatingNotifier;
|
||||||
final ValueNotifier<LatLng?>? dotLocationNotifier;
|
final ValueNotifier<LatLng?>? dotLocationNotifier;
|
||||||
|
@ -60,6 +60,7 @@ class GeoMap extends StatefulWidget {
|
||||||
this.controller,
|
this.controller,
|
||||||
this.collectionListenable,
|
this.collectionListenable,
|
||||||
required this.entries,
|
required this.entries,
|
||||||
|
required this.availableSize,
|
||||||
this.initialCenter,
|
this.initialCenter,
|
||||||
required this.isAnimatingNotifier,
|
required this.isAnimatingNotifier,
|
||||||
this.dotLocationNotifier,
|
this.dotLocationNotifier,
|
||||||
|
@ -133,6 +134,13 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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?>(
|
return Selector<Settings, EntryMapStyle?>(
|
||||||
selector: (context, s) => s.mapStyle,
|
selector: (context, s) => s.mapStyle,
|
||||||
builder: (context, mapStyle, child) {
|
builder: (context, mapStyle, child) {
|
||||||
|
@ -143,6 +151,7 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
buildThumbnailImage: (extent) => ThumbnailImage(
|
buildThumbnailImage: (extent) => ThumbnailImage(
|
||||||
entry: key.entry,
|
entry: key.entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
|
devicePixelRatio: devicePixelRatio,
|
||||||
progressive: !isHeavy,
|
progressive: !isHeavy,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -173,9 +182,8 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
onUserZoomChange: widget.onUserZoomChange,
|
onUserZoomChange: widget.onUserZoomChange,
|
||||||
onMapTap: widget.onMapTap,
|
onMapTap: widget.onMapTap,
|
||||||
onMarkerTap: _onMarkerTap,
|
onMarkerTap: _onMarkerTap,
|
||||||
onMarkerLongPress: _onMarkerLongPress,
|
onMarkerLongPress: onMarkerLongPress,
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case EntryMapStyle.osmHot:
|
case EntryMapStyle.osmHot:
|
||||||
case EntryMapStyle.stamenToner:
|
case EntryMapStyle.stamenToner:
|
||||||
case EntryMapStyle.stamenWatercolor:
|
case EntryMapStyle.stamenWatercolor:
|
||||||
|
@ -204,9 +212,8 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
onUserZoomChange: widget.onUserZoomChange,
|
onUserZoomChange: widget.onUserZoomChange,
|
||||||
onMapTap: widget.onMapTap,
|
onMapTap: widget.onMapTap,
|
||||||
onMarkerTap: _onMarkerTap,
|
onMarkerTap: _onMarkerTap,
|
||||||
onMarkerLongPress: _onMarkerLongPress,
|
onMarkerLongPress: onMarkerLongPress,
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final overlay = Center(
|
final overlay = Center(
|
||||||
|
@ -360,8 +367,8 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
);
|
);
|
||||||
bounds = bounds.copyWith(zoom: max(minInitialZoom, bounds.zoom.floorToDouble()));
|
bounds = bounds.copyWith(zoom: max(minInitialZoom, bounds.zoom.floorToDouble()));
|
||||||
|
|
||||||
final availableSize = window.physicalSize / window.devicePixelRatio;
|
|
||||||
final neededSize = bounds.toDisplaySize();
|
final neededSize = bounds.toDisplaySize();
|
||||||
|
final availableSize = widget.availableSize;
|
||||||
if (neededSize.width > availableSize.width || neededSize.height > availableSize.height) {
|
if (neededSize.width > availableSize.width || neededSize.height > availableSize.height) {
|
||||||
return _initBoundsForEntries(entries: entries, recentCount: (recentCount ?? 10000) ~/ 10);
|
return _initBoundsForEntries(entries: entries, recentCount: (recentCount ?? 10000) ~/ 10);
|
||||||
}
|
}
|
||||||
|
@ -457,7 +464,11 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
onTap(markerLocation, markerEntry);
|
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;
|
final onMarkerLongPress = widget.onMarkerLongPress;
|
||||||
if (onMarkerLongPress == null) return;
|
if (onMarkerLongPress == null) return;
|
||||||
|
|
||||||
|
@ -478,6 +489,7 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
buildThumbnailImage: (extent) => ThumbnailImage(
|
buildThumbnailImage: (extent) => ThumbnailImage(
|
||||||
entry: markerEntry,
|
entry: markerEntry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
|
devicePixelRatio: devicePixelRatio,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
onMarkerLongPress(
|
onMarkerLongPress(
|
||||||
|
|
|
@ -82,7 +82,6 @@ class ScaleLayerWidget extends StatelessWidget {
|
||||||
// meters
|
// meters
|
||||||
distanceMeters = scaleMeters[scaleLevel];
|
distanceMeters = scaleMeters[scaleLevel];
|
||||||
displayDistance = distanceMeters >= metersInAKilometer ? '${(distanceMeters / metersInAKilometer).toStringAsFixed(0)} km' : '${distanceMeters.toStringAsFixed(0)} m';
|
displayDistance = distanceMeters >= metersInAKilometer ? '${(distanceMeters / metersInAKilometer).toStringAsFixed(0)} km' : '${distanceMeters.toStringAsFixed(0)} m';
|
||||||
break;
|
|
||||||
case UnitSystem.imperial:
|
case UnitSystem.imperial:
|
||||||
if (scaleLevel < 15) {
|
if (scaleLevel < 15) {
|
||||||
// miles
|
// miles
|
||||||
|
@ -95,7 +94,6 @@ class ScaleLayerWidget extends StatelessWidget {
|
||||||
distanceMeters = distanceFeet * metersInAFoot;
|
distanceMeters = distanceFeet * metersInAFoot;
|
||||||
displayDistance = '${distanceFeet.toStringAsFixed(0)} ft';
|
displayDistance = '${distanceFeet.toStringAsFixed(0)} ft';
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final start = map.project(center);
|
final start = map.project(center);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
const _tileLayerBackgroundColor = Colors.transparent;
|
const _tileLayerBackgroundColor = Colors.transparent;
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ class OSMHotLayer extends StatelessWidget {
|
||||||
urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
|
urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
|
||||||
subdomains: const ['a', 'b', 'c'],
|
subdomains: const ['a', 'b', 'c'],
|
||||||
backgroundColor: _tileLayerBackgroundColor,
|
backgroundColor: _tileLayerBackgroundColor,
|
||||||
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
|
retinaMode: MediaQuery.devicePixelRatioOf(context) > 1,
|
||||||
userAgentPackageName: device.userAgent,
|
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',
|
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png',
|
||||||
subdomains: const ['a', 'b', 'c', 'd'],
|
subdomains: const ['a', 'b', 'c', 'd'],
|
||||||
backgroundColor: _tileLayerBackgroundColor,
|
backgroundColor: _tileLayerBackgroundColor,
|
||||||
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
|
retinaMode: MediaQuery.devicePixelRatioOf(context) > 1,
|
||||||
userAgentPackageName: device.userAgent,
|
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',
|
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg',
|
||||||
subdomains: const ['a', 'b', 'c', 'd'],
|
subdomains: const ['a', 'b', 'c', 'd'],
|
||||||
backgroundColor: _tileLayerBackgroundColor,
|
backgroundColor: _tileLayerBackgroundColor,
|
||||||
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
|
retinaMode: MediaQuery.devicePixelRatioOf(context) > 1,
|
||||||
userAgentPackageName: device.userAgent,
|
userAgentPackageName: device.userAgent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,13 +26,10 @@ class MapActionDelegate {
|
||||||
),
|
),
|
||||||
onSelection: (v) => settings.mapStyle = v,
|
onSelection: (v) => settings.mapStyle = v,
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case MapAction.zoomIn:
|
case MapAction.zoomIn:
|
||||||
controller?.zoomBy(1);
|
controller?.zoomBy(1);
|
||||||
break;
|
|
||||||
case MapAction.zoomOut:
|
case MapAction.zoomOut:
|
||||||
controller?.zoomBy(-1);
|
controller?.zoomBy(-1);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,13 +114,11 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
key: const ValueKey<SearchBody>(SearchBody.suggestions),
|
key: const ValueKey<SearchBody>(SearchBody.suggestions),
|
||||||
child: widget.delegate.buildSuggestions(context),
|
child: widget.delegate.buildSuggestions(context),
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case SearchBody.results:
|
case SearchBody.results:
|
||||||
body = KeyedSubtree(
|
body = KeyedSubtree(
|
||||||
key: const ValueKey<SearchBody>(SearchBody.results),
|
key: const ValueKey<SearchBody>(SearchBody.results),
|
||||||
child: widget.delegate.buildResults(context),
|
child: widget.delegate.buildResults(context),
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case null:
|
case null:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
final Object? Function()? heroTagger;
|
final Object? Function()? heroTagger;
|
||||||
|
|
||||||
static final Color borderColor = Colors.grey.shade700;
|
static final Color borderColor = Colors.grey.shade700;
|
||||||
static final double borderWidth = AvesBorder.straightBorderWidth;
|
|
||||||
|
static double borderWidth(BuildContext context) => AvesBorder.straightBorderWidth(context);
|
||||||
|
|
||||||
const DecoratedThumbnail({
|
const DecoratedThumbnail({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -35,6 +36,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
Widget child = ThumbnailImage(
|
Widget child = ThumbnailImage(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: tileExtent,
|
extent: tileExtent,
|
||||||
|
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
|
||||||
isMosaic: isMosaic,
|
isMosaic: isMosaic,
|
||||||
cancellableNotifier: cancellableNotifier,
|
cancellableNotifier: cancellableNotifier,
|
||||||
heroTag: heroTagger?.call(),
|
heroTag: heroTagger?.call(),
|
||||||
|
@ -64,7 +66,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
foregroundDecoration: BoxDecoration(
|
foregroundDecoration: BoxDecoration(
|
||||||
border: Border.fromBorderSide(BorderSide(
|
border: Border.fromBorderSide(BorderSide(
|
||||||
color: borderColor,
|
color: borderColor,
|
||||||
width: borderWidth,
|
width: borderWidth(context),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
width: thumbnailWidth,
|
width: thumbnailWidth,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
@ -20,7 +19,7 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ThumbnailImage extends StatefulWidget {
|
class ThumbnailImage extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final double extent;
|
final double extent, devicePixelRatio;
|
||||||
final bool isMosaic, progressive;
|
final bool isMosaic, progressive;
|
||||||
final BoxFit? fit;
|
final BoxFit? fit;
|
||||||
final bool showLoadingBackground;
|
final bool showLoadingBackground;
|
||||||
|
@ -31,6 +30,7 @@ class ThumbnailImage extends StatefulWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.extent,
|
required this.extent,
|
||||||
|
required this.devicePixelRatio,
|
||||||
this.progressive = true,
|
this.progressive = true,
|
||||||
this.isMosaic = false,
|
this.isMosaic = false,
|
||||||
this.fit,
|
this.fit,
|
||||||
|
@ -57,7 +57,6 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
ImageInfo? _lastImageInfo;
|
ImageInfo? _lastImageInfo;
|
||||||
Object? _lastException;
|
Object? _lastException;
|
||||||
late final ImageStreamListener _streamListener;
|
late final ImageStreamListener _streamListener;
|
||||||
late DisposableBuildContext<State<ThumbnailImage>> _scrollAwareContext;
|
|
||||||
|
|
||||||
AvesEntry get entry => widget.entry;
|
AvesEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
@ -69,7 +68,6 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_streamListener = ImageStreamListener(_onImageLoad, onError: _onError);
|
_streamListener = ImageStreamListener(_onImageLoad, onError: _onError);
|
||||||
_scrollAwareContext = DisposableBuildContext<State<ThumbnailImage>>(this);
|
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +83,6 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
_scrollAwareContext.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,16 +123,10 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
_providers.addAll([
|
_providers.addAll([
|
||||||
if (lowQuality != null)
|
if (lowQuality != null)
|
||||||
_ConditionalImageProvider(
|
_ConditionalImageProvider(
|
||||||
ScrollAwareImageProvider(
|
lowQuality,
|
||||||
context: _scrollAwareContext,
|
|
||||||
imageProvider: lowQuality,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
_ConditionalImageProvider(
|
_ConditionalImageProvider(
|
||||||
ScrollAwareImageProvider(
|
highQuality,
|
||||||
context: _scrollAwareContext,
|
|
||||||
imageProvider: highQuality,
|
|
||||||
),
|
|
||||||
_needSizedProvider,
|
_needSizedProvider,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
@ -176,8 +167,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
bool _needSizedProvider(ImageInfo? currentImageInfo) {
|
bool _needSizedProvider(ImageInfo? currentImageInfo) {
|
||||||
if (currentImageInfo == null) return true;
|
if (currentImageInfo == null) return true;
|
||||||
final currentImage = currentImageInfo.image;
|
final currentImage = currentImageInfo.image;
|
||||||
// directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery`
|
final sizedThreshold = extent * widget.devicePixelRatio;
|
||||||
final sizedThreshold = extent * window.devicePixelRatio;
|
|
||||||
return sizedThreshold > min(currentImage.width, currentImage.height);
|
return sizedThreshold > min(currentImage.width, currentImage.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -182,18 +182,15 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
||||||
}, false);
|
}, false);
|
||||||
await favourites.clear();
|
await favourites.clear();
|
||||||
await favourites.add(source.visibleEntries);
|
await favourites.add(source.visibleEntries);
|
||||||
break;
|
|
||||||
case AppDebugAction.prepScreenshotStats:
|
case AppDebugAction.prepScreenshotStats:
|
||||||
settings.changeFilterVisibility(settings.hiddenFilters, true);
|
settings.changeFilterVisibility(settings.hiddenFilters, true);
|
||||||
settings.changeFilterVisibility({
|
settings.changeFilterVisibility({
|
||||||
PathFilter('/storage/emulated/0/Pictures/Dev'),
|
PathFilter('/storage/emulated/0/Pictures/Dev'),
|
||||||
}, false);
|
}, false);
|
||||||
break;
|
|
||||||
case AppDebugAction.prepScreenshotCountries:
|
case AppDebugAction.prepScreenshotCountries:
|
||||||
settings.changeFilterVisibility({
|
settings.changeFilterVisibility({
|
||||||
LocationFilter(LocationLevel.country, 'Belgium;BE'),
|
LocationFilter(LocationLevel.country, 'Belgium;BE'),
|
||||||
}, false);
|
}, false);
|
||||||
break;
|
|
||||||
case AppDebugAction.mediaStoreScanDir:
|
case AppDebugAction.mediaStoreScanDir:
|
||||||
// scan files copied from test assets
|
// scan files copied from test assets
|
||||||
// we do it via the app instead of broadcasting via ADB
|
// we do it via the app instead of broadcasting via ADB
|
||||||
|
@ -202,7 +199,6 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const MediaStoreScanDirDialog(),
|
builder: (context) => const MediaStoreScanDirDialog(),
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
case AppDebugAction.greenScreen:
|
case AppDebugAction.greenScreen:
|
||||||
await Navigator.maybeOf(context)?.push(
|
await Navigator.maybeOf(context)?.push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
@ -212,7 +208,6 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ class DebugSettingsSection extends StatelessWidget {
|
||||||
'recentDestinationAlbums': toMultiline(settings.recentDestinationAlbums),
|
'recentDestinationAlbums': toMultiline(settings.recentDestinationAlbums),
|
||||||
'recentTags': toMultiline(settings.recentTags),
|
'recentTags': toMultiline(settings.recentTags),
|
||||||
'locale': '${settings.locale}',
|
'locale': '${settings.locale}',
|
||||||
'systemLocales': '${WidgetsBinding.instance.window.locales}',
|
'systemLocales': '${WidgetsBinding.instance.platformDispatcher.locales}',
|
||||||
'topEntryIds': '${settings.topEntryIds}',
|
'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:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
import 'aves_dialog.dart';
|
import 'aves_dialog.dart';
|
||||||
|
@ -60,7 +59,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
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);
|
final extent = (shortestSide / 3.0).clamp(60.0, 160.0);
|
||||||
return AvesDialog(
|
return AvesDialog(
|
||||||
scrollableContent: [
|
scrollableContent: [
|
||||||
|
|
|
@ -72,16 +72,12 @@ void _skipConfirmation(ConfirmationDialog type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ConfirmationDialog.createVault:
|
case ConfirmationDialog.createVault:
|
||||||
settings.confirmCreateVault = false;
|
settings.confirmCreateVault = false;
|
||||||
break;
|
|
||||||
case ConfirmationDialog.deleteForever:
|
case ConfirmationDialog.deleteForever:
|
||||||
settings.confirmDeleteForever = false;
|
settings.confirmDeleteForever = false;
|
||||||
break;
|
|
||||||
case ConfirmationDialog.moveToBin:
|
case ConfirmationDialog.moveToBin:
|
||||||
settings.confirmMoveToBin = false;
|
settings.confirmMoveToBin = false;
|
||||||
break;
|
|
||||||
case ConfirmationDialog.moveUndatedItems:
|
case ConfirmationDialog.moveUndatedItems:
|
||||||
settings.confirmMoveUndatedItems = false;
|
settings.confirmMoveUndatedItems = false;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:ui';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class AvesDialog extends StatelessWidget {
|
class AvesDialog extends StatelessWidget {
|
||||||
static const confirmationRouteName = '/dialog/confirmation';
|
static const confirmationRouteName = '/dialog/confirmation';
|
||||||
|
@ -104,7 +103,7 @@ class AvesDialog extends StatelessWidget {
|
||||||
// workaround because the dialog tries
|
// workaround because the dialog tries
|
||||||
// to size itself to the content intrinsic size,
|
// to size itself to the content intrinsic size,
|
||||||
// but the `ListView` viewport does not have one
|
// 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(
|
child: DecoratedBox(
|
||||||
decoration: contentDecoration(context),
|
decoration: contentDecoration(context),
|
||||||
child: child,
|
child: child,
|
||||||
|
|
|
@ -78,11 +78,9 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
||||||
final displaySize = entries.first.displaySize;
|
final displaySize = entries.first.displaySize;
|
||||||
_widthController.text = '${displaySize.width.round()}';
|
_widthController.text = '${displaySize.width.round()}';
|
||||||
_heightController.text = '${displaySize.height.round()}';
|
_heightController.text = '${displaySize.height.round()}';
|
||||||
break;
|
|
||||||
case LengthUnit.percent:
|
case LengthUnit.percent:
|
||||||
_widthController.text = '100';
|
_widthController.text = '100';
|
||||||
_heightController.text = '100';
|
_heightController.text = '100';
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,10 +147,8 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
||||||
switch (_lengthUnit) {
|
switch (_lengthUnit) {
|
||||||
case LengthUnit.px:
|
case LengthUnit.px:
|
||||||
_heightController.text = '${(width / entries.first.displayAspectRatio).round()}';
|
_heightController.text = '${(width / entries.first.displayAspectRatio).round()}';
|
||||||
break;
|
|
||||||
case LengthUnit.percent:
|
case LengthUnit.percent:
|
||||||
_heightController.text = '$width';
|
_heightController.text = '$width';
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_heightController.text = '';
|
_heightController.text = '';
|
||||||
|
@ -175,10 +171,8 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
||||||
switch (_lengthUnit) {
|
switch (_lengthUnit) {
|
||||||
case LengthUnit.px:
|
case LengthUnit.px:
|
||||||
_widthController.text = '${(height * entries.first.displayAspectRatio).round()}';
|
_widthController.text = '${(height * entries.first.displayAspectRatio).round()}';
|
||||||
break;
|
|
||||||
case LengthUnit.percent:
|
case LengthUnit.percent:
|
||||||
_widthController.text = '$height';
|
_widthController.text = '$height';
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_widthController.text = '';
|
_widthController.text = '';
|
||||||
|
|
|
@ -145,7 +145,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
Widget _buildSetCustomContent(BuildContext context) {
|
Widget _buildSetCustomContent(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final locale = l10n.localeName;
|
final locale = l10n.localeName;
|
||||||
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
final use24hour = MediaQuery.alwaysUse24HourFormatOf(context);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
||||||
|
@ -179,7 +179,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
Widget _buildCopyItemContent(BuildContext context) {
|
Widget _buildCopyItemContent(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final locale = l10n.localeName;
|
final locale = l10n.localeName;
|
||||||
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
final use24hour = MediaQuery.alwaysUse24HourFormatOf(context);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
||||||
|
@ -365,11 +365,9 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
case DateEditAction.copyItem:
|
case DateEditAction.copyItem:
|
||||||
case DateEditAction.extractFromTitle:
|
case DateEditAction.extractFromTitle:
|
||||||
_isValidNotifier.value = true;
|
_isValidNotifier.value = true;
|
||||||
break;
|
|
||||||
case DateEditAction.shift:
|
case DateEditAction.shift:
|
||||||
case DateEditAction.remove:
|
case DateEditAction.remove:
|
||||||
_isValidNotifier.value = _fields.isNotEmpty;
|
_isValidNotifier.value = _fields.isNotEmpty;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -307,33 +307,26 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
switch (_action) {
|
switch (_action) {
|
||||||
case LocationEditAction.chooseOnMap:
|
case LocationEditAction.chooseOnMap:
|
||||||
_isValidNotifier.value = _mapCoordinates != null;
|
_isValidNotifier.value = _mapCoordinates != null;
|
||||||
break;
|
|
||||||
case LocationEditAction.copyItem:
|
case LocationEditAction.copyItem:
|
||||||
_isValidNotifier.value = _copyItemSource.hasGps;
|
_isValidNotifier.value = _copyItemSource.hasGps;
|
||||||
break;
|
|
||||||
case LocationEditAction.setCustom:
|
case LocationEditAction.setCustom:
|
||||||
_isValidNotifier.value = _parseLatLng() != null;
|
_isValidNotifier.value = _parseLatLng() != null;
|
||||||
break;
|
|
||||||
case LocationEditAction.remove:
|
case LocationEditAction.remove:
|
||||||
_isValidNotifier.value = true;
|
_isValidNotifier.value = true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submit(BuildContext context) {
|
void _submit(BuildContext context) {
|
||||||
|
final navigator = Navigator.maybeOf(context);
|
||||||
switch (_action) {
|
switch (_action) {
|
||||||
case LocationEditAction.chooseOnMap:
|
case LocationEditAction.chooseOnMap:
|
||||||
Navigator.maybeOf(context)?.pop(_mapCoordinates);
|
navigator?.pop(_mapCoordinates);
|
||||||
break;
|
|
||||||
case LocationEditAction.copyItem:
|
case LocationEditAction.copyItem:
|
||||||
Navigator.maybeOf(context)?.pop(_copyItemSource.latLng);
|
navigator?.pop(_copyItemSource.latLng);
|
||||||
break;
|
|
||||||
case LocationEditAction.setCustom:
|
case LocationEditAction.setCustom:
|
||||||
Navigator.maybeOf(context)?.pop(_parseLatLng());
|
navigator?.pop(_parseLatLng());
|
||||||
break;
|
|
||||||
case LocationEditAction.remove:
|
case LocationEditAction.remove:
|
||||||
Navigator.maybeOf(context)?.pop(ExtraAvesEntryMetadataEdition.removalLocation);
|
navigator?.pop(ExtraAvesEntryMetadataEdition.removalLocation);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,11 +32,9 @@ class _EditEntryRatingDialogState extends State<EditEntryRatingDialog> {
|
||||||
case -1:
|
case -1:
|
||||||
_action = _RatingAction.rejected;
|
_action = _RatingAction.rejected;
|
||||||
_rating = 0;
|
_rating = 0;
|
||||||
break;
|
|
||||||
case 0:
|
case 0:
|
||||||
_action = _RatingAction.unrated;
|
_action = _RatingAction.unrated;
|
||||||
_rating = 0;
|
_rating = 0;
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
_action = _RatingAction.set;
|
_action = _RatingAction.set;
|
||||||
_rating = entryRating;
|
_rating = entryRating;
|
||||||
|
@ -121,13 +119,10 @@ class _EditEntryRatingDialogState extends State<EditEntryRatingDialog> {
|
||||||
switch (_action) {
|
switch (_action) {
|
||||||
case _RatingAction.set:
|
case _RatingAction.set:
|
||||||
entryRating = _rating;
|
entryRating = _rating;
|
||||||
break;
|
|
||||||
case _RatingAction.rejected:
|
case _RatingAction.rejected:
|
||||||
entryRating = -1;
|
entryRating = -1;
|
||||||
break;
|
|
||||||
case _RatingAction.unrated:
|
case _RatingAction.unrated:
|
||||||
entryRating = 0;
|
entryRating = 0;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
Navigator.maybeOf(context)?.pop(entryRating);
|
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:aves/widgets/common/thumbnail/decorated.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class RenameEntrySetPage extends StatefulWidget {
|
class RenameEntrySetPage extends StatefulWidget {
|
||||||
static const routeName = '/rename_entry_set';
|
static const routeName = '/rename_entry_set';
|
||||||
|
@ -60,6 +59,8 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
|
final effectiveThumbnailExtent = max(thumbnailExtent, thumbnailExtent * textScaleFactor);
|
||||||
return AvesScaffold(
|
return AvesScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l10n.renameEntrySetPageTitle),
|
title: Text(l10n.renameEntrySetPageTitle),
|
||||||
|
@ -121,63 +122,58 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Selector<MediaQueryData, double>(
|
child: GridTheme(
|
||||||
selector: (context, mq) => mq.textScaleFactor,
|
extent: effectiveThumbnailExtent,
|
||||||
builder: (context, textScaleFactor, child) {
|
child: ListView.separated(
|
||||||
final effectiveThumbnailExtent = max(thumbnailExtent, thumbnailExtent * textScaleFactor);
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
return GridTheme(
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
extent: effectiveThumbnailExtent,
|
itemBuilder: (context, index) {
|
||||||
child: ListView.separated(
|
final entry = entries[index];
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
final sourceName = entry.filenameWithoutExtension ?? '';
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
return Row(
|
||||||
itemBuilder: (context, index) {
|
children: [
|
||||||
final entry = entries[index];
|
DecoratedThumbnail(
|
||||||
final sourceName = entry.filenameWithoutExtension ?? '';
|
entry: entry,
|
||||||
return Row(
|
tileExtent: effectiveThumbnailExtent,
|
||||||
|
selectable: false,
|
||||||
|
highlightable: false,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
DecoratedThumbnail(
|
Text(
|
||||||
entry: entry,
|
sourceName,
|
||||||
tileExtent: effectiveThumbnailExtent,
|
style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color),
|
||||||
selectable: false,
|
softWrap: false,
|
||||||
highlightable: false,
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 4),
|
||||||
Expanded(
|
ValueListenableBuilder<NamingPattern>(
|
||||||
child: Column(
|
valueListenable: _namingPatternNotifier,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, pattern, child) {
|
||||||
children: [
|
return Text(
|
||||||
Text(
|
pattern.apply(entry, index),
|
||||||
sourceName,
|
softWrap: false,
|
||||||
style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color),
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
maxLines: 1,
|
||||||
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),
|
const Divider(height: 0),
|
||||||
Center(
|
Center(
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue