mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: display more info in asset viewer (#24630)
* feat(mobile): more info for asset viewer * feat(mobile): more info for asset viewer
This commit is contained in:
@@ -1403,6 +1403,7 @@
|
|||||||
"link_to_oauth": "Link to OAuth",
|
"link_to_oauth": "Link to OAuth",
|
||||||
"linked_oauth_account": "Linked OAuth account",
|
"linked_oauth_account": "Linked OAuth account",
|
||||||
"list": "List",
|
"list": "List",
|
||||||
|
"live": "Live",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"loading_search_results_failed": "Loading search results failed",
|
"loading_search_results_failed": "Loading search results failed",
|
||||||
"local": "Local",
|
"local": "Local",
|
||||||
@@ -1584,6 +1585,7 @@
|
|||||||
"month": "Month",
|
"month": "Month",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
|
"motion": "Motion",
|
||||||
"move": "Move",
|
"move": "Move",
|
||||||
"move_down": "Move down",
|
"move_down": "Move down",
|
||||||
"move_off_locked_folder": "Move out of locked folder",
|
"move_off_locked_folder": "Move out of locked folder",
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
|
|
||||||
|
class MotionPhotoPlayButton extends ConsumerWidget {
|
||||||
|
const MotionPhotoPlayButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asset = ref.watch(assetViewerProvider.select((state) => state.currentAsset));
|
||||||
|
final isPlaying = ref.watch(isPlayingMotionVideoProvider);
|
||||||
|
final showControls = ref.watch(assetViewerProvider.select((state) => state.showingControls));
|
||||||
|
final isShowingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails));
|
||||||
|
|
||||||
|
if (asset == null || !asset.isMotionPhoto || isShowingDetails) {
|
||||||
|
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[900]!.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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ class ViewerKebabMenu extends ConsumerWidget {
|
|||||||
timelineOrigin: timelineOrigin,
|
timelineOrigin: timelineOrigin,
|
||||||
);
|
);
|
||||||
|
|
||||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);
|
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context);
|
||||||
|
|
||||||
return MenuAnchor(
|
return MenuAnchor(
|
||||||
consumeOutsideTap: true,
|
consumeOutsideTap: true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
@@ -10,11 +11,13 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_act
|
|||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
|
||||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/timezone.dart';
|
||||||
|
|
||||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
const ViewerTopAppBar({super.key});
|
const ViewerTopAppBar({super.key});
|
||||||
@@ -95,16 +98,17 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
SafeArea(
|
SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: SizedBox.square(
|
child: SizedBox(
|
||||||
|
height: preferredSize.height,
|
||||||
child: Theme(
|
child: Theme(
|
||||||
data: context.themeData.copyWith(iconTheme: const IconThemeData(size: 22, color: Colors.white)),
|
data: context.themeData.copyWith(iconTheme: const IconThemeData(size: 22, color: Colors.white)),
|
||||||
child: Row(
|
child: NavigationToolbar(
|
||||||
children: [
|
centerMiddle: true,
|
||||||
const _AppBarBackButton(),
|
leading: const _AppBarBackButton(),
|
||||||
const Spacer(),
|
middle: showingDetails ? null : _AssetInfoTitle(asset: asset),
|
||||||
if (!showingDetails && !isReadonlyModeEnabled)
|
trailing: !showingDetails && !isReadonlyModeEnabled
|
||||||
if (isInLockedView) ...lockedViewActions else ...actions,
|
? Row(mainAxisSize: MainAxisSize.min, children: isInLockedView ? lockedViewActions : actions)
|
||||||
],
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -139,3 +143,32 @@ class _AppBarBackButton extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AssetInfoTitle extends ConsumerWidget {
|
||||||
|
final BaseAsset asset;
|
||||||
|
|
||||||
|
const _AssetInfoTitle({required this.asset});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
DateTime dateTime = asset.createdAt.toLocal();
|
||||||
|
final currentYear = DateTime.now().year;
|
||||||
|
final exifInfo = ref.watch(assetExifProvider(asset)).valueOrNull;
|
||||||
|
|
||||||
|
if (exifInfo?.dateTimeOriginal != null) {
|
||||||
|
(dateTime, _) = applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo.timeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isCurrentYear = dateTime.year == currentYear;
|
||||||
|
final dateFormatted = isCurrentYear ? DateFormat.MMMd().format(dateTime) : DateFormat.yMMMd().format(dateTime);
|
||||||
|
final timeFormatted = DateFormat.jm().format(dateTime);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(dateFormatted, style: context.textTheme.labelLarge?.copyWith(color: Colors.white)),
|
||||||
|
Text(timeFormatted, style: context.textTheme.labelMedium?.copyWith(color: Colors.white70)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ class ActionButtonBuilder {
|
|||||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) {
|
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext) {
|
||||||
final visibleButtons = defaultViewerKebabMenuOrder
|
final visibleButtons = defaultViewerKebabMenuOrder
|
||||||
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
|
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -331,7 +331,7 @@ class ActionButtonBuilder {
|
|||||||
if (lastGroup != null && type.kebabMenuGroup != lastGroup) {
|
if (lastGroup != null && type.kebabMenuGroup != lastGroup) {
|
||||||
result.add(const Divider(height: 1));
|
result.add(const Divider(height: 1));
|
||||||
}
|
}
|
||||||
result.add(type.buildButton(context, buildContext, false, true).build(buildContext, ref));
|
result.add(type.buildButton(context, buildContext, false, true));
|
||||||
lastGroup = type.kebabMenuGroup;
|
lastGroup = type.kebabMenuGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user