Source Code added

This commit is contained in:
Fr4nz D13trich 2026-02-02 15:06:40 +01:00
parent 800376eafd
commit 9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions

View file

@ -0,0 +1,50 @@
// ignore_for_file: require_trailing_commas
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/models/memories/memory.model.dart';
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
class MemoryBottomInfo extends StatelessWidget {
final Memory memory;
const MemoryBottomInfo({super.key, required this.memory});
@override
Widget build(BuildContext context) {
final df = DateFormat.yMMMMd();
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
memory.title,
style: TextStyle(color: Colors.grey[400], fontSize: 13.0, fontWeight: FontWeight.w500),
),
Text(
df.format(memory.assets[0].fileCreatedAt),
style: const TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.w500),
),
],
),
MaterialButton(
minWidth: 0,
onPressed: () {
context.maybePop();
scrollToDateNotifierProvider.scrollToDate(memory.assets[0].fileCreatedAt);
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(Icons.open_in_new, color: Colors.white),
),
],
),
);
}
}

View file

@ -0,0 +1,117 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
class MemoryCard extends StatelessWidget {
final Asset asset;
final String title;
final bool showTitle;
final Function()? onVideoEnded;
const MemoryCard({required this.asset, required this.title, required this.showTitle, this.onVideoEnded, super.key});
@override
Widget build(BuildContext context) {
return Card(
color: Colors.black,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25.0)),
side: BorderSide(color: Colors.black, width: 1.0),
),
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
SizedBox.expand(child: _BlurredBackdrop(asset: asset)),
LayoutBuilder(
builder: (context, constraints) {
// Determine the fit using the aspect ratio
BoxFit fit = BoxFit.contain;
if (asset.width != null && asset.height != null) {
final aspectRatio = asset.width! / asset.height!;
final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight;
// Look for a 25% difference in either direction
if (phoneAspectRatio * .75 < aspectRatio && phoneAspectRatio * 1.25 > aspectRatio) {
// Cover to look nice if we have nearly the same aspect ratio
fit = BoxFit.cover;
}
}
if (asset.isImage) {
return Hero(
tag: 'memory-${asset.id}',
child: ImmichImage(asset, fit: fit, height: double.infinity, width: double.infinity),
);
} else {
return Hero(
tag: 'memory-${asset.id}',
child: SizedBox(
width: context.width,
height: context.height,
child: NativeVideoViewerPage(
key: ValueKey(asset.id),
asset: asset,
showControls: false,
playbackDelayFactor: 2,
image: ImmichImage(asset, width: context.width, height: context.height, fit: BoxFit.contain),
),
),
);
}
},
),
if (showTitle)
Positioned(
left: 18.0,
bottom: 18.0,
child: Text(
title,
style: context.textTheme.headlineMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
class _BlurredBackdrop extends HookWidget {
final Asset asset;
const _BlurredBackdrop({required this.asset});
@override
Widget build(BuildContext context) {
final blurhash = useBlurHashRef(asset).value;
if (blurhash != null) {
// Use a nice cheap blur hash image decoration
return Container(
decoration: BoxDecoration(
image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover),
),
child: Container(color: Colors.black.withValues(alpha: 0.2)),
);
} else {
// Fall back to using a more expensive image filtered
// Since the ImmichImage is already precached, we can
// safely use that as the image provider
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: ImmichImage.imageProvider(asset: asset, height: context.height, width: context.width),
fit: BoxFit.cover,
),
),
child: Container(color: Colors.black.withValues(alpha: 0.2)),
),
);
}
}
}

View file

