diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c359adab..0fb3f7929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.11.3] - 2024-06-17 + +### Added + +- handle `MediaStore.ACTION_REVIEW` intent + ## [v1.11.2] - 2024-06-11 ### Added diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e0a82ddb2..67ad18af7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -142,6 +142,7 @@ + @@ -161,6 +162,7 @@ + diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index 5c4903840..96dd14303 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -10,6 +10,7 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper +import android.provider.MediaStore import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.pm.ShortcutInfoCompat @@ -299,6 +300,7 @@ open class MainActivity : FlutterFragmentActivity() { Intent.ACTION_VIEW, Intent.ACTION_SEND, + MediaStore.ACTION_REVIEW, "com.android.camera.action.REVIEW", "com.android.camera.action.SPLIT_SCREEN_REVIEW" -> { (intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri -> diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index a96f84e90..5ac512f91 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -340,7 +340,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { } ioScope.launch { - provider.fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback { + provider.fetchSingle(context, uri, mimeType, false, object : ImageProvider.ImageOpCallback { override fun onSuccess(fields: FieldMap) { resultFields.putAll(fields) result.success(resultFields) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchObjectHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchObjectHandler.kt index 941928831..be5696a03 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchObjectHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchObjectHandler.kt @@ -29,6 +29,7 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler private fun getEntry(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") // MIME type is optional val uri = call.argument("uri")?.let { Uri.parse(it) } + val allowUnsized = call.argument("allowUnsized") ?: false if (uri == null) { result.error("getEntry-args", "missing arguments", null) return @@ -40,7 +41,7 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler return } - provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback { + provider.fetchSingle(context, uri, mimeType, allowUnsized, object : ImageOpCallback { override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri mimeType=$mimeType", throwable.message) }) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt index cfa0c34e8..bc45c1c2c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt @@ -12,7 +12,7 @@ import deckers.thibault.aves.utils.LogUtils import java.io.File internal class FileImageProvider : ImageProvider() { - override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { + override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) { var mimeType = sourceMimeType if (mimeType == null) { @@ -54,7 +54,7 @@ internal class FileImageProvider : ImageProvider() { } entry.fillPreCatalogMetadata(context) - if (entry.isSized || entry.isSvg || entry.isVideo) { + if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) { callback.onSuccess(entry.toMap()) } else { callback.onFailure(Exception("entry has no size")) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index d406f12fd..be8dc5270 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -70,7 +70,7 @@ import java.util.TimeZone import kotlin.math.absoluteValue abstract class ImageProvider { - open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { + open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) { callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider")) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 87c166c28..faa55eef7 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -89,7 +89,7 @@ class MediaStoreImageProvider : ImageProvider() { // the provided URI can point to the wrong media collection, // e.g. a GIF image with the URI `content://media/external/video/media/[ID]` // so the effective entry URI may not match the provided URI - override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { + override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) { var found = false val fetched = arrayListOf() val id = uri.tryParseId() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt index 8fc4e6db0..ffcc2400f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt @@ -17,7 +17,7 @@ open class UnknownContentProvider : ImageProvider() { open val reliableProviderMimeType: Boolean get() = false - override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { + override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) { var mimeType = sourceMimeType if (sourceMimeType == null || !reliableProviderMimeType) { // source MIME type may be incorrect, so we get a second opinion if possible @@ -71,7 +71,7 @@ open class UnknownContentProvider : ImageProvider() { } val entry = SourceEntry(fields).fillPreCatalogMetadata(context) - if (entry.isSized || entry.isSvg || entry.isVideo) { + if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) { callback.onSuccess(entry.toMap()) } else { callback.onFailure(Exception("entry has no size")) diff --git a/android/build.gradle b/android/build.gradle index 87bd1337b..90a76ffec 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - agp_version = '8.4.1' // same as `settings.ext.agp_version` in `/android/settings.gradle` + agp_version = '8.5.0' // same as `settings.ext.agp_version` in `/android/settings.gradle` glide_version = '4.16.0' // AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550 huawei_agconnect_version = '1.9.1.300' diff --git a/android/settings.gradle b/android/settings.gradle index 293bc6423..5ad529b31 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -10,7 +10,7 @@ pluginManagement { settings.ext.kotlin_version = '1.9.24' settings.ext.ksp_version = "$kotlin_version-1.0.20" - settings.ext.agp_version = '8.4.1' + settings.ext.agp_version = '8.5.0' includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") diff --git a/fastlane/metadata/android/en-US/changelogs/122.txt b/fastlane/metadata/android/en-US/changelogs/122.txt new file mode 100644 index 000000000..dfb8b7c93 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/122.txt @@ -0,0 +1,3 @@ +In v1.11.3: +- show selected albums together in Collection +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/12201.txt b/fastlane/metadata/android/en-US/changelogs/12201.txt new file mode 100644 index 000000000..dfb8b7c93 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/12201.txt @@ -0,0 +1,3 @@ +In v1.11.3: +- show selected albums together in Collection +Full changelog available on GitHub \ No newline at end of file diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 1b1e4c318..d616dbe98 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -360,7 +360,7 @@ class MediaStoreSource extends CollectionSource { existingDirectories.add(existingDirectory); } } else { - final sourceEntry = await mediaFetchService.getEntry(uri, null); + final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true); if (sourceEntry != null) { newEntries.add(sourceEntry.copyWith( id: metadataDb.nextId, diff --git a/lib/model/source/trash.dart b/lib/model/source/trash.dart index 0249da2d4..98393b0b1 100644 --- a/lib/model/source/trash.dart +++ b/lib/model/source/trash.dart @@ -67,7 +67,7 @@ mixin TrashMixin on SourceBase { await metadataDb.updateTrash(id, entry.trashDetails); } else { // there is no matching entry - final sourceEntry = await mediaFetchService.getEntry(uri, null); + final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true); if (sourceEntry != null) { final id = metadataDb.nextId; sourceEntry.id = id; diff --git a/lib/model/vaults/vaults.dart b/lib/model/vaults/vaults.dart index 90a442dab..1491a9ec0 100644 --- a/lib/model/vaults/vaults.dart +++ b/lib/model/vaults/vaults.dart @@ -166,7 +166,7 @@ class Vaults extends ChangeNotifier { debugPrint('Recovering ${untrackedPaths.length} untracked vault items'); await Future.forEach(untrackedPaths, (untrackedPath) async { final uri = Uri.file(untrackedPath).toString(); - final sourceEntry = await mediaFetchService.getEntry(uri, null); + final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true); if (sourceEntry != null) { sourceEntry.id = metadataDb.nextId; sourceEntry.origin = EntryOrigins.vault; diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart index e249024d2..756e1d58e 100644 --- a/lib/services/media/media_fetch_service.dart +++ b/lib/services/media/media_fetch_service.dart @@ -13,7 +13,7 @@ import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; abstract class MediaFetchService { - Future getEntry(String uri, String? mimeType); + Future getEntry(String uri, String? mimeType, {bool allowUnsized = false}); Future getSvg( String uri, @@ -75,11 +75,12 @@ class PlatformMediaFetchService implements MediaFetchService { static const double _thumbnailDefaultSize = 64.0; @override - Future getEntry(String uri, String? mimeType) async { + Future getEntry(String uri, String? mimeType, {bool allowUnsized = false}) async { try { final result = await _platformObject.invokeMethod('getEntry', { 'uri': uri, 'mimeType': mimeType, + 'allowUnsized': allowUnsized, }) as Map; return AvesEntry.fromMap(result); } on PlatformException catch (e, stack) { diff --git a/lib/widgets/about/translators.dart b/lib/widgets/about/translators.dart index 2e582a454..37e4641bf 100644 --- a/lib/widgets/about/translators.dart +++ b/lib/widgets/about/translators.dart @@ -51,6 +51,7 @@ class _RandomTextSpanHighlighter extends StatefulWidget { class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> with SingleTickerProviderStateMixin { late final AnimationController _controller; + late final CurvedAnimation _animation; late final Animation _animatedStyle; late final TextStyle _baseStyle; int _highlightedIndex = 0; @@ -90,14 +91,16 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> } }) ..repeat(reverse: true); - _animatedStyle = ShadowedTextStyleTween(begin: _baseStyle, end: highlightStyle).animate(CurvedAnimation( + _animation = CurvedAnimation( parent: _controller, curve: Curves.easeInOutCubic, - )); + ); + _animatedStyle = ShadowedTextStyleTween(begin: _baseStyle, end: highlightStyle).animate(_animation); } @override void dispose() { + _animation.dispose(); _controller.dispose(); super.dispose(); } diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 08832097a..d4d54012f 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -113,11 +113,13 @@ class _CollectionGridContent extends StatefulWidget { class _CollectionGridContentState extends State<_CollectionGridContent> { final ValueNotifier _focusedItemNotifier = ValueNotifier(null); final ValueNotifier _isScrollingNotifier = ValueNotifier(false); + final ValueNotifier _selectingAppModeNotifier = ValueNotifier(AppMode.pickFilteredMediaInternal); @override void dispose() { _focusedItemNotifier.dispose(); _isScrollingNotifier.dispose(); + _selectingAppModeNotifier.dispose(); super.dispose(); } @@ -252,7 +254,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { if (selection.isSelecting) { child = MultiProvider( providers: [ - ListenableProvider>.value(value: ValueNotifier(AppMode.pickFilteredMediaInternal)), + ListenableProvider>.value(value: _selectingAppModeNotifier), ChangeNotifierProvider>.value(value: selection), ], child: child, diff --git a/lib/widgets/common/action_controls/quick_choosers/common/button.dart b/lib/widgets/common/action_controls/quick_choosers/common/button.dart index b4d409d1f..5925099b7 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/button.dart @@ -22,7 +22,7 @@ abstract class ChooserQuickButton extends StatefulWidget { abstract class ChooserQuickButtonState, U> extends State with SingleTickerProviderStateMixin { AnimationController? _animationController; - Animation? _animation; + CurvedAnimation? _animation; OverlayEntry? _chooserOverlayEntry; final ValueNotifier _chooserValueNotifier = ValueNotifier(null); final StreamController _moveUpdateStreamController = StreamController.broadcast(); @@ -47,6 +47,7 @@ abstract class ChooserQuickButtonState, U> exten @override void dispose() { + _animation?.dispose(); _animationController?.dispose(); _clearChooserOverlayEntry(); _chooserValueNotifier.dispose(); diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index ad348f983..ef145603f 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -178,7 +178,7 @@ class ReportOverlay extends StatefulWidget { class _ReportOverlayState extends State> with SingleTickerProviderStateMixin { final processed = {}; late AnimationController _animationController; - late Animation _animation; + late CurvedAnimation _animation; Stream get opStream => widget.opStream; @@ -212,6 +212,7 @@ class _ReportOverlayState extends State> with SingleTickerPr @override void dispose() { + _animation.dispose(); _animationController.dispose(); super.dispose(); } @@ -317,6 +318,7 @@ class _FeedbackMessage extends StatefulWidget { class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerProviderStateMixin { AnimationController? _animationController; + CurvedAnimation? _animation; Animation? _remainingDurationMillis; int? _totalDurationMillis; @@ -333,19 +335,21 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro duration: effectiveDuration, vsync: this, ); + _animation = CurvedAnimation( + parent: _animationController!, + curve: Curves.linear, + ); _remainingDurationMillis = IntTween( begin: effectiveDuration.inMilliseconds, end: 0, - ).animate(CurvedAnimation( - parent: _animationController!, - curve: Curves.linear, - )); + ).animate(_animation!); _animationController!.forward(); } } @override void dispose() { + _animation?.dispose(); _animationController?.dispose(); super.dispose(); } diff --git a/lib/widgets/common/action_mixins/overlay_snack_bar.dart b/lib/widgets/common/action_mixins/overlay_snack_bar.dart index 97a71a97c..ed2d5a710 100644 --- a/lib/widgets/common/action_mixins/overlay_snack_bar.dart +++ b/lib/widgets/common/action_mixins/overlay_snack_bar.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; // adapted from Flutter `SnackBar` in `/material/snack_bar.dart` -// As of Flutter v3.0.1, `SnackBar` is not customizable enough to add margin +// As of Flutter v3.23.0, `SnackBar` is not customizable enough to add margin // and ignore pointers in that area, so we use an overlay entry instead. // This overlay entry is not under a `Scaffold` (which is expected by `SnackBar` // and `SnackBarAction`), and is not dismissed the same way. @@ -73,10 +73,17 @@ class OverlaySnackBar extends StatefulWidget { class _OverlaySnackBarState extends State { bool _wasVisible = false; + CurvedAnimation? _heightAnimation; + CurvedAnimation? _fadeInAnimation; + CurvedAnimation? _fadeInM3Animation; + CurvedAnimation? _fadeOutAnimation; + CurvedAnimation? _heightM3Animation; + @override void initState() { super.initState(); widget.animation!.addStatusListener(_onAnimationStatusChanged); + _setAnimations(); } @override @@ -85,26 +92,55 @@ class _OverlaySnackBarState extends State { if (widget.animation != oldWidget.animation) { oldWidget.animation!.removeStatusListener(_onAnimationStatusChanged); widget.animation!.addStatusListener(_onAnimationStatusChanged); + _disposeAnimations(); + _setAnimations(); } } + void _setAnimations() { + assert(widget.animation != null); + _heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarHeightCurve); + _fadeInAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarFadeInCurve); + _fadeInM3Animation = CurvedAnimation(parent: widget.animation!, curve: _snackBarM3FadeInCurve); + _fadeOutAnimation = CurvedAnimation( + parent: widget.animation!, + curve: _snackBarFadeOutCurve, + reverseCurve: const Threshold(0.0), + ); + // Material 3 Animation has a height animation on entry, but a direct fade out on exit. + _heightM3Animation = CurvedAnimation( + parent: widget.animation!, + curve: _snackBarM3HeightCurve, + reverseCurve: const Threshold(0.0), + ); + } + + void _disposeAnimations() { + _heightAnimation?.dispose(); + _fadeInAnimation?.dispose(); + _fadeInM3Animation?.dispose(); + _fadeOutAnimation?.dispose(); + _heightM3Animation?.dispose(); + _heightAnimation = null; + _fadeInAnimation = null; + _fadeInM3Animation = null; + _fadeOutAnimation = null; + _heightM3Animation = null; + } + @override void dispose() { widget.animation!.removeStatusListener(_onAnimationStatusChanged); + _disposeAnimations(); super.dispose(); } void _onAnimationStatusChanged(AnimationStatus animationStatus) { - switch (animationStatus) { - case AnimationStatus.dismissed: - case AnimationStatus.forward: - case AnimationStatus.reverse: - break; - case AnimationStatus.completed: - if (widget.onVisible != null && !_wasVisible) { - widget.onVisible!(); - } - _wasVisible = true; + if (animationStatus == AnimationStatus.completed) { + if (widget.onVisible != null && !_wasVisible) { + widget.onVisible!(); + } + _wasVisible = true; } } @@ -173,28 +209,13 @@ class _OverlaySnackBarState extends State { final double iconHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 12.0; - final CurvedAnimation heightAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarHeightCurve); - final CurvedAnimation fadeInAnimation = CurvedAnimation(parent: widget.animation!, curve: _snackBarFadeInCurve); - final CurvedAnimation fadeInM3Animation = CurvedAnimation(parent: widget.animation!, curve: _snackBarM3FadeInCurve); - - final CurvedAnimation fadeOutAnimation = CurvedAnimation( - parent: widget.animation!, - curve: _snackBarFadeOutCurve, - reverseCurve: const Threshold(0.0), - ); - // Material 3 Animation has a height animation on entry, but a direct fade out on exit. - final CurvedAnimation heightM3Animation = CurvedAnimation( - parent: widget.animation!, - curve: _snackBarM3HeightCurve, - reverseCurve: const Threshold(0.0), - ); - final IconButton? iconButton = showCloseIcon ? IconButton( icon: const Icon(Icons.close), iconSize: 24.0, color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor, onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss), + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, ) : null; @@ -253,7 +274,7 @@ class _OverlaySnackBarState extends State { child: accessibleNavigation || theme.useMaterial3 ? snackBar : FadeTransition( - opacity: fadeOutAnimation, + opacity: _fadeOutAnimation!, child: snackBar, ), ), @@ -288,7 +309,7 @@ class _OverlaySnackBarState extends State { key: const Key('dismissible'), direction: widget.dismissDirection, resizeDuration: null, - behavior: widget.hitTestBehavior ?? (widget.margin != null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque), + behavior: widget.hitTestBehavior ?? (widget.margin != null || snackBarTheme.insetPadding != null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque), onDismissed: (direction) => widget.onDismiss(), child: snackBar, ), @@ -299,19 +320,19 @@ class _OverlaySnackBarState extends State { snackBarTransition = snackBar; } else if (isFloatingSnackBar && !theme.useMaterial3) { snackBarTransition = FadeTransition( - opacity: fadeInAnimation, + opacity: _fadeInAnimation!, child: snackBar, ); // Is Material 3 Floating Snack Bar. } else if (isFloatingSnackBar && theme.useMaterial3) { snackBarTransition = FadeTransition( - opacity: fadeInM3Animation, - child: AnimatedBuilder( - animation: heightM3Animation, - builder: (context, child) { + opacity: _fadeInM3Animation!, + child: ValueListenableBuilder( + valueListenable: _heightM3Animation!, + builder: (context, value, child) { return Align( - alignment: AlignmentDirectional.bottomStart, - heightFactor: heightM3Animation.value, + alignment: Alignment.bottomLeft, + heightFactor: value, child: child, ); }, @@ -319,12 +340,12 @@ class _OverlaySnackBarState extends State { ), ); } else { - snackBarTransition = AnimatedBuilder( - animation: heightAnimation, - builder: (context, child) { + snackBarTransition = ValueListenableBuilder( + valueListenable: _heightAnimation!, + builder: (context, value, child) { return Align( alignment: AlignmentDirectional.topStart, - heightFactor: heightAnimation.value, + heightFactor: value, child: child, ); }, diff --git a/lib/widgets/common/basic/draggable_scrollbar/scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar/scrollbar.dart index 40f91e8bc..eb42528bb 100644 --- a/lib/widgets/common/basic/draggable_scrollbar/scrollbar.dart +++ b/lib/widgets/common/basic/draggable_scrollbar/scrollbar.dart @@ -121,9 +121,9 @@ class _DraggableScrollbarState extends State with TickerProv late Offset _longPressLastGlobalPosition; late AnimationController _thumbAnimationController; - late Animation _thumbAnimation; + late CurvedAnimation _thumbAnimation; late AnimationController _labelAnimationController; - late Animation _labelAnimation; + late CurvedAnimation _labelAnimation; Timer? _fadeoutTimer; Map? _percentCrumbs; final Map _viewportCrumbs = {}; @@ -167,7 +167,9 @@ class _DraggableScrollbarState extends State with TickerProv @override void dispose() { + _thumbAnimation.dispose(); _thumbAnimationController.dispose(); + _labelAnimation.dispose(); _labelAnimationController.dispose(); _fadeoutTimer?.cancel(); super.dispose(); diff --git a/lib/widgets/common/basic/text/animated_diff.dart b/lib/widgets/common/basic/text/animated_diff.dart index a76726e25..fe37d428c 100644 --- a/lib/widgets/common/basic/text/animated_diff.dart +++ b/lib/widgets/common/basic/text/animated_diff.dart @@ -26,7 +26,7 @@ class AnimatedDiffText extends StatefulWidget { class _AnimatedDiffTextState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; - late final Animation _animation; + late final CurvedAnimation _animation; final List<_TextDiff> _diffs = []; TextStyle get _textStyle { @@ -66,6 +66,7 @@ class _AnimatedDiffTextState extends State with SingleTickerPr @override void dispose() { + _animation.dispose(); _controller.dispose(); super.dispose(); } diff --git a/lib/widgets/common/basic/text/change_highlight.dart b/lib/widgets/common/basic/text/change_highlight.dart index eaebecf12..cea4737a2 100644 --- a/lib/widgets/common/basic/text/change_highlight.dart +++ b/lib/widgets/common/basic/text/change_highlight.dart @@ -22,6 +22,7 @@ class ChangeHighlightText extends StatefulWidget { class _ChangeHighlightTextState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; + late final CurvedAnimation _animation; late final Animation _style; @override @@ -33,10 +34,11 @@ class _ChangeHighlightTextState extends State with SingleTi ) ..value = 1 ..addListener(() => setState(() {})); - _style = ShadowedTextStyleTween(begin: widget.changedStyle, end: widget.style).animate(CurvedAnimation( + _animation = CurvedAnimation( parent: _controller, curve: widget.curve, - )); + ); + _style = ShadowedTextStyleTween(begin: widget.changedStyle, end: widget.style).animate(_animation); } @override @@ -51,6 +53,7 @@ class _ChangeHighlightTextState extends State with SingleTi @override void dispose() { + _animation.dispose(); _controller.dispose(); super.dispose(); } diff --git a/lib/widgets/common/fx/sweeper.dart b/lib/widgets/common/fx/sweeper.dart index fc218fa3b..6ed6bdec8 100644 --- a/lib/widgets/common/fx/sweeper.dart +++ b/lib/widgets/common/fx/sweeper.dart @@ -31,6 +31,7 @@ class Sweeper extends StatefulWidget { class _SweeperState extends State with SingleTickerProviderStateMixin { late AnimationController _angleAnimationController; + late CurvedAnimation _angleAnimation; late Animation _angle; bool _isAppearing = false; @@ -46,13 +47,14 @@ class _SweeperState extends State with SingleTickerProviderStateMixin { final startAngle = widget.startAngle; final sweepAngle = widget.sweepAngle; final centerSweep = widget.centerSweep; + _angleAnimation = CurvedAnimation( + parent: _angleAnimationController, + curve: widget.curve, + ); _angle = Tween( begin: startAngle - sweepAngle * (centerSweep ? .5 : 0), end: startAngle + pi * 2 - sweepAngle * (centerSweep ? .5 : 1), - ).animate(CurvedAnimation( - parent: _angleAnimationController, - curve: widget.curve, - )); + ).animate(_angleAnimation); _angleAnimationController.addStatusListener(_onAnimationStatusChanged); _registerWidget(widget); } @@ -66,7 +68,7 @@ class _SweeperState extends State with SingleTickerProviderStateMixin { @override void dispose() { - _angleAnimationController.removeStatusListener(_onAnimationStatusChanged); + _angleAnimation.dispose(); _angleAnimationController.dispose(); _unregisterWidget(widget); super.dispose(); diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index 901ffe79e..84d87be05 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -286,9 +286,8 @@ class _EntryLeafletMapState extends State> with TickerProv final animation = CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn); controller.addListener(() => animate(animation)); animation.addStatusListener((status) { - if (status == AnimationStatus.completed) { - controller.dispose(); - } else if (status == AnimationStatus.dismissed) { + if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) { + animation.dispose(); controller.dispose(); } }); diff --git a/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart index 634341c88..ae7d7cc7e 100644 --- a/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart @@ -64,6 +64,7 @@ class _AlbumPickPage extends StatefulWidget { class _AlbumPickPageState extends State<_AlbumPickPage> { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); + final ValueNotifier _appModeNotifier = ValueNotifier(AppMode.pickFilterInternal); CollectionSource get source => widget.source; @@ -93,13 +94,14 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { @override void dispose() { _appBarHeightNotifier.dispose(); + _appModeNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableProvider>.value( - value: ValueNotifier(AppMode.pickFilterInternal), + value: _appModeNotifier, child: Selector( selector: (context, s) => (s.albumGroupFactor, s.albumSortFactor), builder: (context, s, child) { diff --git a/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart index 490f5edb7..49fe4266a 100644 --- a/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart @@ -29,20 +29,23 @@ class ItemPickPage extends StatefulWidget { } class _ItemPickPageState extends State { + final ValueNotifier _appModeNotifier = ValueNotifier(AppMode.initialization); + CollectionLens get collection => widget.collection; @override void dispose() { collection.dispose(); + _appModeNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final liveFilter = collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; - final mode = widget.canRemoveFilters ? AppMode.pickUnfilteredMediaInternal : AppMode.pickFilteredMediaInternal; + _appModeNotifier.value = widget.canRemoveFilters ? AppMode.pickUnfilteredMediaInternal : AppMode.pickFilteredMediaInternal; return ListenableProvider>.value( - value: ValueNotifier(mode), + value: _appModeNotifier, child: AvesScaffold( body: SelectionProvider( child: QueryProvider( diff --git a/lib/widgets/editor/transform/cropper.dart b/lib/widgets/editor/transform/cropper.dart index 86447f735..c3abc7338 100644 --- a/lib/widgets/editor/transform/cropper.dart +++ b/lib/widgets/editor/transform/cropper.dart @@ -43,7 +43,7 @@ class _CropperState extends State with SingleTickerProviderStateMixin { final ValueNotifier _outlineNotifier = ValueNotifier(Rect.zero); final ValueNotifier _gridDivisionNotifier = ValueNotifier(0); late AnimationController _gridAnimationController; - late Animation _gridOpacity; + late CurvedAnimation _gridOpacity; static const double minDimension = Cropper.handleDimension; static const int panResizeGridDivision = 3; @@ -87,6 +87,7 @@ class _CropperState extends State with SingleTickerProviderStateMixin { _viewportSizeNotifier.dispose(); _outlineNotifier.dispose(); _gridDivisionNotifier.dispose(); + _gridOpacity.dispose(); _gridAnimationController.dispose(); _unregisterWidget(widget); super.dispose(); diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 7d44c479a..528db5eb7 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -103,7 +103,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin final ValueNotifier _overlayOpacityNotifier = ValueNotifier(1); final ValueNotifier _overlayVisible = ValueNotifier(true); late AnimationController _overlayAnimationController; - late Animation _overlayScale, _scrollerSize; + late CurvedAnimation _overlayScale, _scrollerSize; CoordinateFilter? _regionFilter; CollectionLens? get regionCollection => _regionCollectionNotifier.value; @@ -170,6 +170,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin _dotEntryNotifier.dispose(); _overlayOpacityNotifier.dispose(); _overlayVisible.dispose(); + _overlayScale.dispose(); + _scrollerSize.dispose(); _overlayAnimationController.dispose(); // provided collection should be a new instance specifically created diff --git a/lib/widgets/navigation/nav_bar/floating.dart b/lib/widgets/navigation/nav_bar/floating.dart index 2c4f27ad7..63ffb53a4 100644 --- a/lib/widgets/navigation/nav_bar/floating.dart +++ b/lib/widgets/navigation/nav_bar/floating.dart @@ -25,7 +25,8 @@ class FloatingNavBar extends StatefulWidget { class _FloatingNavBarState extends State with SingleTickerProviderStateMixin { final List _subscriptions = []; late AnimationController _controller; - late Animation _offsetAnimation; + late CurvedAnimation _animation; + late Animation _offset; double? _lastOffset; bool _isDragging = false; @@ -36,13 +37,14 @@ class _FloatingNavBarState extends State with SingleTickerProvid duration: const Duration(milliseconds: 200), vsync: this, ); - _offsetAnimation = Tween( - begin: const Offset(0, 0), - end: const Offset(0, 1), - ).animate(CurvedAnimation( + _animation = CurvedAnimation( parent: _controller, curve: Curves.linear, - )) + ); + _offset = Tween( + begin: const Offset(0, 0), + end: const Offset(0, 1), + ).animate(_animation) ..addListener(() { if (!mounted) return; setState(() {}); @@ -63,6 +65,8 @@ class _FloatingNavBarState extends State with SingleTickerProvid @override void dispose() { _unregisterWidget(widget); + _animation.dispose(); + _controller.dispose(); super.dispose(); } @@ -82,7 +86,7 @@ class _FloatingNavBarState extends State with SingleTickerProvid @override Widget build(BuildContext context) { return SlideTransition( - position: _offsetAnimation, + position: _offset, child: widget.child, ); } diff --git a/lib/widgets/viewer/controls/controller.dart b/lib/widgets/viewer/controls/controller.dart index 0ee13ddbd..54bb798f7 100644 --- a/lib/widgets/viewer/controls/controller.dart +++ b/lib/widgets/viewer/controls/controller.dart @@ -22,7 +22,7 @@ class ViewerController with CastMixin { late final ValueNotifier _autopilotNotifier; Timer? _playTimer; final StreamController _streamController = StreamController.broadcast(); - final Map _autopilotAnimationControllers = {}; + final Map _autopilotAnimators = {}; ScaleLevel? _autopilotInitialScale; Stream get _events => _streamController.stream; @@ -94,9 +94,12 @@ class ViewerController with CastMixin { void _stopPlayTimer() => _playTimer?.cancel(); - void _clearAutopilotAnimations() => _autopilotAnimationControllers.keys.toSet().forEach((v) => stopAutopilotAnimation(vsync: v)); + void _clearAutopilotAnimations() => _autopilotAnimators.keys.toSet().forEach((v) => stopAutopilotAnimation(vsync: v)); - void stopAutopilotAnimation({required TickerProvider vsync}) => _autopilotAnimationControllers.remove(vsync)?.dispose(); + void stopAutopilotAnimation({required TickerProvider vsync}) { + final animationController = _autopilotAnimators.remove(vsync); + return animationController?.dispose(); + } void startAutopilotAnimation({ required TickerProvider vsync, @@ -113,16 +116,37 @@ class ViewerController with CastMixin { duration: autopilotInterval, vsync: vsync, ); - animationController.addListener(() => onUpdate.call( - scaleLevel: ScaleLevel( - ref: scaleLevelRef, - factor: scaleFactorTween.evaluate(CurvedAnimation( - parent: animationController, - curve: Curves.linear, - )), - ), - )); - _autopilotAnimationControllers[vsync] = animationController; - Future.delayed(ADurations.viewerHorizontalPageAnimation).then((_) => _autopilotAnimationControllers[vsync]?.forward()); + final animation = CurvedAnimation( + parent: animationController, + curve: Curves.linear, + ); + animationController.addListener(() { + return onUpdate.call( + scaleLevel: ScaleLevel( + ref: scaleLevelRef, + factor: scaleFactorTween.evaluate(animation), + ), + ); + }); + _autopilotAnimators[vsync] = _AutopilotAnimators( + controller: animationController, + animation: animation, + ); + Future.delayed(ADurations.viewerHorizontalPageAnimation).then((_) => _autopilotAnimators[vsync]?.controller.forward()); + } +} + +class _AutopilotAnimators { + final AnimationController controller; + final CurvedAnimation animation; + + _AutopilotAnimators({ + required this.controller, + required this.animation, + }); + + void dispose() { + animation.dispose(); + controller.dispose(); } } diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index afa511a45..16188cf7d 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -73,7 +73,7 @@ class _EntryViewerStackState extends State with EntryViewContr final ValueNotifier _viewLocked = ValueNotifier(false); final ValueNotifier _overlayExpandedNotifier = ValueNotifier(false); late AnimationController _verticalPageAnimationController, _overlayAnimationController; - late Animation _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity; + late CurvedAnimation _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity, _overlayTopOffsetAnimation; late Animation _overlayTopOffset; EdgeInsets? _frozenViewInsets, _frozenViewPadding; late VideoActionDelegate _videoActionDelegate; @@ -158,10 +158,11 @@ class _EntryViewerStackState extends State with EntryViewContr parent: _overlayAnimationController, curve: Curves.easeOutQuad, ); - _overlayTopOffset = Tween(begin: const Offset(0, -1), end: const Offset(0, 0)).animate(CurvedAnimation( + _overlayTopOffsetAnimation = CurvedAnimation( parent: _overlayAnimationController, curve: Curves.easeOutQuad, - )); + ); + _overlayTopOffset = Tween(begin: const Offset(0, -1), end: const Offset(0, 0)).animate(_overlayTopOffsetAnimation); _overlayVisible.value = settings.showOverlayOnOpening && !viewerController.autopilot; _overlayVisible.addListener(_onOverlayVisibleChanged); _viewLocked.addListener(_onViewLockedChanged); @@ -188,6 +189,10 @@ class _EntryViewerStackState extends State with EntryViewContr cleanEntryControllers(entryNotifier.value); _videoActionDelegate.dispose(); _verticalPageAnimationController.dispose(); + _overlayButtonScale.dispose(); + _overlayVideoControlScale.dispose(); + _overlayOpacity.dispose(); + _overlayTopOffsetAnimation.dispose(); _overlayAnimationController.dispose(); _overlayVisible.dispose(); _viewLocked.dispose(); diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 95bfd9642..85cb274c7 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -286,5 +286,8 @@ class _InfoPageContentState extends State<_InfoPageContent> { }); } - void _onFilter(CollectionFilter filter) => FilterSelectedNotification(filter).dispatch(context); + void _onFilter(CollectionFilter filter) { + if (!mounted) return; + FilterSelectedNotification(filter).dispatch(context); + } } diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 728c530d5..9a6024f17 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -133,7 +133,7 @@ class _BottomOverlayContent extends StatefulWidget { class _BottomOverlayContentState extends State<_BottomOverlayContent> { final FocusScopeNode _buttonRowFocusScopeNode = FocusScopeNode(); - late Animation _buttonScale, _thumbnailOpacity; + late CurvedAnimation _buttonScale, _thumbnailOpacity; @override void initState() { @@ -170,7 +170,8 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { } void _unregisterWidget(_BottomOverlayContent widget) { - // nothing + _buttonScale.dispose(); + _thumbnailOpacity.dispose(); } @override diff --git a/lib/widgets/viewer/overlay/histogram.dart b/lib/widgets/viewer/overlay/histogram.dart index 62a4e9ca8..60ec190c3 100644 --- a/lib/widgets/viewer/overlay/histogram.dart +++ b/lib/widgets/viewer/overlay/histogram.dart @@ -53,8 +53,10 @@ class _ImageHistogramState extends State { void _registerWidget(ImageHistogram widget) { _imageStream = imageProvider.resolve(ImageConfiguration.empty); - _imageListener = ImageStreamListener((image, synchronousCall) { - _updateLevels(image); + _imageListener = ImageStreamListener((image, synchronousCall) async { + // implementer is responsible for disposing the provided `ImageInfo` + await _updateLevels(image); + image.dispose(); }); _imageStream?.addListener(_imageListener); } diff --git a/lib/widgets/viewer/overlay/locked.dart b/lib/widgets/viewer/overlay/locked.dart index 7c730708a..ed4afe749 100644 --- a/lib/widgets/viewer/overlay/locked.dart +++ b/lib/widgets/viewer/overlay/locked.dart @@ -25,7 +25,7 @@ class ViewerLockedOverlay extends StatefulWidget { } class _ViewerLockedOverlayState extends State { - late Animation _buttonScale; + late CurvedAnimation _buttonScale; @override void initState() { @@ -55,7 +55,7 @@ class _ViewerLockedOverlayState extends State { } void _unregisterWidget(ViewerLockedOverlay widget) { - // nothing + _buttonScale.dispose(); } @override diff --git a/lib/widgets/viewer/overlay/slideshow_buttons.dart b/lib/widgets/viewer/overlay/slideshow_buttons.dart index 5b84cc031..a34af9a6c 100644 --- a/lib/widgets/viewer/overlay/slideshow_buttons.dart +++ b/lib/widgets/viewer/overlay/slideshow_buttons.dart @@ -25,7 +25,7 @@ class SlideshowButtons extends StatefulWidget { class _SlideshowButtonsState extends State { final FocusScopeNode _buttonRowFocusScopeNode = FocusScopeNode(); - late Animation _buttonScale; + late CurvedAnimation _buttonScale; static const List _actions = [ SlideshowAction.resume, @@ -65,7 +65,7 @@ class _SlideshowButtonsState extends State { } void _unregisterWidget(SlideshowButtons widget) { - // nothing + _buttonScale.dispose(); } @override diff --git a/lib/widgets/viewer/overlay/video/progress_bar.dart b/lib/widgets/viewer/overlay/video/progress_bar.dart index 0a9144c9c..310b363a2 100644 --- a/lib/widgets/viewer/overlay/video/progress_bar.dart +++ b/lib/widgets/viewer/overlay/video/progress_bar.dart @@ -222,6 +222,6 @@ class _VideoProgressBarState extends State { double? _progressToDx(double progress) { final box = _getProgressBarRenderBox(); - return box == null ? null : progress * box.size.width; + return box != null && box.hasSize ? progress * box.size.width : null; } } diff --git a/lib/widgets/viewer/slideshow_page.dart b/lib/widgets/viewer/slideshow_page.dart index 12331b58a..a67ca879d 100644 --- a/lib/widgets/viewer/slideshow_page.dart +++ b/lib/widgets/viewer/slideshow_page.dart @@ -35,6 +35,7 @@ class SlideshowPage extends StatefulWidget { } class _SlideshowPageState extends State { + final ValueNotifier _appModeNotifier = ValueNotifier(AppMode.slideshow); late ViewerController _viewerController; late CollectionLens _slideshowCollection; AvesEntry? _initialEntry; @@ -51,6 +52,7 @@ class _SlideshowPageState extends State { @override void dispose() { + _appModeNotifier.dispose(); _disposeViewerController(); super.dispose(); } @@ -59,7 +61,7 @@ class _SlideshowPageState extends State { Widget build(BuildContext context) { final initialEntry = _initialEntry; return ListenableProvider>.value( - value: ValueNotifier(AppMode.slideshow), + value: _appModeNotifier, child: AvesScaffold( body: initialEntry == null ? EmptyContent( diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 7e38eed88..b4c015b6c 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -41,6 +41,7 @@ class _RasterImageViewState extends State { ImageStream? _fullImageStream; late ImageStreamListener _fullImageListener; final ValueNotifier _fullImageLoaded = ValueNotifier(false); + ImageInfo? _fullImageInfo; AvesEntry get entry => widget.entry; @@ -101,10 +102,13 @@ class _RasterImageViewState extends State { void _unregisterFullImage() { _fullImageStream?.removeListener(_fullImageListener); _fullImageStream = null; + _fullImageInfo?.dispose(); } void _onFullImageCompleted(ImageInfo image, bool synchronousCall) { + // implementer is responsible for disposing the provided `ImageInfo` _unregisterFullImage(); + _fullImageInfo = image; _fullImageLoaded.value = true; FullImageLoadedNotification(entry, fullImageProvider).dispatch(context); } diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index b76cbb5ab..95ced38ed 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -37,6 +37,7 @@ class _VectorImageViewState extends State { ImageStream? _fullImageStream; late ImageStreamListener _fullImageListener; final ValueNotifier _fullImageLoaded = ValueNotifier(false); + ImageInfo? _fullImageInfo; AvesEntry get entry => widget.entry; @@ -91,10 +92,13 @@ class _VectorImageViewState extends State { void _unregisterFullImage() { _fullImageStream?.removeListener(_fullImageListener); _fullImageStream = null; + _fullImageInfo?.dispose(); } void _onFullImageCompleted(ImageInfo image, bool synchronousCall) { + // implementer is responsible for disposing the provided `ImageInfo` _unregisterFullImage(); + _fullImageInfo = image; _fullImageLoaded.value = true; } diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index 56b51a35b..10763ee5a 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -71,7 +71,7 @@ class EntryEditor extends StatefulWidget { class _EntryEditorState extends State with EntryViewControllerMixin, SingleTickerProviderStateMixin { final ValueNotifier _overlayVisible = ValueNotifier(true); late AnimationController _overlayAnimationController; - late Animation _overlayVideoControlScale; + late CurvedAnimation _overlayVideoControlScale; EdgeInsets? _frozenViewInsets, _frozenViewPadding; late VideoActionDelegate _videoActionDelegate; late final ViewerController _viewerController; @@ -120,6 +120,7 @@ class _EntryEditorState extends State with EntryViewControllerMixin void dispose() { cleanEntryControllers(entry); _overlayVisible.dispose(); + _overlayVideoControlScale.dispose(); _overlayAnimationController.dispose(); _videoActionDelegate.dispose(); _viewerController.dispose(); diff --git a/plugins/aves_screen_state/android/build.gradle b/plugins/aves_screen_state/android/build.gradle index 941d165b2..2a7c98004 100644 --- a/plugins/aves_screen_state/android/build.gradle +++ b/plugins/aves_screen_state/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { ext { kotlin_version = '1.9.24' - agp_version = '8.4.1' + agp_version = '8.5.0' } repositories { diff --git a/plugins/aves_video/lib/src/controller.dart b/plugins/aves_video/lib/src/controller.dart index 7babf040f..8fac10917 100644 --- a/plugins/aves_video/lib/src/controller.dart +++ b/plugins/aves_video/lib/src/controller.dart @@ -47,6 +47,7 @@ abstract class AvesVideoController with ABRepeatMixin { if (kFlutterMemoryAllocationsEnabled) { FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); } + abRepeatNotifier.dispose(); _entry.visualChangeNotifier.removeListener(onVisualChanged); await _savePlaybackState(); } diff --git a/pubspec.yaml b/pubspec.yaml index eca07a22b..c8742c81d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt -version: 1.11.2+121 +version: 1.11.3+122 publish_to: none environment: @@ -176,7 +176,7 @@ flutter: # and `_PackageLicensePage` in `/material/about.dart` # # `OverlaySnackBar` in `/widgets/common/action_mixins/overlay_snack_bar.dart` -# adapts from Flutter v3.16.0 `SnackBar` in `/material/snack_bar.dart` +# adapts from Flutter v3.23.0 `SnackBar` in `/material/snack_bar.dart` # # `EagerScaleGestureRecognizer` in `/widgets/common/behaviour/eager_scale_gesture_recognizer.dart` # adapts from Flutter v3.16.0 `ScaleGestureRecognizer` in `/gestures/scale.dart` diff --git a/test/fake/media_fetch_service.dart b/test/fake/media_fetch_service.dart index 50dd1d969..9b18b6e6e 100644 --- a/test/fake/media_fetch_service.dart +++ b/test/fake/media_fetch_service.dart @@ -7,7 +7,7 @@ class FakeMediaFetchService extends Fake implements MediaFetchService { Set entries = {}; @override - Future getEntry(String uri, String? mimeType) async { + Future getEntry(String uri, String? mimeType, {bool allowUnsized = false}) async { return entries.firstWhereOrNull((v) => v.uri == uri); } } diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 24d17aed2..dfb8b7c93 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,3 +1,3 @@ -In v1.11.2: +In v1.11.3: - show selected albums together in Collection Full changelog available on GitHub \ No newline at end of file