chore(mobile): centralize duplicate mesmerizing header components (#31590)
* chore(mobile): centralize duplicate mesmerizing header components * Fix formatting
This commit is contained in:
@@ -28,7 +28,7 @@ class ArchivePage extends StatelessWidget {
|
||||
}),
|
||||
],
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(title: context.t.archive, icon: Icons.archive_outlined),
|
||||
appBar: MesmerizingSliverAppBar(title: context.t.archive),
|
||||
bottomSheet: const ArchiveBottomSheet(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ class FavoritePage extends StatelessWidget {
|
||||
}),
|
||||
],
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(title: context.t.favorites, icon: Icons.favorite_outline),
|
||||
appBar: MesmerizingSliverAppBar(title: context.t.favorites),
|
||||
bottomSheet: const FavoriteBottomSheet(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ class PartnerDetailPage extends StatelessWidget {
|
||||
}),
|
||||
],
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(title: partner.name, icon: Icons.person_outline),
|
||||
appBar: MesmerizingSliverAppBar(title: partner.name),
|
||||
topSliverWidget: _InfoBox(partner: partner),
|
||||
topSliverWidgetHeight: 110,
|
||||
bottomSheet: const PartnerDetailBottomSheet(),
|
||||
|
||||
@@ -21,9 +21,7 @@ class PlaceDetailPage extends StatelessWidget {
|
||||
return timelineService;
|
||||
}),
|
||||
],
|
||||
child: Timeline(
|
||||
appBar: MesmerizingSliverAppBar(title: place, icon: Icons.location_on),
|
||||
),
|
||||
child: Timeline(appBar: MesmerizingSliverAppBar(title: place)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,6 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||
child: Timeline(
|
||||
topSliverWidget: PendingUploadsBanner(albumId: _album.id),
|
||||
appBar: RemoteAlbumSliverAppBar(
|
||||
icon: Icons.photo_album_outlined,
|
||||
kebabMenu: _AlbumKebabMenu(
|
||||
album: _album,
|
||||
onDeleteAlbum: () => deleteAlbum(context),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
|
||||
/// Displays the number of timeline items in this view
|
||||
///
|
||||
/// Intended for use in Album style views
|
||||
class ItemCountText extends ConsumerStatefulWidget {
|
||||
final TextStyle style;
|
||||
|
||||
const ItemCountText({super.key, required this.style});
|
||||
|
||||
@override
|
||||
ConsumerState<ItemCountText> createState() => _ItemCountTextState();
|
||||
}
|
||||
|
||||
class _ItemCountTextState extends ConsumerState<ItemCountText> {
|
||||
StreamSubscription? _reloadSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_reloadSubscription = EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_reloadSubscription?.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assetCount = ref.watch(timelineServiceProvider.select((service) => service.totalAssets));
|
||||
|
||||
return Text(context.t.items_count(count: assetCount), style: context.textTheme.labelLarge?.merge(widget.style));
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
|
||||
/// Randomly selects an asset from the provided [TimelineService] for a persistent Ken Burns zoom
|
||||
class RandomAssetBackgroundImage extends StatefulWidget {
|
||||
final TimelineService timelineService;
|
||||
|
||||
const RandomAssetBackgroundImage({super.key, required this.timelineService});
|
||||
|
||||
@override
|
||||
State<RandomAssetBackgroundImage> createState() => _RandomAssetBackgroundImageState();
|
||||
}
|
||||
|
||||
class _RandomAssetBackgroundImageState extends State<RandomAssetBackgroundImage> with TickerProviderStateMixin {
|
||||
late AnimationController _zoomController;
|
||||
late AnimationController _crossFadeController;
|
||||
late Animation<double> _zoomAnimation;
|
||||
late Animation<Offset> _panAnimation;
|
||||
late Animation<double> _crossFadeAnimation;
|
||||
BaseAsset? _currentAsset;
|
||||
BaseAsset? _nextAsset;
|
||||
bool _isZoomingIn = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_zoomController = AnimationController(
|
||||
duration: const Duration(seconds: 12),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
);
|
||||
|
||||
_crossFadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
);
|
||||
|
||||
_zoomAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.2,
|
||||
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
|
||||
|
||||
_panAnimation = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.5, -0.5),
|
||||
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
|
||||
|
||||
_crossFadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(parent: _crossFadeController, curve: Curves.easeInOutCubic));
|
||||
|
||||
Future.delayed(Durations.medium1, () => _loadFirstAsset());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_zoomController.dispose();
|
||||
_crossFadeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startAnimationCycle() {
|
||||
if (_isZoomingIn) {
|
||||
unawaited(
|
||||
_zoomController.forward().then((_) {
|
||||
unawaited(_loadNextAsset());
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_zoomController.reverse().then((_) {
|
||||
unawaited(_loadNextAsset());
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFirstAsset() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
setState(() {
|
||||
_currentAsset = null;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentAsset = widget.timelineService.getRandomAsset();
|
||||
});
|
||||
|
||||
await _crossFadeController.forward();
|
||||
|
||||
if (_zoomController.status == AnimationStatus.dismissed) {
|
||||
if (_isZoomingIn) {
|
||||
_zoomController.reset();
|
||||
} else {
|
||||
_zoomController.value = 1.0;
|
||||
}
|
||||
_startAnimationCycle();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadNextAsset() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (widget.timelineService.totalAssets > 1) {
|
||||
// Load next asset while keeping current one visible
|
||||
final nextAsset = widget.timelineService.getRandomAsset();
|
||||
|
||||
setState(() {
|
||||
_nextAsset = nextAsset;
|
||||
});
|
||||
|
||||
await _crossFadeController.reverse();
|
||||
setState(() {
|
||||
_currentAsset = _nextAsset;
|
||||
_nextAsset = null;
|
||||
});
|
||||
|
||||
_crossFadeController.value = 1.0;
|
||||
|
||||
_isZoomingIn = !_isZoomingIn;
|
||||
|
||||
_startAnimationCycle();
|
||||
}
|
||||
} catch (e) {
|
||||
_zoomController.reset();
|
||||
_startAnimationCycle();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: Listenable.merge([_zoomAnimation, _panAnimation, _crossFadeAnimation]),
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _zoomAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: _panAnimation.value,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Current image
|
||||
if (_currentAsset != null)
|
||||
Opacity(
|
||||
opacity: _crossFadeAnimation.value,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
alignment: Alignment.topRight,
|
||||
image: getFullImageProvider(_currentAsset!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_nextAsset != null)
|
||||
Opacity(
|
||||
opacity: 1.0 - _crossFadeAnimation.value,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
alignment: Alignment.topRight,
|
||||
image: getFullImageProvider(_nextAsset!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,16 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/sliver_app_bar/item_count_text.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/sliver_app_bar/random_asset_background_image.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class MesmerizingSliverAppBar extends ConsumerStatefulWidget {
|
||||
const MesmerizingSliverAppBar({super.key, required this.title, this.icon = Icons.camera});
|
||||
const MesmerizingSliverAppBar({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
@override
|
||||
ConsumerState<MesmerizingSliverAppBar> createState() => _MesmerizingSliverAppBarState();
|
||||
}
|
||||
@@ -94,11 +89,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<MesmerizingSliverAppBa
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: _ExpandedBackground(
|
||||
scrollProgress: scrollProgress,
|
||||
title: widget.title,
|
||||
icon: widget.icon,
|
||||
),
|
||||
background: _ExpandedBackground(scrollProgress: scrollProgress, title: widget.title),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -109,9 +100,8 @@ class _MesmerizingSliverAppBarState extends ConsumerState<MesmerizingSliverAppBa
|
||||
class _ExpandedBackground extends ConsumerStatefulWidget {
|
||||
final double scrollProgress;
|
||||
final String title;
|
||||
final IconData icon;
|
||||
|
||||
const _ExpandedBackground({required this.scrollProgress, required this.title, required this.icon});
|
||||
const _ExpandedBackground({required this.scrollProgress, required this.title});
|
||||
|
||||
@override
|
||||
ConsumerState<_ExpandedBackground> createState() => _ExpandedBackgroundState();
|
||||
@@ -156,7 +146,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
offset: Offset(0, widget.scrollProgress * 50),
|
||||
child: Transform.scale(
|
||||
scale: 1.4 - (widget.scrollProgress * 0.2),
|
||||
child: _RandomAssetBackground(timelineService: timelineService, icon: widget.icon),
|
||||
child: RandomAssetBackgroundImage(timelineService: timelineService),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@@ -200,267 +190,21 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(duration: const Duration(milliseconds: 300), child: const _ItemCountText()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ItemCountText extends ConsumerStatefulWidget {
|
||||
const _ItemCountText();
|
||||
|
||||
@override
|
||||
ConsumerState<_ItemCountText> createState() => _ItemCountTextState();
|
||||
}
|
||||
|
||||
class _ItemCountTextState extends ConsumerState<_ItemCountText> {
|
||||
StreamSubscription? _reloadSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_reloadSubscription = EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_reloadSubscription?.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets));
|
||||
|
||||
return Text(
|
||||
context.t.items_count(count: assetCount),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
// letterSpacing: 0.2,
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: const ItemCountText(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
shadows: [const Shadow(offset: Offset(0, 1), blurRadius: 6, color: Colors.black45)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RandomAssetBackground extends StatefulWidget {
|
||||
final TimelineService timelineService;
|
||||
final IconData icon;
|
||||
|
||||
const _RandomAssetBackground({required this.timelineService, required this.icon});
|
||||
|
||||
@override
|
||||
State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState();
|
||||
}
|
||||
|
||||
class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with TickerProviderStateMixin {
|
||||
late AnimationController _zoomController;
|
||||
late AnimationController _crossFadeController;
|
||||
late Animation<double> _zoomAnimation;
|
||||
late Animation<Offset> _panAnimation;
|
||||
late Animation<double> _crossFadeAnimation;
|
||||
BaseAsset? _currentAsset;
|
||||
BaseAsset? _nextAsset;
|
||||
bool _isZoomingIn = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_zoomController = AnimationController(
|
||||
duration: const Duration(seconds: 12),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
);
|
||||
|
||||
_crossFadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
);
|
||||
|
||||
_zoomAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.2,
|
||||
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
|
||||
|
||||
_panAnimation = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.5, -0.5),
|
||||
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
|
||||
|
||||
_crossFadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(parent: _crossFadeController, curve: Curves.easeInOutCubic));
|
||||
|
||||
Future.delayed(Durations.medium1, () => _loadFirstAsset());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_zoomController.dispose();
|
||||
_crossFadeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startAnimationCycle() {
|
||||
if (_isZoomingIn) {
|
||||
unawaited(
|
||||
_zoomController.forward().then((_) {
|
||||
unawaited(_loadNextAsset());
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_zoomController.reverse().then((_) {
|
||||
unawaited(_loadNextAsset());
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFirstAsset() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
setState(() {
|
||||
_currentAsset = null;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentAsset = widget.timelineService.getRandomAsset();
|
||||
});
|
||||
|
||||
await _crossFadeController.forward();
|
||||
|
||||
if (_zoomController.status == AnimationStatus.dismissed) {
|
||||
if (_isZoomingIn) {
|
||||
_zoomController.reset();
|
||||
} else {
|
||||
_zoomController.value = 1.0;
|
||||
}
|
||||
_startAnimationCycle();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadNextAsset() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (widget.timelineService.totalAssets > 1) {
|
||||
// Load next asset while keeping current one visible
|
||||
final nextAsset = widget.timelineService.getRandomAsset();
|
||||
|
||||
setState(() {
|
||||
_nextAsset = nextAsset;
|
||||
});
|
||||
|
||||
await _crossFadeController.reverse();
|
||||
setState(() {
|
||||
_currentAsset = _nextAsset;
|
||||
_nextAsset = null;
|
||||
});
|
||||
|
||||
_crossFadeController.value = 1.0;
|
||||
|
||||
_isZoomingIn = !_isZoomingIn;
|
||||
|
||||
_startAnimationCycle();
|
||||
}
|
||||
} catch (e) {
|
||||
_zoomController.reset();
|
||||
_startAnimationCycle();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: Listenable.merge([_zoomAnimation, _panAnimation, _crossFadeAnimation]),
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _zoomAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: _panAnimation.value,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Current image
|
||||
if (_currentAsset != null)
|
||||
Opacity(
|
||||
opacity: _crossFadeAnimation.value,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
alignment: Alignment.topRight,
|
||||
image: getFullImageProvider(_currentAsset!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_nextAsset != null)
|
||||
Opacity(
|
||||
opacity: 1.0 - _crossFadeAnimation.value,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
alignment: Alignment.topRight,
|
||||
image: getFullImageProvider(_nextAsset!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||
);
|
||||
},
|
||||
shadows: [Shadow(offset: Offset(0, 1), blurRadius: 6, color: Colors.black45)],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,12 @@ import 'dart:ui';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/sliver_app_bar/item_count_text.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/sliver_app_bar/random_asset_background_image.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
@@ -191,7 +188,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
offset: Offset(0, widget.scrollProgress * 50),
|
||||
child: Transform.scale(
|
||||
scale: 1.4 - (widget.scrollProgress * 0.2),
|
||||
child: _RandomAssetBackground(timelineService: timelineService),
|
||||
child: RandomAssetBackgroundImage(timelineService: timelineService),
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
@@ -272,7 +269,16 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(duration: const Duration(milliseconds: 300), child: const _ItemCountText()),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: const ItemCountText(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
shadows: [Shadow(offset: Offset(0, 1), blurRadius: 6, color: Colors.black45)],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: widget.onBirthdayTap,
|
||||
@@ -316,256 +322,3 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ItemCountText extends ConsumerStatefulWidget {
|
||||
const _ItemCountText();
|
||||
|
||||
@override
|
||||
ConsumerState<_ItemCountText> createState() => _ItemCountTextState();
|
||||
}
|
||||
|
||||
class _ItemCountTextState extends ConsumerState<_ItemCountText> {
|
||||
StreamSubscription? _reloadSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_reloadSubscription = EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_reloadSubscription?.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets));
|
||||
|
||||
return Text(
|
||||
context.t.items_count(count: assetCount),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
shadows: [const Shadow(offset: Offset(0, 1), blurRadius: 6, color: Colors.black45)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RandomAssetBackground extends StatefulWidget {
|
||||
final TimelineService timelineService;
|
||||
|
||||
const _RandomAssetBackground({required this.timelineService});
|
||||
|
||||
@override
|
||||
State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState();
|
||||
}
|
||||
|
||||
class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with TickerProviderStateMixin {
|
||||
late AnimationController _zoomController;
|
||||
late AnimationController _crossFadeController;
|
||||
late Animation<double> _zoomAnimation;
|
||||
late Animation<Offset> _panAnimation;
|
||||
late Animation<double> _crossFadeAnimation;
|
||||
BaseAsset? _currentAsset;
|
||||
BaseAsset? _nextAsset;
|
||||
bool _isZoomingIn = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_zoomController = AnimationController(
|
||||
duration: const Duration(seconds: 12),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
);
|
||||
|
||||
_crossFadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
);
|
||||
|
||||
_zoomAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.2,
|
||||
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
|
||||
|
||||
_panAnimation = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.5, -0.5),
|
||||
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
|
||||
|
||||
_crossFadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(parent: _crossFadeController, curve: Curves.easeInOutCubic));
|
||||
|
||||
Future.delayed(Durations.medium1, () => _loadFirstAsset());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_zoomController.dispose();
|
||||
_crossFadeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startAnimationCycle() {
|
||||
if (_isZoomingIn) {
|
||||
unawaited(
|
||||
_zoomController.forward().then((_) {
|
||||
unawaited(_loadNextAsset());
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_zoomController.reverse().then((_) {
|
||||
unawaited(_loadNextAsset());
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFirstAsset() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
setState(() {
|
||||
_currentAsset = null;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentAsset = widget.timelineService.getRandomAsset();
|
||||
});
|
||||
|
||||
await _crossFadeController.forward();
|
||||
|
||||
if (_zoomController.status == AnimationStatus.dismissed) {
|
||||
if (_isZoomingIn) {
|
||||
_zoomController.reset();
|
||||
} else {
|
||||
_zoomController.value = 1.0;
|
||||
}
|
||||
_startAnimationCycle();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadNextAsset() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (widget.timelineService.totalAssets > 1) {
|
||||
// Load next asset while keeping current one visible
|
||||
final nextAsset = widget.timelineService.getRandomAsset();
|
||||
|
||||
setState(() {
|
||||
_nextAsset = nextAsset;
|
||||
});
|
||||
|
||||
await _crossFadeController.reverse();
|
||||
setState(() {
|
||||
_currentAsset = _nextAsset;
|
||||
_nextAsset = null;
|
||||
});
|
||||
|
||||
_crossFadeController.value = 1.0;
|
||||
|
||||
_isZoomingIn = !_isZoomingIn;
|
||||
|
||||
_startAnimationCycle();
|
||||
}
|
||||
} catch (e) {
|
||||
_zoomController.reset();
|
||||
_startAnimationCycle();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: Listenable.merge([_zoomAnimation, _panAnimation, _crossFadeAnimation]),
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _zoomAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: _panAnimation.value,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Current image
|
||||
if (_currentAsset != null)
|
||||
Opacity(
|
||||
opacity: _crossFadeAnimation.value,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
alignment: Alignment.topRight,
|
||||
image: getFullImageProvider(_currentAsset!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_nextAsset != null)
|
||||
Opacity(
|
||||
opacity: 1.0 - _crossFadeAnimation.value,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
alignment: Alignment.topRight,
|
||||
image: getFullImageProvider(_nextAsset!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,10 @@ import 'dart:ui';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/sliver_app_bar/item_count_text.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/sliver_app_bar/random_asset_background_image.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
@@ -21,15 +17,8 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
||||
|
||||
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
||||
const RemoteAlbumSliverAppBar({
|
||||
super.key,
|
||||
this.icon = Icons.camera,
|
||||
required this.kebabMenu,
|
||||
this.onEditTitle,
|
||||
this.onActivity,
|
||||
});
|
||||
const RemoteAlbumSliverAppBar({super.key, required this.kebabMenu, this.onEditTitle, this.onActivity});
|
||||
|
||||
final IconData icon;
|
||||
final Widget kebabMenu;
|
||||
final void Function()? onEditTitle;
|
||||
final void Function()? onActivity;
|
||||
@@ -131,11 +120,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
||||
});
|
||||
|
||||
return FlexibleSpaceBar(
|
||||
background: _ExpandedBackground(
|
||||
scrollProgress: scrollProgress,
|
||||
icon: widget.icon,
|
||||
onEditTitle: widget.onEditTitle,
|
||||
),
|
||||
background: _ExpandedBackground(scrollProgress: scrollProgress, onEditTitle: widget.onEditTitle),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -145,10 +130,9 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
||||
|
||||
class _ExpandedBackground extends ConsumerStatefulWidget {
|
||||
final double scrollProgress;
|
||||
final IconData icon;
|
||||
final void Function()? onEditTitle;
|
||||
|
||||
const _ExpandedBackground({required this.scrollProgress, required this.icon, this.onEditTitle});
|
||||
const _ExpandedBackground({required this.scrollProgress, this.onEditTitle});
|
||||
|
||||
@override
|
||||
ConsumerState<_ExpandedBackground> createState() => _ExpandedBackgroundState();
|
||||
@@ -199,7 +183,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
offset: Offset(0, widget.scrollProgress * 50),
|
||||
child: Transform.scale(
|
||||
scale: 1.4 - (widget.scrollProgress * 0.2),
|
||||
child: _RandomAssetBackground(timelineService: timelineService, icon: widget.icon),
|
||||
child: RandomAssetBackgroundImage(timelineService: timelineService),
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
@@ -252,7 +236,15 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black87)],
|
||||
),
|
||||
),
|
||||
AnimatedContainer(duration: const Duration(milliseconds: 300), child: const _ItemCountText()),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: const ItemCountText(
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black87)],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GestureDetector(
|
||||
@@ -289,259 +281,6 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
}
|
||||
}
|
||||
|
||||
class _ItemCountText extends ConsumerStatefulWidget {
|
||||
const _ItemCountText();
|
||||
|
||||
@override
|
||||
ConsumerState<_ItemCountText> createState() => _ItemCountTextState();
|
||||
}
|
||||
|
||||
class _ItemCountTextState extends ConsumerState<_ItemCountText> {
|
||||
StreamSubscription? _reloadSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_reloadSubscription = EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_reloadSubscription?.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets));
|
||||
|
||||
return Text(
|
||||
context.t.items_count(count: assetCount),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: [const Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black87)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RandomAssetBackground extends StatefulWidget {
|
||||
final TimelineService timelineService;
|
||||
final IconData icon;
|
||||
|
||||
const _RandomAssetBackground({required this.timelineService, required this.icon});
|
||||
|
||||
@override
|
||||
State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState();
|
||||
}
|
||||
|
||||
class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with TickerProviderStateMixin {
|
||||
late AnimationController _zoomController;
|
||||
late AnimationController _crossFadeController;
|
||||
late Animation<double> _zoomAnimation;
|
||||
late Animation<Offset> _panAnimation;
|
||||
late Animation<double> _crossFadeAnimation;
|
||||
BaseAsset? _currentAsset;
|
||||
BaseAsset? _nextAsset;
|
||||
bool _isZoomingIn = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_zoomController = AnimationController(
|
||||
duration: const Duration(seconds: 12),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
);
|
||||
|
||||
_crossFadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
);
|
||||
|
||||
_zoomAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.2,
|
||||
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
|
||||
|
||||
_panAnimation = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.5, -0.5),
|
||||
).animate(CurvedAnimation(parent: _zoomController, curve: Curves.easeInOut));
|
||||
|
||||
_crossFadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(parent: _crossFadeController, curve: Curves.easeInOutCubic));
|
||||
|
||||
Future.delayed(Durations.medium1, () => _loadFirstAsset());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_zoomController.dispose();
|
||||
_crossFadeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startAnimationCycle() {
|
||||
if (_isZoomingIn) {
|
||||
unawaited(
|
||||
_zoomController.forward().then((_) {
|
||||
unawaited(_loadNextAsset());
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_zoomController.reverse().then((_) {
|
||||
unawaited(_loadNextAsset());
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFirstAsset() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
setState(() {
|
||||
_currentAsset = null;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentAsset = widget.timelineService.getRandomAsset();
|
||||
});
|
||||
|
||||
await _crossFadeController.forward();
|
||||
|
||||
if (_zoomController.status == AnimationStatus.dismissed) {
|
||||
if (_isZoomingIn) {
|
||||
_zoomController.reset();
|
||||
} else {
|
||||
_zoomController.value = 1.0;
|
||||
}
|
||||
_startAnimationCycle();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadNextAsset() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (widget.timelineService.totalAssets > 1) {
|
||||
// Load next asset while keeping current one visible
|
||||
final nextAsset = widget.timelineService.getRandomAsset();
|
||||
|
||||
setState(() {
|
||||
_nextAsset = nextAsset;
|
||||
});
|
||||
|
||||
await _crossFadeController.reverse();
|
||||
setState(() {
|
||||
_currentAsset = _nextAsset;
|
||||
_nextAsset = null;
|
||||
});
|
||||
|
||||
_crossFadeController.value = 1.0;
|
||||
|
||||
_isZoomingIn = !_isZoomingIn;
|
||||
|
||||
_startAnimationCycle();
|
||||
}
|
||||
} catch (e) {
|
||||
_zoomController.reset();
|
||||
_startAnimationCycle();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.timelineService.totalAssets == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: Listenable.merge([_zoomAnimation, _panAnimation, _crossFadeAnimation]),
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _zoomAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: _panAnimation.value,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Current image
|
||||
if (_currentAsset != null)
|
||||
Opacity(
|
||||
opacity: _crossFadeAnimation.value,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
alignment: Alignment.topRight,
|
||||
image: getFullImageProvider(_currentAsset!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_nextAsset != null)
|
||||
Opacity(
|
||||
opacity: 1.0 - _crossFadeAnimation.value,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Image(
|
||||
alignment: Alignment.topRight,
|
||||
image: getFullImageProvider(_nextAsset!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.error_outline_rounded, size: 24, color: Colors.red[300]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DynamicText extends StatelessWidget {
|
||||
final String text;
|
||||
final double maxWidth;
|
||||
|
||||
Reference in New Issue
Block a user