diff --git a/i18n/en.json b/i18n/en.json index 5903d7850e..5279463c73 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1286,6 +1286,7 @@ "link_to_oauth": "Link to OAuth", "linked_oauth_account": "Linked OAuth account", "list": "List", + "live": "Live", "loading": "Loading", "loading_search_results_failed": "Loading search results failed", "local": "Local", @@ -1415,6 +1416,7 @@ "month": "Month", "monthly_title_text_date_format": "MMMM y", "more": "More", + "motion": "Motion", "move": "Move", "move_off_locked_folder": "Move out of locked folder", "move_to": "Move to", diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index d992d243ee..b576d96c9e 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -20,6 +20,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widg import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/motion_photo_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; @@ -692,6 +693,7 @@ class _AssetViewerState extends ConsumerState { backgroundDecoration: BoxDecoration(color: backgroundColor), enablePanAlways: true, ), + const Positioned(top: -40, left: 0, right: 0, child: MotionPhotoPlayButton()), if (!showingBottomSheet) const Positioned( bottom: 0, diff --git a/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart new file mode 100644 index 0000000000..efff17826b --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; + +class MotionPhotoPlayButton extends ConsumerWidget { + const MotionPhotoPlayButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + final isPlaying = ref.watch(isPlayingMotionVideoProvider); + final showControls = ref.watch(assetViewerProvider.select((state) => state.showingControls)); + final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); + + if (asset == null || !asset.isMotionPhoto || isShowingSheet) { + return const SizedBox.shrink(); + } + + return IgnorePointer( + ignoring: !showControls, + child: AnimatedOpacity( + opacity: showControls ? 1.0 : 0.0, + duration: Durations.short2, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: Center( + child: _MotionButton( + isPlaying: isPlaying, + onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle, + ), + ), + ), + ), + ), + ); + } +} + +class _MotionButton extends StatelessWidget { + final bool isPlaying; + final VoidCallback onPressed; + + const _MotionButton({required this.isPlaying, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.grey[800]!.withValues(alpha: 0.4), + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: InkWell( + onTap: onPressed, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 8), + Text( + CurrentPlatform.isAndroid ? 'motion'.t(context: context) : 'live'.t(context: context), + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index f8c758dfc1..872764b801 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -8,7 +8,6 @@ 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/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart'; @@ -51,7 +50,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final originalTheme = context.themeData; final actions = [ - if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true), if (album != null && album.isActivityEnabled && album.isShared) IconButton( icon: const Icon(Icons.chat_outlined), @@ -132,7 +130,10 @@ class _AssetInfoTitle extends StatelessWidget { @override Widget build(BuildContext context) { final dateTime = asset.createdAt.toLocal(); - final dateFormatted = DateFormat.yMMMd().format(dateTime); + final currentYear = DateTime.now().year; + final isCurrentYear = dateTime.year == currentYear; + + final dateFormatted = isCurrentYear ? DateFormat.MMMd().format(dateTime) : DateFormat.yMMMd().format(dateTime); final timeFormatted = DateFormat.jm().format(dateTime); return Column(