@ -0,0 +1,99 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class MemoryEpilogue extends StatefulWidget {
final Function()? onStartOver;
const MemoryEpilogue({super.key, this.onStartOver});
@override
State<MemoryEpilogue> createState() => _MemoryEpilogueState();
}
class _MemoryEpilogueState extends State<MemoryEpilogue> with TickerProviderStateMixin {
late final _animationController = AnimationController(vsync: this, duration: const Duration(seconds: 2))
..repeat(reverse: true);
late final Animation _animation;
@override
void initState() {
super.initState();
_animation = CurvedAnimation(parent: _animationController, curve: Curves.easeIn);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Stack(
children: [
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle_outline_sharp,
color: context.isDarkTheme ? context.colorScheme.primary : context.colorScheme.inversePrimary,
size: 64.0,
),
const SizedBox(height: 16.0),
Text(
"memories_all_caught_up",
style: context.textTheme.headlineMedium?.copyWith(color: Colors.white),
).tr(),
const SizedBox(height: 16.0),
Text(
"memories_check_back_tomorrow",
style: context.textTheme.bodyMedium?.copyWith(color: Colors.white),
).tr(),
const SizedBox(height: 16.0),
TextButton(
onPressed: widget.onStartOver,
child: Text(
"memories_start_over",
style: context.textTheme.displayMedium?.copyWith(
color: context.isDarkTheme ? context.colorScheme.primary : context.colorScheme.inversePrimary,
),
).tr(),
),
],
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
children: [
SizedBox(
height: 48,
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(offset: Offset(0, 8 * _animationController.value), child: child);
},
child: const Icon(size: 32, Icons.expand_less_sharp, color: Colors.white),
),
),
Text(
"memories_swipe_to_close",
style: context.textTheme.bodyMedium?.copyWith(color: Colors.white),
).tr(),
],
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,95 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/memories/memory.model.dart';
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
class MemoryLane extends HookConsumerWidget {
const MemoryLane({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final memoryLaneFutureProvider = ref.watch(memoryFutureProvider);
final memoryLane = memoryLaneFutureProvider
.whenData(
(memories) => memories != null
? ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView(
itemExtent: 145.0,
shrinkExtent: 1.0,
elevation: 2,
backgroundColor: Colors.black,
overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)),
onTap: (memoryIndex) {
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
if (memories[memoryIndex].assets.isNotEmpty) {
final asset = memories[memoryIndex].assets[0];
ref.read(currentAssetProvider.notifier).set(asset);
if (asset.isVideo || asset.isMotionPhoto) {
ref.read(videoPlaybackValueProvider.notifier).reset();
}
}
context.pushRoute(MemoryRoute(memories: memories, memoryIndex: memoryIndex));
},
children: memories
.mapIndexed<Widget>((index, memory) => MemoryCard(index: index, memory: memory))
.toList(),
),
)
: const SizedBox(),
)
.value;
return memoryLane ?? const SizedBox();
}
}
class MemoryCard extends ConsumerWidget {
const MemoryCard({super.key, required this.index, required this.memory});
final int index;
final Memory memory;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Center(
child: Stack(
children: [
ColorFiltered(
colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken),
child: Hero(
tag: 'memory-${memory.assets[0].id}',
child: ImmichImage(
memory.assets[0],
fit: BoxFit.cover,
width: 205,
height: 200,
placeholder: const ThumbnailPlaceholder(width: 105, height: 200),
),
),
),
Positioned(
bottom: 16,
left: 16,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 114),
child: Text(
memory.title,
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15),
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class MemoryProgressIndicator extends StatelessWidget {
/// The number of ticks in the progress indicator
final int ticks;
/// The current value of the indicator
final double value;
const MemoryProgressIndicator({super.key, required this.ticks, required this.value});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final tickWidth = constraints.maxWidth / ticks;
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(2.0)),
child: Stack(
children: [
LinearProgressIndicator(
value: value,
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
backgroundColor: Colors.grey[800],
color: context.isDarkTheme ? context.colorScheme.primary : context.colorScheme.inversePrimary,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(
ticks,
(i) => Container(
width: tickWidth,
height: 4,
decoration: BoxDecoration(
border: i == 0 ? null : const Border(left: BorderSide(color: Colors.black, width: 1)),
),
),
),
),
],
),
);
},
);
}
}