Files
immich/mobile/lib/presentation/pages/drift_album_options.page.dart
Thomas d2682f160e fix(mobile): inherit toolbar opacity (#25694)
Some widgets, like Icon widgets, automatically inherit opacity from the
icon theme in the context. Many other widgets however, do not. The
Immich logo, profile picture, and backup badge are examples of widgets
of this.

All unsupported toolbar widgets have been updated to support inheriting
the opacity from the icon theme.

IconButtons internally animate properties like opacity, which is kind of
nice, but means we have to do more work to replicate that behaviour for
other widgets. In most cases, we can simply use an IconButton widget and
forward the correct opacity. The Immich logo however is not a button,
and therefore we need to use a custom TweenAnimationBuilder.

All widgets are using efficient, native opacity rather than the heavy
Opacity widget.
2026-02-16 09:54:57 +05:30

240 lines
8.5 KiB
Dart

import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@RoutePage()
class DriftAlbumOptionsPage extends HookConsumerWidget {
final RemoteAlbum album;
const DriftAlbumOptionsPage({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedUsersAsync = ref.watch(remoteAlbumSharedUsersProvider(album.id));
final userId = ref.watch(authProvider).userId;
final activityEnabled = useState(album.isActivityEnabled);
final isOwner = album.ownerId == userId;
void showErrorMessage() {
context.pop();
ImmichToast.show(
context: context,
msg: "shared_album_section_people_action_error".t(context: context),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
void leaveAlbum() async {
try {
await ref.read(remoteAlbumProvider.notifier).leaveAlbum(album.id, userId: userId);
unawaited(context.navigateTo(const DriftAlbumsRoute()));
} catch (_) {
showErrorMessage();
}
}
void removeUserFromAlbum(UserDto user) async {
try {
await ref.read(remoteAlbumProvider.notifier).removeUser(album.id, user.id);
ref.invalidate(remoteAlbumSharedUsersProvider(album.id));
} catch (_) {
showErrorMessage();
}
context.pop();
}
Future<void> addUsers() async {
final newUsers = await context.pushRoute<List<String>>(DriftUserSelectionRoute(album: album));
if (newUsers == null || newUsers.isEmpty) {
return;
}
try {
await ref.read(remoteAlbumProvider.notifier).addUsers(album.id, newUsers);
if (newUsers.isNotEmpty) {
ImmichToast.show(
context: context,
msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}),
toastType: ToastType.success,
);
}
ref.invalidate(remoteAlbumSharedUsersProvider(album.id));
} catch (e) {
ImmichToast.show(
context: context,
msg: "Failed to add users to album: ${e.toString()}",
toastType: ToastType.error,
);
}
}
void handleUserClick(UserDto user) {
var actions = [];
if (user.id == userId) {
actions = [
ListTile(
leading: const Icon(Icons.exit_to_app_rounded),
title: const Text("leave_album").t(context: context),
onTap: leaveAlbum,
),
];
}
if (isOwner) {
actions = [
ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: const Text("remove_user").t(context: context),
onTap: () => removeUserFromAlbum(user),
),
];
}
showModalBottomSheet(
backgroundColor: context.colorScheme.surfaceContainer,
isScrollControlled: false,
context: context,
builder: (context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]),
),
);
},
);
}
buildOwnerInfo() {
if (isOwner) {
final owner = ref.watch(currentUserProvider);
return ListTile(
leading: owner != null ? UserCircleAvatar(user: owner) : const SizedBox(),
title: Text(album.ownerName, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(owner?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context),
);
} else {
final usersProvider = ref.watch(driftUsersProvider);
return usersProvider.maybeWhen(
data: (users) {
final user = users.firstWhereOrNull((u) => u.id == album.ownerId);
if (user == null) {
return const SizedBox();
}
return ListTile(
leading: UserCircleAvatar(user: user),
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context),
);
},
orElse: () => const SizedBox(),
);
}
}
buildSharedUsersList() {
return sharedUsersAsync.maybeWhen(
data: (sharedUsers) => ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: sharedUsers.length,
itemBuilder: (context, index) {
final user = sharedUsers[index];
return ListTile(
leading: UserCircleAvatar(user: user),
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(),
onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null,
);
},
),
orElse: () => const Center(child: CircularProgressIndicator()),
);
}
buildSectionTitle(String text) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Text(text, style: context.textTheme.bodySmall),
);
}
return ProviderScope(
overrides: [currentRemoteAlbumScopedProvider.overrideWithValue(album)],
child: Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () => context.maybePop(null),
),
centerTitle: true,
title: Text("options".t(context: context)),
),
body: ListView(
children: [
const SizedBox(height: 8),
if (isOwner)
SwitchListTile.adaptive(
value: activityEnabled.value,
onChanged: (bool value) async {
activityEnabled.value = value;
await ref.read(remoteAlbumProvider.notifier).setActivityStatus(album.id, value);
},
activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
dense: true,
title: Text(
"comments_and_likes",
style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
).t(context: context),
subtitle: Text(
"let_others_respond",
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
).t(context: context),
),
buildSectionTitle("shared_album_section_people_title".t(context: context)),
if (isOwner) ...[
ListTile(
leading: const Icon(Icons.person_add_rounded),
title: Text("invite_people".t(context: context)),
onTap: () async => addUsers(),
),
const Divider(indent: 16),
],
buildOwnerInfo(),
buildSharedUsersList(),
],
),
),
);
}
}