mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'feature/kebab-menu-2' into feature/rearrange-buttons-2
This commit is contained in:
@@ -7,7 +7,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.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';
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
class ViewerOpenBottomSheetEvent extends Event {
|
||||
final bool activitiesMode;
|
||||
const ViewerOpenBottomSheetEvent({this.activitiesMode = false});
|
||||
}
|
||||
|
||||
class ViewerReloadAssetEvent extends Event {
|
||||
const ViewerReloadAssetEvent();
|
||||
}
|
||||
|
||||
class AssetViewerState {
|
||||
final int backgroundOpacity;
|
||||
final bool showingBottomSheet;
|
||||
|
||||
@@ -4,20 +4,20 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
@@ -30,6 +30,7 @@ import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
const _kSeparator = ' • ';
|
||||
@@ -86,19 +87,27 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
const _AssetDetailBottomSheet();
|
||||
|
||||
String _getDateTime(BuildContext ctx, BaseAsset asset) {
|
||||
final dateTime = asset.createdAt.toLocal();
|
||||
String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) {
|
||||
DateTime dateTime = asset.createdAt.toLocal();
|
||||
Duration timeZoneOffset = dateTime.timeZoneOffset;
|
||||
|
||||
// Use EXIF timezone information if available (matching web app behavior)
|
||||
if (exifInfo?.dateTimeOriginal != null) {
|
||||
(dateTime, timeZoneOffset) = applyTimezoneOffset(
|
||||
dateTime: exifInfo!.dateTimeOriginal!,
|
||||
timeZone: exifInfo.timeZone,
|
||||
);
|
||||
}
|
||||
|
||||
final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime);
|
||||
final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime);
|
||||
final timezone = dateTime.timeZoneOffset.isNegative
|
||||
? 'UTC-${dateTime.timeZoneOffset.inHours.abs().toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}'
|
||||
: 'UTC+${dateTime.timeZoneOffset.inHours.toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}';
|
||||
final timezone = 'GMT${timeZoneOffset.formatAsOffset()}';
|
||||
return '$date$_kSeparator$time $timezone';
|
||||
}
|
||||
|
||||
String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) {
|
||||
final height = asset.height ?? exifInfo?.height;
|
||||
final width = asset.width ?? exifInfo?.width;
|
||||
final height = asset.height;
|
||||
final width = asset.width;
|
||||
final resolution = (width != null && height != null) ? "${width.toInt()} x ${height.toInt()}" : null;
|
||||
final fileSize = exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null;
|
||||
|
||||
@@ -181,7 +190,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (albums.isNotEmpty)
|
||||
_SheetTile(
|
||||
SheetTile(
|
||||
title: 'appears_in'.t(context: context).toUpperCase(),
|
||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
@@ -233,7 +242,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
future: assetMediaRepository.getOriginalFilename(asset.id),
|
||||
builder: (context, snapshot) {
|
||||
final displayName = snapshot.data ?? asset.name;
|
||||
return _SheetTile(
|
||||
return SheetTile(
|
||||
title: displayName,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(
|
||||
@@ -242,15 +251,15 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
color: context.textTheme.labelLarge?.color,
|
||||
),
|
||||
subtitle: _getFileInfo(asset, exifInfo),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// For remote assets, use the name directly
|
||||
return _SheetTile(
|
||||
return SheetTile(
|
||||
title: asset.name,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(
|
||||
@@ -259,8 +268,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
color: context.textTheme.labelLarge?.color,
|
||||
),
|
||||
subtitle: _getFileInfo(asset, exifInfo),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -269,9 +278,9 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
return SliverList.list(
|
||||
children: [
|
||||
// Asset Date and Time
|
||||
_SheetTile(
|
||||
title: _getDateTime(context, asset),
|
||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
SheetTile(
|
||||
title: _getDateTime(context, asset, exifInfo),
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
||||
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null,
|
||||
),
|
||||
@@ -279,8 +288,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
const SheetPeopleDetails(),
|
||||
const SheetLocationDetails(),
|
||||
// Details header
|
||||
_SheetTile(
|
||||
title: 'exif_bottom_sheet_details'.t(context: context),
|
||||
SheetTile(
|
||||
title: 'details'.t(context: context).toUpperCase(),
|
||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -289,29 +298,33 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
// File info
|
||||
buildFileInfoTile(),
|
||||
// Camera info
|
||||
if (cameraTitle != null)
|
||||
_SheetTile(
|
||||
if (cameraTitle != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SheetTile(
|
||||
title: cameraTitle,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
),
|
||||
],
|
||||
// Lens info
|
||||
if (lensTitle != null)
|
||||
_SheetTile(
|
||||
if (lensTitle != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SheetTile(
|
||||
title: lensTitle,
|
||||
titleStyle: context.textTheme.labelLarge,
|
||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||
subtitle: _getLensInfoSubtitle(exifInfo),
|
||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
||||
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
),
|
||||
],
|
||||
// Appears in (Albums)
|
||||
_buildAppearsInList(ref, context),
|
||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||
// padding at the bottom to avoid cut-off
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
@@ -319,77 +332,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SheetTile extends ConsumerWidget {
|
||||
final String title;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final String? subtitle;
|
||||
final TextStyle? titleStyle;
|
||||
final TextStyle? subtitleStyle;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _SheetTile({
|
||||
required this.title,
|
||||
this.titleStyle,
|
||||
this.leading,
|
||||
this.subtitle,
|
||||
this.subtitleStyle,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
void copyTitle(BuildContext context, WidgetRef ref) {
|
||||
Clipboard.setData(ClipboardData(text: title));
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'copied_to_clipboard'.t(context: context),
|
||||
toastType: ToastType.info,
|
||||
);
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final Widget titleWidget;
|
||||
if (leading == null) {
|
||||
titleWidget = LimitedBox(
|
||||
maxWidth: double.infinity,
|
||||
child: Text(title, style: titleStyle),
|
||||
);
|
||||
} else {
|
||||
titleWidget = Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Text(title, style: titleStyle),
|
||||
);
|
||||
}
|
||||
|
||||
final Widget? subtitleWidget;
|
||||
if (leading == null && subtitle != null) {
|
||||
subtitleWidget = Text(subtitle!, style: subtitleStyle);
|
||||
} else if (leading != null && subtitle != null) {
|
||||
subtitleWidget = Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Text(subtitle!, style: subtitleStyle),
|
||||
);
|
||||
} else {
|
||||
subtitleWidget = null;
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget),
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
contentPadding: leading == null ? null : const EdgeInsets.only(left: 25),
|
||||
subtitle: subtitleWidget,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SheetAssetDescription extends ConsumerStatefulWidget {
|
||||
final ExifInfo exif;
|
||||
final bool isEditable;
|
||||
|
||||
+57
-33
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
@@ -16,8 +19,6 @@ class SheetLocationDetails extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||
BaseAsset? asset;
|
||||
ExifInfo? exifInfo;
|
||||
MapLibreMapController? _mapController;
|
||||
|
||||
String? _getLocationName(ExifInfo? exifInfo) {
|
||||
@@ -39,14 +40,11 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||
}
|
||||
|
||||
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
|
||||
asset = ref.read(currentAssetNotifier);
|
||||
setState(() {
|
||||
exifInfo = current.valueOrNull;
|
||||
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
|
||||
if (exifInfo != null && hasCoordinates) {
|
||||
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exifInfo!.latitude!, exifInfo!.longitude!)));
|
||||
}
|
||||
});
|
||||
final currentExif = current.valueOrNull;
|
||||
|
||||
if (currentExif != null && currentExif.hasCoordinates) {
|
||||
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -55,45 +53,71 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||
ref.listenManual(currentAssetExifProvider, _onExifChanged, fireImmediately: true);
|
||||
}
|
||||
|
||||
void editLocation() async {
|
||||
await ref.read(actionProvider.notifier).editLocation(ActionSource.viewer, context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final asset = ref.watch(currentAssetNotifier);
|
||||
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
|
||||
|
||||
// Guard no lat/lng
|
||||
if (!hasCoordinates || (asset != null && asset is LocalAsset && asset!.hasRemote)) {
|
||||
// Guard local assets
|
||||
if (asset != null && asset is LocalAsset && asset.hasRemote) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final remoteId = asset is LocalAsset ? (asset as LocalAsset).remoteId : (asset as RemoteAsset).id;
|
||||
final remoteId = asset is LocalAsset ? asset.remoteId : (asset as RemoteAsset).id;
|
||||
final locationName = _getLocationName(exifInfo);
|
||||
final coordinates = "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}";
|
||||
final coordinates = "${exifInfo?.latitude?.toStringAsFixed(4)}, ${exifInfo?.longitude?.toStringAsFixed(4)}";
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: context.isMobile ? 16.0 : 56.0),
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
"exif_bottom_sheet_location".t(context: context),
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
SheetTile(
|
||||
title: 'location'.t(context: context).toUpperCase(),
|
||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null,
|
||||
onTap: editLocation,
|
||||
),
|
||||
if (hasCoordinates)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: context.isMobile ? 16.0 : 56.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated),
|
||||
const SizedBox(height: 16),
|
||||
if (locationName != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Text(locationName, style: context.textTheme.labelLarge),
|
||||
),
|
||||
Text(
|
||||
coordinates,
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated),
|
||||
const SizedBox(height: 15),
|
||||
if (locationName != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Text(locationName, style: context.textTheme.labelLarge),
|
||||
if (!hasCoordinates)
|
||||
SheetTile(
|
||||
title: "add_a_location".t(context: context),
|
||||
titleStyle: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
leading: const Icon(Icons.location_off),
|
||||
onTap: editLocation,
|
||||
),
|
||||
Text(
|
||||
coordinates,
|
||||
style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class SheetTile extends ConsumerWidget {
|
||||
final String title;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final String? subtitle;
|
||||
final TextStyle? titleStyle;
|
||||
final TextStyle? subtitleStyle;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const SheetTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.titleStyle,
|
||||
this.leading,
|
||||
this.subtitle,
|
||||
this.subtitleStyle,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
void copyTitle(BuildContext context, WidgetRef ref) {
|
||||
Clipboard.setData(ClipboardData(text: title));
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'copied_to_clipboard'.t(context: context),
|
||||
toastType: ToastType.info,
|
||||
);
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final Widget titleWidget;
|
||||
if (leading == null) {
|
||||
titleWidget = LimitedBox(
|
||||
maxWidth: double.infinity,
|
||||
child: Text(title, style: titleStyle),
|
||||
);
|
||||
} else {
|
||||
titleWidget = Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(left: 15, right: 15),
|
||||
child: Text(title, style: titleStyle),
|
||||
);
|
||||
}
|
||||
|
||||
final Widget? subtitleWidget;
|
||||
if (leading == null && subtitle != null) {
|
||||
subtitleWidget = Text(subtitle!, style: subtitleStyle);
|
||||
} else if (leading != null && subtitle != null) {
|
||||
subtitleWidget = Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Text(subtitle!, style: subtitleStyle),
|
||||
);
|
||||
} else {
|
||||
subtitleWidget = null;
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget),
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
contentPadding: leading == null ? null : const EdgeInsets.only(left: 25),
|
||||
subtitle: subtitleWidget,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.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';
|
||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_actio
|
||||
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';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
@@ -89,12 +90,12 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
|
||||
const _KebabMenu(),
|
||||
const ViewerKebabMenu(),
|
||||
];
|
||||
|
||||
final lockedViewActions = <Widget>[
|
||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
||||
const _KebabMenu(),
|
||||
const ViewerKebabMenu(),
|
||||
];
|
||||
|
||||
return IgnorePointer(
|
||||
@@ -122,20 +123,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
Size get preferredSize => const Size.fromHeight(60.0);
|
||||
}
|
||||
|
||||
class _KebabMenu extends ConsumerWidget {
|
||||
const _KebabMenu();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
EventStream.shared.emit(const ViewerOpenBottomSheetEvent());
|
||||
},
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBarBackButton extends ConsumerWidget {
|
||||
const _AppBarBackButton();
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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/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/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
|
||||
class ViewerKebabMenu extends ConsumerWidget {
|
||||
const ViewerKebabMenu({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asset = ref.watch(currentAssetNotifier);
|
||||
if (asset == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = context.themeData;
|
||||
final menuChildren = <Widget>[
|
||||
BaseActionButton(
|
||||
label: 'open_bottom_sheet_info'.tr(),
|
||||
iconData: Icons.info_outline,
|
||||
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
|
||||
),
|
||||
];
|
||||
|
||||
return MenuAnchor(
|
||||
style: MenuStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(theme.scaffoldBackgroundColor),
|
||||
elevation: const WidgetStatePropertyAll(4),
|
||||
shape: const WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 2)),
|
||||
),
|
||||
menuChildren: menuChildren,
|
||||
builder: (context, controller, child) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user