Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-06-17 21:40:55 +02:00
commit 90fa60df69
49 changed files with 238 additions and 123 deletions

View file

@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17
### Added
- handle `MediaStore.ACTION_REVIEW` intent
## <a id="v1.11.2"></a>[v1.11.2] - 2024-06-11 ## <a id="v1.11.2"></a>[v1.11.2] - 2024-06-11
### Added ### Added

View file

@ -142,6 +142,7 @@
<action android:name="android.intent.action.PICK" /> <action android:name="android.intent.action.PICK" />
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.provider.action.REVIEW" />
<action android:name="com.android.camera.action.REVIEW" /> <action android:name="com.android.camera.action.REVIEW" />
<action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" /> <action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" />
@ -161,6 +162,7 @@
<action android:name="android.intent.action.PICK" /> <action android:name="android.intent.action.PICK" />
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.provider.action.REVIEW" />
<action android:name="com.android.camera.action.REVIEW" /> <action android:name="com.android.camera.action.REVIEW" />
<action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" /> <action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" />

View file

@ -10,6 +10,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
@ -299,6 +300,7 @@ open class MainActivity : FlutterFragmentActivity() {
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
Intent.ACTION_SEND, Intent.ACTION_SEND,
MediaStore.ACTION_REVIEW,
"com.android.camera.action.REVIEW", "com.android.camera.action.REVIEW",
"com.android.camera.action.SPLIT_SCREEN_REVIEW" -> { "com.android.camera.action.SPLIT_SCREEN_REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri -> (intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->

View file

@ -340,7 +340,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
} }
ioScope.launch { ioScope.launch {
provider.fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback { provider.fetchSingle(context, uri, mimeType, false, object : ImageProvider.ImageOpCallback {
override fun onSuccess(fields: FieldMap) { override fun onSuccess(fields: FieldMap) {
resultFields.putAll(fields) resultFields.putAll(fields)
result.success(resultFields) result.success(resultFields)

View file

@ -29,6 +29,7 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
private fun getEntry(call: MethodCall, result: MethodChannel.Result) { private fun getEntry(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") // MIME type is optional val mimeType = call.argument<String>("mimeType") // MIME type is optional
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val allowUnsized = call.argument<Boolean>("allowUnsized") ?: false
if (uri == null) { if (uri == null) {
result.error("getEntry-args", "missing arguments", null) result.error("getEntry-args", "missing arguments", null)
return return
@ -40,7 +41,7 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
return 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 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) override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri mimeType=$mimeType", throwable.message)
}) })

View file

@ -12,7 +12,7 @@ import deckers.thibault.aves.utils.LogUtils
import java.io.File import java.io.File
internal class FileImageProvider : ImageProvider() { 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 var mimeType = sourceMimeType
if (mimeType == null) { if (mimeType == null) {
@ -54,7 +54,7 @@ internal class FileImageProvider : ImageProvider() {
} }
entry.fillPreCatalogMetadata(context) entry.fillPreCatalogMetadata(context)
if (entry.isSized || entry.isSvg || entry.isVideo) { if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap()) callback.onSuccess(entry.toMap())
} else { } else {
callback.onFailure(Exception("entry has no size")) callback.onFailure(Exception("entry has no size"))

View file

@ -70,7 +70,7 @@ import java.util.TimeZone
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
abstract class ImageProvider { 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")) callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider"))
} }

View file

@ -89,7 +89,7 @@ class MediaStoreImageProvider : ImageProvider() {
// the provided URI can point to the wrong media collection, // the provided URI can point to the wrong media collection,
// e.g. a GIF image with the URI `content://media/external/video/media/[ID]` // 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 // 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 var found = false
val fetched = arrayListOf<FieldMap>() val fetched = arrayListOf<FieldMap>()
val id = uri.tryParseId() val id = uri.tryParseId()

View file

@ -17,7 +17,7 @@ open class UnknownContentProvider : ImageProvider() {
open val reliableProviderMimeType: Boolean open val reliableProviderMimeType: Boolean
get() = false 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 var mimeType = sourceMimeType
if (sourceMimeType == null || !reliableProviderMimeType) { if (sourceMimeType == null || !reliableProviderMimeType) {
// source MIME type may be incorrect, so we get a second opinion if possible // 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) 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()) callback.onSuccess(entry.toMap())
} else { } else {
callback.onFailure(Exception("entry has no size")) callback.onFailure(Exception("entry has no size"))

View file

@ -1,6 +1,6 @@
buildscript { buildscript {
ext { 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' glide_version = '4.16.0'
// AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550 // 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' huawei_agconnect_version = '1.9.1.300'

View file

@ -10,7 +10,7 @@ pluginManagement {
settings.ext.kotlin_version = '1.9.24' settings.ext.kotlin_version = '1.9.24'
settings.ext.ksp_version = "$kotlin_version-1.0.20" 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") includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")

View file

@ -0,0 +1,3 @@
In v1.11.3:
- show selected albums together in Collection
Full changelog available on GitHub

View file

@ -0,0 +1,3 @@
In v1.11.3:
- show selected albums together in Collection
Full changelog available on GitHub

View file

@ -360,7 +360,7 @@ class MediaStoreSource extends CollectionSource {
existingDirectories.add(existingDirectory); existingDirectories.add(existingDirectory);
} }
} else { } else {
final sourceEntry = await mediaFetchService.getEntry(uri, null); final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true);
if (sourceEntry != null) { if (sourceEntry != null) {
newEntries.add(sourceEntry.copyWith( newEntries.add(sourceEntry.copyWith(
id: metadataDb.nextId, id: metadataDb.nextId,

View file

@ -67,7 +67,7 @@ mixin TrashMixin on SourceBase {
await metadataDb.updateTrash(id, entry.trashDetails); await metadataDb.updateTrash(id, entry.trashDetails);
} else { } else {
// there is no matching entry // there is no matching entry
final sourceEntry = await mediaFetchService.getEntry(uri, null); final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true);
if (sourceEntry != null) { if (sourceEntry != null) {
final id = metadataDb.nextId; final id = metadataDb.nextId;
sourceEntry.id = id; sourceEntry.id = id;

View file

@ -166,7 +166,7 @@ class Vaults extends ChangeNotifier {
debugPrint('Recovering ${untrackedPaths.length} untracked vault items'); debugPrint('Recovering ${untrackedPaths.length} untracked vault items');
await Future.forEach(untrackedPaths, (untrackedPath) async { await Future.forEach(untrackedPaths, (untrackedPath) async {
final uri = Uri.file(untrackedPath).toString(); 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) { if (sourceEntry != null) {
sourceEntry.id = metadataDb.nextId; sourceEntry.id = metadataDb.nextId;
sourceEntry.origin = EntryOrigins.vault; sourceEntry.origin = EntryOrigins.vault;

View file

@ -13,7 +13,7 @@ import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart'; import 'package:streams_channel/streams_channel.dart';
abstract class MediaFetchService { abstract class MediaFetchService {
Future<AvesEntry?> getEntry(String uri, String? mimeType); Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false});
Future<Uint8List> getSvg( Future<Uint8List> getSvg(
String uri, String uri,
@ -75,11 +75,12 @@ class PlatformMediaFetchService implements MediaFetchService {
static const double _thumbnailDefaultSize = 64.0; static const double _thumbnailDefaultSize = 64.0;
@override @override
Future<AvesEntry?> getEntry(String uri, String? mimeType) async { Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false}) async {
try { try {
final result = await _platformObject.invokeMethod('getEntry', <String, dynamic>{ final result = await _platformObject.invokeMethod('getEntry', <String, dynamic>{
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
'allowUnsized': allowUnsized,
}) as Map; }) as Map;
return AvesEntry.fromMap(result); return AvesEntry.fromMap(result);
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {

View file

@ -51,6 +51,7 @@ class _RandomTextSpanHighlighter extends StatefulWidget {
class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> with SingleTickerProviderStateMixin { class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> with SingleTickerProviderStateMixin {
late final AnimationController _controller; late final AnimationController _controller;
late final CurvedAnimation _animation;
late final Animation<TextStyle> _animatedStyle; late final Animation<TextStyle> _animatedStyle;
late final TextStyle _baseStyle; late final TextStyle _baseStyle;
int _highlightedIndex = 0; int _highlightedIndex = 0;
@ -90,14 +91,16 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter>
} }
}) })
..repeat(reverse: true); ..repeat(reverse: true);
_animatedStyle = ShadowedTextStyleTween(begin: _baseStyle, end: highlightStyle).animate(CurvedAnimation( _animation = CurvedAnimation(
parent: _controller, parent: _controller,
curve: Curves.easeInOutCubic, curve: Curves.easeInOutCubic,
)); );
_animatedStyle = ShadowedTextStyleTween(begin: _baseStyle, end: highlightStyle).animate(_animation);
} }
@override @override
void dispose() { void dispose() {
_animation.dispose();
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }

View file

@ -113,11 +113,13 @@ class _CollectionGridContent extends StatefulWidget {
class _CollectionGridContentState extends State<_CollectionGridContent> { class _CollectionGridContentState extends State<_CollectionGridContent> {
final ValueNotifier<AvesEntry?> _focusedItemNotifier = ValueNotifier(null); final ValueNotifier<AvesEntry?> _focusedItemNotifier = ValueNotifier(null);
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
final ValueNotifier<AppMode> _selectingAppModeNotifier = ValueNotifier(AppMode.pickFilteredMediaInternal);
@override @override
void dispose() { void dispose() {
_focusedItemNotifier.dispose(); _focusedItemNotifier.dispose();
_isScrollingNotifier.dispose(); _isScrollingNotifier.dispose();
_selectingAppModeNotifier.dispose();
super.dispose(); super.dispose();
} }
@ -252,7 +254,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
if (selection.isSelecting) { if (selection.isSelecting) {
child = MultiProvider( child = MultiProvider(
providers: [ providers: [
ListenableProvider<ValueNotifier<AppMode>>.value(value: ValueNotifier(AppMode.pickFilteredMediaInternal)), ListenableProvider<ValueNotifier<AppMode>>.value(value: _selectingAppModeNotifier),
ChangeNotifierProvider<Selection<AvesEntry>>.value(value: selection), ChangeNotifierProvider<Selection<AvesEntry>>.value(value: selection),
], ],
child: child, child: child,

View file

@ -22,7 +22,7 @@ abstract class ChooserQuickButton<T> extends StatefulWidget {
abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> extends State<T> with SingleTickerProviderStateMixin { abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> extends State<T> with SingleTickerProviderStateMixin {
AnimationController? _animationController; AnimationController? _animationController;
Animation<double>? _animation; CurvedAnimation? _animation;
OverlayEntry? _chooserOverlayEntry; OverlayEntry? _chooserOverlayEntry;
final ValueNotifier<U?> _chooserValueNotifier = ValueNotifier(null); final ValueNotifier<U?> _chooserValueNotifier = ValueNotifier(null);
final StreamController<LongPressMoveUpdateDetails> _moveUpdateStreamController = StreamController.broadcast(); final StreamController<LongPressMoveUpdateDetails> _moveUpdateStreamController = StreamController.broadcast();
@ -47,6 +47,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
@override @override
void dispose() { void dispose() {
_animation?.dispose();
_animationController?.dispose(); _animationController?.dispose();
_clearChooserOverlayEntry(); _clearChooserOverlayEntry();
_chooserValueNotifier.dispose(); _chooserValueNotifier.dispose();

View file

@ -178,7 +178,7 @@ class ReportOverlay<T> extends StatefulWidget {
class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerProviderStateMixin { class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerProviderStateMixin {
final processed = <T>{}; final processed = <T>{};
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _animation; late CurvedAnimation _animation;
Stream<T> get opStream => widget.opStream; Stream<T> get opStream => widget.opStream;
@ -212,6 +212,7 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
@override @override
void dispose() { void dispose() {
_animation.dispose();
_animationController.dispose(); _animationController.dispose();
super.dispose(); super.dispose();
} }
@ -317,6 +318,7 @@ class _FeedbackMessage extends StatefulWidget {
class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerProviderStateMixin { class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerProviderStateMixin {
AnimationController? _animationController; AnimationController? _animationController;
CurvedAnimation? _animation;
Animation<int>? _remainingDurationMillis; Animation<int>? _remainingDurationMillis;
int? _totalDurationMillis; int? _totalDurationMillis;
@ -333,19 +335,21 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro
duration: effectiveDuration, duration: effectiveDuration,
vsync: this, vsync: this,
); );
_animation = CurvedAnimation(
parent: _animationController!,
curve: Curves.linear,
);
_remainingDurationMillis = IntTween( _remainingDurationMillis = IntTween(
begin: effectiveDuration.inMilliseconds, begin: effectiveDuration.inMilliseconds,
end: 0, end: 0,
).animate(CurvedAnimation( ).animate(_animation!);
parent: _animationController!,
curve: Curves.linear,
));
_animationController!.forward(); _animationController!.forward();
} }
} }
@override @override
void dispose() { void dispose() {
_animation?.dispose();
_animationController?.dispose(); _animationController?.dispose();
super.dispose(); super.dispose();
} }

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
// adapted from Flutter `SnackBar` in `/material/snack_bar.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. // 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` // This overlay entry is not under 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.
@ -73,10 +73,17 @@ class OverlaySnackBar extends StatefulWidget {
class _OverlaySnackBarState extends State<OverlaySnackBar> { class _OverlaySnackBarState extends State<OverlaySnackBar> {
bool _wasVisible = false; bool _wasVisible = false;
CurvedAnimation? _heightAnimation;
CurvedAnimation? _fadeInAnimation;
CurvedAnimation? _fadeInM3Animation;
CurvedAnimation? _fadeOutAnimation;
CurvedAnimation? _heightM3Animation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
widget.animation!.addStatusListener(_onAnimationStatusChanged); widget.animation!.addStatusListener(_onAnimationStatusChanged);
_setAnimations();
} }
@override @override
@ -85,26 +92,55 @@ class _OverlaySnackBarState extends State<OverlaySnackBar> {
if (widget.animation != oldWidget.animation) { if (widget.animation != oldWidget.animation) {
oldWidget.animation!.removeStatusListener(_onAnimationStatusChanged); oldWidget.animation!.removeStatusListener(_onAnimationStatusChanged);
widget.animation!.addStatusListener(_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 @override
void dispose() { void dispose() {
widget.animation!.removeStatusListener(_onAnimationStatusChanged); widget.animation!.removeStatusListener(_onAnimationStatusChanged);
_disposeAnimations();
super.dispose(); super.dispose();
} }
void _onAnimationStatusChanged(AnimationStatus animationStatus) { void _onAnimationStatusChanged(AnimationStatus animationStatus) {
switch (animationStatus) { if (animationStatus == AnimationStatus.completed) {
case AnimationStatus.dismissed: if (widget.onVisible != null && !_wasVisible) {
case AnimationStatus.forward: widget.onVisible!();
case AnimationStatus.reverse: }
break; _wasVisible = true;
case AnimationStatus.completed:
if (widget.onVisible != null && !_wasVisible) {
widget.onVisible!();
}
_wasVisible = true;
} }
} }
@ -173,28 +209,13 @@ class _OverlaySnackBarState extends State<OverlaySnackBar> {
final double iconHorizontalMargin = (widget.padding?.resolve(TextDirection.ltr).right ?? horizontalPadding) / 12.0; 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 final IconButton? iconButton = showCloseIcon
? IconButton( ? IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
iconSize: 24.0, iconSize: 24.0,
color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor, color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor,
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss), onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss),
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
) )
: null; : null;
@ -253,7 +274,7 @@ class _OverlaySnackBarState extends State<OverlaySnackBar> {
child: accessibleNavigation || theme.useMaterial3 child: accessibleNavigation || theme.useMaterial3
? snackBar ? snackBar
: FadeTransition( : FadeTransition(
opacity: fadeOutAnimation, opacity: _fadeOutAnimation!,
child: snackBar, child: snackBar,
), ),
), ),
@ -288,7 +309,7 @@ class _OverlaySnackBarState extends State<OverlaySnackBar> {
key: const Key('dismissible'), key: const Key('dismissible'),
direction: widget.dismissDirection, direction: widget.dismissDirection,
resizeDuration: null, 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(), onDismissed: (direction) => widget.onDismiss(),
child: snackBar, child: snackBar,
), ),
@ -299,19 +320,19 @@ class _OverlaySnackBarState extends State<OverlaySnackBar> {
snackBarTransition = snackBar; snackBarTransition = snackBar;
} else if (isFloatingSnackBar && !theme.useMaterial3) { } else if (isFloatingSnackBar && !theme.useMaterial3) {
snackBarTransition = FadeTransition( snackBarTransition = FadeTransition(
opacity: fadeInAnimation, opacity: _fadeInAnimation!,
child: snackBar, child: snackBar,
); );
// Is Material 3 Floating Snack Bar. // Is Material 3 Floating Snack Bar.
} else if (isFloatingSnackBar && theme.useMaterial3) { } else if (isFloatingSnackBar && theme.useMaterial3) {
snackBarTransition = FadeTransition( snackBarTransition = FadeTransition(
opacity: fadeInM3Animation, opacity: _fadeInM3Animation!,
child: AnimatedBuilder( child: ValueListenableBuilder<double>(
animation: heightM3Animation, valueListenable: _heightM3Animation!,
builder: (context, child) { builder: (context, value, child) {
return Align( return Align(
alignment: AlignmentDirectional.bottomStart, alignment: Alignment.bottomLeft,
heightFactor: heightM3Animation.value, heightFactor: value,
child: child, child: child,
); );
}, },
@ -319,12 +340,12 @@ class _OverlaySnackBarState extends State<OverlaySnackBar> {
), ),
); );
} else { } else {
snackBarTransition = AnimatedBuilder( snackBarTransition = ValueListenableBuilder<double>(
animation: heightAnimation, valueListenable: _heightAnimation!,
builder: (context, child) { builder: (context, value, child) {
return Align( return Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
heightFactor: heightAnimation.value, heightFactor: value,
child: child, child: child,
); );
}, },

View file

@ -121,9 +121,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
late Offset _longPressLastGlobalPosition; late Offset _longPressLastGlobalPosition;
late AnimationController _thumbAnimationController; late AnimationController _thumbAnimationController;
late Animation<double> _thumbAnimation; late CurvedAnimation _thumbAnimation;
late AnimationController _labelAnimationController; late AnimationController _labelAnimationController;
late Animation<double> _labelAnimation; late CurvedAnimation _labelAnimation;
Timer? _fadeoutTimer; Timer? _fadeoutTimer;
Map<double, String>? _percentCrumbs; Map<double, String>? _percentCrumbs;
final Map<double, String> _viewportCrumbs = {}; final Map<double, String> _viewportCrumbs = {};
@ -167,7 +167,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
@override @override
void dispose() { void dispose() {
_thumbAnimation.dispose();
_thumbAnimationController.dispose(); _thumbAnimationController.dispose();
_labelAnimation.dispose();
_labelAnimationController.dispose(); _labelAnimationController.dispose();
_fadeoutTimer?.cancel(); _fadeoutTimer?.cancel();
super.dispose(); super.dispose();

View file

@ -26,7 +26,7 @@ class AnimatedDiffText extends StatefulWidget {
class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerProviderStateMixin { class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerProviderStateMixin {
late final AnimationController _controller; late final AnimationController _controller;
late final Animation<double> _animation; late final CurvedAnimation _animation;
final List<_TextDiff> _diffs = []; final List<_TextDiff> _diffs = [];
TextStyle get _textStyle { TextStyle get _textStyle {
@ -66,6 +66,7 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
@override @override
void dispose() { void dispose() {
_animation.dispose();
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }

View file

@ -22,6 +22,7 @@ class ChangeHighlightText extends StatefulWidget {
class _ChangeHighlightTextState extends State<ChangeHighlightText> with SingleTickerProviderStateMixin { class _ChangeHighlightTextState extends State<ChangeHighlightText> with SingleTickerProviderStateMixin {
late final AnimationController _controller; late final AnimationController _controller;
late final CurvedAnimation _animation;
late final Animation<TextStyle> _style; late final Animation<TextStyle> _style;
@override @override
@ -33,10 +34,11 @@ class _ChangeHighlightTextState extends State<ChangeHighlightText> with SingleTi
) )
..value = 1 ..value = 1
..addListener(() => setState(() {})); ..addListener(() => setState(() {}));
_style = ShadowedTextStyleTween(begin: widget.changedStyle, end: widget.style).animate(CurvedAnimation( _animation = CurvedAnimation(
parent: _controller, parent: _controller,
curve: widget.curve, curve: widget.curve,
)); );
_style = ShadowedTextStyleTween(begin: widget.changedStyle, end: widget.style).animate(_animation);
} }
@override @override
@ -51,6 +53,7 @@ class _ChangeHighlightTextState extends State<ChangeHighlightText> with SingleTi
@override @override
void dispose() { void dispose() {
_animation.dispose();
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }

View file

@ -31,6 +31,7 @@ class Sweeper extends StatefulWidget {
class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin { class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
late AnimationController _angleAnimationController; late AnimationController _angleAnimationController;
late CurvedAnimation _angleAnimation;
late Animation<double> _angle; late Animation<double> _angle;
bool _isAppearing = false; bool _isAppearing = false;
@ -46,13 +47,14 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
final startAngle = widget.startAngle; final startAngle = widget.startAngle;
final sweepAngle = widget.sweepAngle; final sweepAngle = widget.sweepAngle;
final centerSweep = widget.centerSweep; final centerSweep = widget.centerSweep;
_angleAnimation = CurvedAnimation(
parent: _angleAnimationController,
curve: widget.curve,
);
_angle = Tween( _angle = Tween(
begin: startAngle - sweepAngle * (centerSweep ? .5 : 0), begin: startAngle - sweepAngle * (centerSweep ? .5 : 0),
end: startAngle + pi * 2 - sweepAngle * (centerSweep ? .5 : 1), end: startAngle + pi * 2 - sweepAngle * (centerSweep ? .5 : 1),
).animate(CurvedAnimation( ).animate(_angleAnimation);
parent: _angleAnimationController,
curve: widget.curve,
));
_angleAnimationController.addStatusListener(_onAnimationStatusChanged); _angleAnimationController.addStatusListener(_onAnimationStatusChanged);
_registerWidget(widget); _registerWidget(widget);
} }
@ -66,7 +68,7 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
@override @override
void dispose() { void dispose() {
_angleAnimationController.removeStatusListener(_onAnimationStatusChanged); _angleAnimation.dispose();
_angleAnimationController.dispose(); _angleAnimationController.dispose();
_unregisterWidget(widget); _unregisterWidget(widget);
super.dispose(); super.dispose();

View file

@ -286,9 +286,8 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
final animation = CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn); final animation = CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn);
controller.addListener(() => animate(animation)); controller.addListener(() => animate(animation));
animation.addStatusListener((status) { animation.addStatusListener((status) {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) {
controller.dispose(); animation.dispose();
} else if (status == AnimationStatus.dismissed) {
controller.dispose(); controller.dispose();
} }
}); });

View file

@ -64,6 +64,7 @@ class _AlbumPickPage extends StatefulWidget {
class _AlbumPickPageState extends State<_AlbumPickPage> { class _AlbumPickPageState extends State<_AlbumPickPage> {
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.pickFilterInternal);
CollectionSource get source => widget.source; CollectionSource get source => widget.source;
@ -93,13 +94,14 @@ class _AlbumPickPageState extends State<_AlbumPickPage> {
@override @override
void dispose() { void dispose() {
_appBarHeightNotifier.dispose(); _appBarHeightNotifier.dispose();
_appModeNotifier.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListenableProvider<ValueNotifier<AppMode>>.value( return ListenableProvider<ValueNotifier<AppMode>>.value(
value: ValueNotifier(AppMode.pickFilterInternal), value: _appModeNotifier,
child: Selector<Settings, (AlbumChipGroupFactor, ChipSortFactor)>( child: Selector<Settings, (AlbumChipGroupFactor, ChipSortFactor)>(
selector: (context, s) => (s.albumGroupFactor, s.albumSortFactor), selector: (context, s) => (s.albumGroupFactor, s.albumSortFactor),
builder: (context, s, child) { builder: (context, s, child) {

View file

@ -29,20 +29,23 @@ class ItemPickPage extends StatefulWidget {
} }
class _ItemPickPageState extends State<ItemPickPage> { class _ItemPickPageState extends State<ItemPickPage> {
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.initialization);
CollectionLens get collection => widget.collection; CollectionLens get collection => widget.collection;
@override @override
void dispose() { void dispose() {
collection.dispose(); collection.dispose();
_appModeNotifier.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final liveFilter = collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; 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<ValueNotifier<AppMode>>.value( return ListenableProvider<ValueNotifier<AppMode>>.value(
value: ValueNotifier(mode), value: _appModeNotifier,
child: AvesScaffold( child: AvesScaffold(
body: SelectionProvider<AvesEntry>( body: SelectionProvider<AvesEntry>(
child: QueryProvider( child: QueryProvider(

View file

@ -43,7 +43,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero); final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero);
final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0); final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0);
late AnimationController _gridAnimationController; late AnimationController _gridAnimationController;
late Animation<double> _gridOpacity; late CurvedAnimation _gridOpacity;
static const double minDimension = Cropper.handleDimension; static const double minDimension = Cropper.handleDimension;
static const int panResizeGridDivision = 3; static const int panResizeGridDivision = 3;
@ -87,6 +87,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
_viewportSizeNotifier.dispose(); _viewportSizeNotifier.dispose();
_outlineNotifier.dispose(); _outlineNotifier.dispose();
_gridDivisionNotifier.dispose(); _gridDivisionNotifier.dispose();
_gridOpacity.dispose();
_gridAnimationController.dispose(); _gridAnimationController.dispose();
_unregisterWidget(widget); _unregisterWidget(widget);
super.dispose(); super.dispose();

View file

@ -103,7 +103,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
final ValueNotifier<double> _overlayOpacityNotifier = ValueNotifier(1); final ValueNotifier<double> _overlayOpacityNotifier = ValueNotifier(1);
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true); final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
late AnimationController _overlayAnimationController; late AnimationController _overlayAnimationController;
late Animation<double> _overlayScale, _scrollerSize; late CurvedAnimation _overlayScale, _scrollerSize;
CoordinateFilter? _regionFilter; CoordinateFilter? _regionFilter;
CollectionLens? get regionCollection => _regionCollectionNotifier.value; CollectionLens? get regionCollection => _regionCollectionNotifier.value;
@ -170,6 +170,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
_dotEntryNotifier.dispose(); _dotEntryNotifier.dispose();
_overlayOpacityNotifier.dispose(); _overlayOpacityNotifier.dispose();
_overlayVisible.dispose(); _overlayVisible.dispose();
_overlayScale.dispose();
_scrollerSize.dispose();
_overlayAnimationController.dispose(); _overlayAnimationController.dispose();
// provided collection should be a new instance specifically created // provided collection should be a new instance specifically created

View file

@ -25,7 +25,8 @@ class FloatingNavBar extends StatefulWidget {
class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProviderStateMixin { class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
late AnimationController _controller; late AnimationController _controller;
late Animation<Offset> _offsetAnimation; late CurvedAnimation _animation;
late Animation<Offset> _offset;
double? _lastOffset; double? _lastOffset;
bool _isDragging = false; bool _isDragging = false;
@ -36,13 +37,14 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
vsync: this, vsync: this,
); );
_offsetAnimation = Tween<Offset>( _animation = CurvedAnimation(
begin: const Offset(0, 0),
end: const Offset(0, 1),
).animate(CurvedAnimation(
parent: _controller, parent: _controller,
curve: Curves.linear, curve: Curves.linear,
)) );
_offset = Tween<Offset>(
begin: const Offset(0, 0),
end: const Offset(0, 1),
).animate(_animation)
..addListener(() { ..addListener(() {
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
@ -63,6 +65,8 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
@override @override
void dispose() { void dispose() {
_unregisterWidget(widget); _unregisterWidget(widget);
_animation.dispose();
_controller.dispose();
super.dispose(); super.dispose();
} }
@ -82,7 +86,7 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SlideTransition( return SlideTransition(
position: _offsetAnimation, position: _offset,
child: widget.child, child: widget.child,
); );
} }

View file

@ -22,7 +22,7 @@ class ViewerController with CastMixin {
late final ValueNotifier<bool> _autopilotNotifier; late final ValueNotifier<bool> _autopilotNotifier;
Timer? _playTimer; Timer? _playTimer;
final StreamController _streamController = StreamController.broadcast(); final StreamController _streamController = StreamController.broadcast();
final Map<TickerProvider, AnimationController> _autopilotAnimationControllers = {}; final Map<TickerProvider, _AutopilotAnimators> _autopilotAnimators = {};
ScaleLevel? _autopilotInitialScale; ScaleLevel? _autopilotInitialScale;
Stream<dynamic> get _events => _streamController.stream; Stream<dynamic> get _events => _streamController.stream;
@ -94,9 +94,12 @@ class ViewerController with CastMixin {
void _stopPlayTimer() => _playTimer?.cancel(); 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({ void startAutopilotAnimation({
required TickerProvider vsync, required TickerProvider vsync,
@ -113,16 +116,37 @@ class ViewerController with CastMixin {
duration: autopilotInterval, duration: autopilotInterval,
vsync: vsync, vsync: vsync,
); );
animationController.addListener(() => onUpdate.call( final animation = CurvedAnimation(
scaleLevel: ScaleLevel( parent: animationController,
ref: scaleLevelRef, curve: Curves.linear,
factor: scaleFactorTween.evaluate(CurvedAnimation( );
parent: animationController, animationController.addListener(() {
curve: Curves.linear, return onUpdate.call(
)), scaleLevel: ScaleLevel(
), ref: scaleLevelRef,
)); factor: scaleFactorTween.evaluate(animation),
_autopilotAnimationControllers[vsync] = animationController; ),
Future.delayed(ADurations.viewerHorizontalPageAnimation).then((_) => _autopilotAnimationControllers[vsync]?.forward()); );
});
_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();
} }
} }

View file

@ -73,7 +73,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
final ValueNotifier<bool> _viewLocked = ValueNotifier(false); final ValueNotifier<bool> _viewLocked = ValueNotifier(false);
final ValueNotifier<bool> _overlayExpandedNotifier = ValueNotifier(false); final ValueNotifier<bool> _overlayExpandedNotifier = ValueNotifier(false);
late AnimationController _verticalPageAnimationController, _overlayAnimationController; late AnimationController _verticalPageAnimationController, _overlayAnimationController;
late Animation<double> _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity; late CurvedAnimation _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity, _overlayTopOffsetAnimation;
late Animation<Offset> _overlayTopOffset; late Animation<Offset> _overlayTopOffset;
EdgeInsets? _frozenViewInsets, _frozenViewPadding; EdgeInsets? _frozenViewInsets, _frozenViewPadding;
late VideoActionDelegate _videoActionDelegate; late VideoActionDelegate _videoActionDelegate;
@ -158,10 +158,11 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
parent: _overlayAnimationController, parent: _overlayAnimationController,
curve: Curves.easeOutQuad, curve: Curves.easeOutQuad,
); );
_overlayTopOffset = Tween(begin: const Offset(0, -1), end: const Offset(0, 0)).animate(CurvedAnimation( _overlayTopOffsetAnimation = CurvedAnimation(
parent: _overlayAnimationController, parent: _overlayAnimationController,
curve: Curves.easeOutQuad, curve: Curves.easeOutQuad,
)); );
_overlayTopOffset = Tween(begin: const Offset(0, -1), end: const Offset(0, 0)).animate(_overlayTopOffsetAnimation);
_overlayVisible.value = settings.showOverlayOnOpening && !viewerController.autopilot; _overlayVisible.value = settings.showOverlayOnOpening && !viewerController.autopilot;
_overlayVisible.addListener(_onOverlayVisibleChanged); _overlayVisible.addListener(_onOverlayVisibleChanged);
_viewLocked.addListener(_onViewLockedChanged); _viewLocked.addListener(_onViewLockedChanged);
@ -188,6 +189,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
cleanEntryControllers(entryNotifier.value); cleanEntryControllers(entryNotifier.value);
_videoActionDelegate.dispose(); _videoActionDelegate.dispose();
_verticalPageAnimationController.dispose(); _verticalPageAnimationController.dispose();
_overlayButtonScale.dispose();
_overlayVideoControlScale.dispose();
_overlayOpacity.dispose();
_overlayTopOffsetAnimation.dispose();
_overlayAnimationController.dispose(); _overlayAnimationController.dispose();
_overlayVisible.dispose(); _overlayVisible.dispose();
_viewLocked.dispose(); _viewLocked.dispose();

View file

@ -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);
}
} }

View file

@ -133,7 +133,7 @@ class _BottomOverlayContent extends StatefulWidget {
class _BottomOverlayContentState extends State<_BottomOverlayContent> { class _BottomOverlayContentState extends State<_BottomOverlayContent> {
final FocusScopeNode _buttonRowFocusScopeNode = FocusScopeNode(); final FocusScopeNode _buttonRowFocusScopeNode = FocusScopeNode();
late Animation<double> _buttonScale, _thumbnailOpacity; late CurvedAnimation _buttonScale, _thumbnailOpacity;
@override @override
void initState() { void initState() {
@ -170,7 +170,8 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
} }
void _unregisterWidget(_BottomOverlayContent widget) { void _unregisterWidget(_BottomOverlayContent widget) {
// nothing _buttonScale.dispose();
_thumbnailOpacity.dispose();
} }
@override @override

View file

@ -53,8 +53,10 @@ class _ImageHistogramState extends State<ImageHistogram> {
void _registerWidget(ImageHistogram widget) { void _registerWidget(ImageHistogram widget) {
_imageStream = imageProvider.resolve(ImageConfiguration.empty); _imageStream = imageProvider.resolve(ImageConfiguration.empty);
_imageListener = ImageStreamListener((image, synchronousCall) { _imageListener = ImageStreamListener((image, synchronousCall) async {
_updateLevels(image); // implementer is responsible for disposing the provided `ImageInfo`
await _updateLevels(image);
image.dispose();
}); });
_imageStream?.addListener(_imageListener); _imageStream?.addListener(_imageListener);
} }

View file

@ -25,7 +25,7 @@ class ViewerLockedOverlay extends StatefulWidget {
} }
class _ViewerLockedOverlayState extends State<ViewerLockedOverlay> { class _ViewerLockedOverlayState extends State<ViewerLockedOverlay> {
late Animation<double> _buttonScale; late CurvedAnimation _buttonScale;
@override @override
void initState() { void initState() {
@ -55,7 +55,7 @@ class _ViewerLockedOverlayState extends State<ViewerLockedOverlay> {
} }
void _unregisterWidget(ViewerLockedOverlay widget) { void _unregisterWidget(ViewerLockedOverlay widget) {
// nothing _buttonScale.dispose();
} }
@override @override

View file

@ -25,7 +25,7 @@ class SlideshowButtons extends StatefulWidget {
class _SlideshowButtonsState extends State<SlideshowButtons> { class _SlideshowButtonsState extends State<SlideshowButtons> {
final FocusScopeNode _buttonRowFocusScopeNode = FocusScopeNode(); final FocusScopeNode _buttonRowFocusScopeNode = FocusScopeNode();
late Animation<double> _buttonScale; late CurvedAnimation _buttonScale;
static const List<SlideshowAction> _actions = [ static const List<SlideshowAction> _actions = [
SlideshowAction.resume, SlideshowAction.resume,
@ -65,7 +65,7 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
} }
void _unregisterWidget(SlideshowButtons widget) { void _unregisterWidget(SlideshowButtons widget) {
// nothing _buttonScale.dispose();
} }
@override @override

View file

@ -222,6 +222,6 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
double? _progressToDx(double progress) { double? _progressToDx(double progress) {
final box = _getProgressBarRenderBox(); final box = _getProgressBarRenderBox();
return box == null ? null : progress * box.size.width; return box != null && box.hasSize ? progress * box.size.width : null;
} }
} }

View file

@ -35,6 +35,7 @@ class SlideshowPage extends StatefulWidget {
} }
class _SlideshowPageState extends State<SlideshowPage> { class _SlideshowPageState extends State<SlideshowPage> {
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.slideshow);
late ViewerController _viewerController; late ViewerController _viewerController;
late CollectionLens _slideshowCollection; late CollectionLens _slideshowCollection;
AvesEntry? _initialEntry; AvesEntry? _initialEntry;
@ -51,6 +52,7 @@ class _SlideshowPageState extends State<SlideshowPage> {
@override @override
void dispose() { void dispose() {
_appModeNotifier.dispose();
_disposeViewerController(); _disposeViewerController();
super.dispose(); super.dispose();
} }
@ -59,7 +61,7 @@ class _SlideshowPageState extends State<SlideshowPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final initialEntry = _initialEntry; final initialEntry = _initialEntry;
return ListenableProvider<ValueNotifier<AppMode>>.value( return ListenableProvider<ValueNotifier<AppMode>>.value(
value: ValueNotifier(AppMode.slideshow), value: _appModeNotifier,
child: AvesScaffold( child: AvesScaffold(
body: initialEntry == null body: initialEntry == null
? EmptyContent( ? EmptyContent(

View file

@ -41,6 +41,7 @@ class _RasterImageViewState extends State<RasterImageView> {
ImageStream? _fullImageStream; ImageStream? _fullImageStream;
late ImageStreamListener _fullImageListener; late ImageStreamListener _fullImageListener;
final ValueNotifier<bool> _fullImageLoaded = ValueNotifier(false); final ValueNotifier<bool> _fullImageLoaded = ValueNotifier(false);
ImageInfo? _fullImageInfo;
AvesEntry get entry => widget.entry; AvesEntry get entry => widget.entry;
@ -101,10 +102,13 @@ class _RasterImageViewState extends State<RasterImageView> {
void _unregisterFullImage() { void _unregisterFullImage() {
_fullImageStream?.removeListener(_fullImageListener); _fullImageStream?.removeListener(_fullImageListener);
_fullImageStream = null; _fullImageStream = null;
_fullImageInfo?.dispose();
} }
void _onFullImageCompleted(ImageInfo image, bool synchronousCall) { void _onFullImageCompleted(ImageInfo image, bool synchronousCall) {
// implementer is responsible for disposing the provided `ImageInfo`
_unregisterFullImage(); _unregisterFullImage();
_fullImageInfo = image;
_fullImageLoaded.value = true; _fullImageLoaded.value = true;
FullImageLoadedNotification(entry, fullImageProvider).dispatch(context); FullImageLoadedNotification(entry, fullImageProvider).dispatch(context);
} }

View file

@ -37,6 +37,7 @@ class _VectorImageViewState extends State<VectorImageView> {
ImageStream? _fullImageStream; ImageStream? _fullImageStream;
late ImageStreamListener _fullImageListener; late ImageStreamListener _fullImageListener;
final ValueNotifier<bool> _fullImageLoaded = ValueNotifier(false); final ValueNotifier<bool> _fullImageLoaded = ValueNotifier(false);
ImageInfo? _fullImageInfo;
AvesEntry get entry => widget.entry; AvesEntry get entry => widget.entry;
@ -91,10 +92,13 @@ class _VectorImageViewState extends State<VectorImageView> {
void _unregisterFullImage() { void _unregisterFullImage() {
_fullImageStream?.removeListener(_fullImageListener); _fullImageStream?.removeListener(_fullImageListener);
_fullImageStream = null; _fullImageStream = null;
_fullImageInfo?.dispose();
} }
void _onFullImageCompleted(ImageInfo image, bool synchronousCall) { void _onFullImageCompleted(ImageInfo image, bool synchronousCall) {
// implementer is responsible for disposing the provided `ImageInfo`
_unregisterFullImage(); _unregisterFullImage();
_fullImageInfo = image;
_fullImageLoaded.value = true; _fullImageLoaded.value = true;
} }

View file

@ -71,7 +71,7 @@ class EntryEditor extends StatefulWidget {
class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin, SingleTickerProviderStateMixin { class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin, SingleTickerProviderStateMixin {
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true); final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
late AnimationController _overlayAnimationController; late AnimationController _overlayAnimationController;
late Animation<double> _overlayVideoControlScale; late CurvedAnimation _overlayVideoControlScale;
EdgeInsets? _frozenViewInsets, _frozenViewPadding; EdgeInsets? _frozenViewInsets, _frozenViewPadding;
late VideoActionDelegate _videoActionDelegate; late VideoActionDelegate _videoActionDelegate;
late final ViewerController _viewerController; late final ViewerController _viewerController;
@ -120,6 +120,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
void dispose() { void dispose() {
cleanEntryControllers(entry); cleanEntryControllers(entry);
_overlayVisible.dispose(); _overlayVisible.dispose();
_overlayVideoControlScale.dispose();
_overlayAnimationController.dispose(); _overlayAnimationController.dispose();
_videoActionDelegate.dispose(); _videoActionDelegate.dispose();
_viewerController.dispose(); _viewerController.dispose();

View file

@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
buildscript { buildscript {
ext { ext {
kotlin_version = '1.9.24' kotlin_version = '1.9.24'
agp_version = '8.4.1' agp_version = '8.5.0'
} }
repositories { repositories {

View file

@ -47,6 +47,7 @@ abstract class AvesVideoController with ABRepeatMixin {
if (kFlutterMemoryAllocationsEnabled) { if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
} }
abRepeatNotifier.dispose();
_entry.visualChangeNotifier.removeListener(onVisualChanged); _entry.visualChangeNotifier.removeListener(onVisualChanged);
await _savePlaybackState(); await _savePlaybackState();
} }

View file

@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US # - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
version: 1.11.2+121 version: 1.11.3+122
publish_to: none publish_to: none
environment: environment:
@ -176,7 +176,7 @@ flutter:
# and `_PackageLicensePage` in `/material/about.dart` # and `_PackageLicensePage` in `/material/about.dart`
# #
# `OverlaySnackBar` in `/widgets/common/action_mixins/overlay_snack_bar.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` # `EagerScaleGestureRecognizer` in `/widgets/common/behaviour/eager_scale_gesture_recognizer.dart`
# adapts from Flutter v3.16.0 `ScaleGestureRecognizer` in `/gestures/scale.dart` # adapts from Flutter v3.16.0 `ScaleGestureRecognizer` in `/gestures/scale.dart`

View file

@ -7,7 +7,7 @@ class FakeMediaFetchService extends Fake implements MediaFetchService {
Set<AvesEntry> entries = {}; Set<AvesEntry> entries = {};
@override @override
Future<AvesEntry?> getEntry(String uri, String? mimeType) async { Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false}) async {
return entries.firstWhereOrNull((v) => v.uri == uri); return entries.firstWhereOrNull((v) => v.uri == uri);
} }
} }

View file

@ -1,3 +1,3 @@
In v1.11.2: In v1.11.3:
- show selected albums together in Collection - show selected albums together in Collection
Full changelog available on GitHub Full changelog available on GitHub