mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 09:15:05 +03:00
feat(mobile): implement viewer kebab menu with about option
This commit is contained in:
@@ -1496,6 +1496,7 @@
|
|||||||
"online": "Online",
|
"online": "Online",
|
||||||
"only_favorites": "Only favorites",
|
"only_favorites": "Only favorites",
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
|
"open_bottom_sheet_about": "About",
|
||||||
"open_in_map_view": "Open in map view",
|
"open_in_map_view": "Open in map view",
|
||||||
"open_in_openstreetmap": "Open in OpenStreetMap",
|
"open_in_openstreetmap": "Open in OpenStreetMap",
|
||||||
"open_the_search_filters": "Open the search filters",
|
"open_the_search_filters": "Open the search filters",
|
||||||
|
|||||||
@@ -38,6 +38,29 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context.findAncestorWidgetOfExactType<MenuAnchor>() != null) {
|
||||||
|
final theme = context.themeData;
|
||||||
|
final textStyle = theme.textTheme.bodyMedium;
|
||||||
|
final defaultTextColor = theme.colorScheme.onSurfaceVariant;
|
||||||
|
final effectiveStyle = (textStyle ?? theme.textTheme.bodyMedium)?.copyWith(
|
||||||
|
color: (textStyle?.color ?? defaultTextColor),
|
||||||
|
);
|
||||||
|
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
|
||||||
|
|
||||||
|
return MenuItemButton(
|
||||||
|
style: MenuItemButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
|
visualDensity: const VisualDensity(vertical: -2.5),
|
||||||
|
),
|
||||||
|
trailingIcon: Icon(iconData, size: 18, color: effectiveIconColor),
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(label, style: effectiveStyle),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
child: MaterialButton(
|
child: MaterialButton(
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/constants/enums.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/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_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/album/album_tile.dart';
|
||||||
@@ -20,14 +19,9 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
|
|||||||
import 'package:immich_mobile/providers/infrastructure/action.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/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.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/bytes_units.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@@ -46,29 +40,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
|
||||||
final isOwner = asset is RemoteAsset && asset.ownerId == ref.watch(currentUserProvider)?.id;
|
|
||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
|
||||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
|
||||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
|
||||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
|
||||||
|
|
||||||
final buttonContext = ActionButtonContext(
|
|
||||||
asset: asset,
|
|
||||||
isOwner: isOwner,
|
|
||||||
isArchived: isArchived,
|
|
||||||
isTrashEnabled: isTrashEnable,
|
|
||||||
isInLockedView: isInLockedView,
|
|
||||||
isStacked: asset is RemoteAsset && asset.stackId != null,
|
|
||||||
currentAlbum: currentAlbum,
|
|
||||||
advancedTroubleshooting: advancedTroubleshooting,
|
|
||||||
source: ActionSource.viewer,
|
|
||||||
);
|
|
||||||
|
|
||||||
final actions = ActionButtonBuilder.build(buttonContext);
|
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
actions: actions,
|
actions: [],
|
||||||
slivers: const [_AssetDetailBottomSheet()],
|
slivers: const [_AssetDetailBottomSheet()],
|
||||||
controller: controller,
|
controller: controller,
|
||||||
initialChildSize: initialChildSize,
|
initialChildSize: initialChildSize,
|
||||||
|
|||||||
@@ -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/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/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/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/activity.provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.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)
|
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||||
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
||||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
|
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
|
||||||
const _KebabMenu(),
|
const ViewerKebabMenu(),
|
||||||
];
|
];
|
||||||
|
|
||||||
final lockedViewActions = <Widget>[
|
final lockedViewActions = <Widget>[
|
||||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
||||||
const _KebabMenu(),
|
const ViewerKebabMenu(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
@@ -122,20 +123,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
Size get preferredSize => const Size.fromHeight(60.0);
|
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 {
|
class _AppBarBackButton extends ConsumerWidget {
|
||||||
const _AppBarBackButton();
|
const _AppBarBackButton();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_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';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.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 isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
final isOwner = asset is RemoteAsset && asset.ownerId == ref.watch(currentUserProvider)?.id;
|
||||||
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||||
|
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||||
|
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||||
|
|
||||||
|
final buttonContext = ActionButtonContext(
|
||||||
|
asset: asset,
|
||||||
|
isOwner: isOwner,
|
||||||
|
isArchived: isArchived,
|
||||||
|
isTrashEnabled: isTrashEnable,
|
||||||
|
isInLockedView: isInLockedView,
|
||||||
|
isStacked: asset is RemoteAsset && asset.stackId != null,
|
||||||
|
currentAlbum: currentAlbum,
|
||||||
|
advancedTroubleshooting: advancedTroubleshooting,
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
);
|
||||||
|
|
||||||
|
final theme = context.themeData;
|
||||||
|
final menuChildren = <Widget>[
|
||||||
|
BaseActionButton(
|
||||||
|
label: 'open_bottom_sheet_about'.t(context: context),
|
||||||
|
iconData: Icons.info_outline,
|
||||||
|
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final actions = ActionButtonBuilder.build(
|
||||||
|
buttonContext,
|
||||||
|
actionTypes: ActionButtonBuilder.kebabMenuActionTypes,
|
||||||
|
).map((w) => w.build(context, ref)).expand((action) => [const Divider(height: 0), action]).toList(growable: false);
|
||||||
|
|
||||||
|
if (actions.isNotEmpty) {
|
||||||
|
menuChildren.addAll(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.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/asset/base_asset.model.dart';
|
||||||
@@ -131,7 +131,7 @@ enum ActionButtonType {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildButton(ActionButtonContext context) {
|
ConsumerWidget buildButton(ActionButtonContext context) {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
|
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
|
||||||
ActionButtonType.share => ShareActionButton(source: context.source),
|
ActionButtonType.share => ShareActionButton(source: context.source),
|
||||||
@@ -160,7 +160,19 @@ enum ActionButtonType {
|
|||||||
class ActionButtonBuilder {
|
class ActionButtonBuilder {
|
||||||
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
||||||
|
|
||||||
static List<Widget> build(ActionButtonContext context) {
|
static const _excludedActions = {
|
||||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
ActionButtonType.share,
|
||||||
|
ActionButtonType.archive,
|
||||||
|
ActionButtonType.delete,
|
||||||
|
ActionButtonType.moveToLockFolder,
|
||||||
|
};
|
||||||
|
|
||||||
|
static final List<ActionButtonType> kebabMenuActionTypes = ActionButtonType.values
|
||||||
|
.where((type) => !_excludedActions.contains(type))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
static List<ConsumerWidget> build(ActionButtonContext context, {List<ActionButtonType>? actionTypes}) {
|
||||||
|
final types = actionTypes ?? _actionTypes;
|
||||||
|
return types.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user