Source Code added

This commit is contained in:
Fr4nz D13trich 2026-02-02 15:06:40 +01:00
parent 800376eafd
commit 9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions

View file

@ -0,0 +1,115 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@RoutePage()
class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
final Album album;
const AlbumAdditionalSharedUserSelectionPage({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<List<UserDto>> suggestedShareUsers = ref.watch(otherUsersProvider);
final sharedUsersList = useState<Set<UserDto>>({});
addNewUsersHandler() {
context.maybePop(sharedUsersList.value.map((e) => e.id).toList());
}
buildTileIcon(UserDto user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25));
} else {
return UserCircleAvatar(user: user);
}
}
buildUserList(List<UserDto> users) {
List<Widget> usersChip = [];
for (var user in sharedUsersList.value) {
usersChip.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip(
backgroundColor: context.primaryColor.withValues(alpha: 0.15),
label: Text(user.name, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
),
),
);
}
return ListView(
children: [
Wrap(children: [...usersChip]),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'suggestions'.tr(),
style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
),
),
ListView.builder(
primary: false,
shrinkWrap: true,
itemBuilder: ((context, index) {
return ListTile(
leading: buildTileIcon(users[index]),
dense: true,
title: Text(users[index].name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
subtitle: Text(users[index].email, style: const TextStyle(fontSize: 12)),
onTap: () {
if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value
.where((selectedUser) => selectedUser.id != users[index].id)
.toSet();
} else {
sharedUsersList.value = {...sharedUsersList.value, users[index]};
}
},
);
}),
itemCount: users.length,
),
],
);
}
return Scaffold(
appBar: AppBar(
title: const Text('invite_to_album').tr(),
elevation: 0,
centerTitle: false,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
context.maybePop(null);
},
),
actions: [
TextButton(
onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler,
child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(),
),
],
),
body: suggestedShareUsers.widgetWhen(
onData: (users) {
for (var sharedUsers in album.sharedUsers) {
users.removeWhere((u) => u.id == sharedUsers.id || u.id == album.ownerId);
}
return buildUserList(users);
},
),
);
}
}

View file

@ -0,0 +1,74 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/providers/timeline.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart';
@RoutePage()
class AlbumAssetSelectionPage extends HookConsumerWidget {
const AlbumAssetSelectionPage({super.key, required this.existingAssets, this.canDeselect = false});
final Set<Asset> existingAssets;
final bool canDeselect;
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetSelectionRenderList = ref.watch(assetSelectionTimelineProvider);
final selected = useState<Set<Asset>>(existingAssets);
final selectionEnabledHook = useState(true);
Widget buildBody(RenderList renderList) {
return ImmichAssetGrid(
renderList: renderList,
listener: (active, assets) {
selectionEnabledHook.value = active;
selected.value = assets;
},
selectionActive: true,
preselectedAssets: existingAssets,
canDeselect: canDeselect,
showMultiSelectIndicator: false,
);
}
return Scaffold(
appBar: AppBar(
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
AutoRouter.of(context).popForced(null);
},
),
title: selected.value.isEmpty
? const Text('add_photos', style: TextStyle(fontSize: 18)).tr()
: const Text(
'share_assets_selected',
style: TextStyle(fontSize: 18),
).tr(namedArgs: {'count': selected.value.length.toString()}),
centerTitle: false,
actions: [
if (selected.value.isNotEmpty || canDeselect)
TextButton(
onPressed: () {
var payload = AssetSelectionPageResult(selectedAssets: selected.value);
AutoRouter.of(context).popForced<AssetSelectionPageResult>(payload);
},
child: Text(
canDeselect ? "done" : "add",
style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor),
).tr(),
),
],
),
body: assetSelectionRenderList.widgetWhen(onData: (data) => buildBody(data)),
);
}
}

View file

@ -0,0 +1,37 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
class AlbumControlButton extends ConsumerWidget {
final void Function()? onAddPhotosPressed;
final void Function()? onAddUsersPressed;
const AlbumControlButton({super.key, this.onAddPhotosPressed, this.onAddUsersPressed});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SizedBox(
height: 36,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
if (onAddPhotosPressed != null)
AlbumActionFilledButton(
key: const ValueKey('add_photos_button'),
iconData: Icons.add_photo_alternate_outlined,
onPressed: onAddPhotosPressed,
labelText: "add_photos".tr(),
),
if (onAddUsersPressed != null)
AlbumActionFilledButton(
key: const ValueKey('add_users_button'),
iconData: Icons.person_add_alt_rounded,
onPressed: onAddUsersPressed,
labelText: "album_viewer_page_share_add_users".tr(),
),
],
),
);
}
}

View file

@ -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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
class AlbumDateRange extends ConsumerWidget {
const AlbumDateRange({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(
currentAlbumProvider.select((album) {
if (album == null || album.assets.isEmpty) {
return null;
}
final startDate = album.startDate;
final endDate = album.endDate;
if (startDate == null || endDate == null) {
return null;
}
return (startDate, endDate, album.shared);
}),
);
if (data == null) {
return const SizedBox();
}
final (startDate, endDate, shared) = data;
return Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
_getDateRangeText(startDate, endDate),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant),
),
);
}
@pragma('vm:prefer-inline')
String _getDateRangeText(DateTime startDate, DateTime endDate) {
if (startDate.day == endDate.day && startDate.month == endDate.month && startDate.year == endDate.year) {
return DateFormat.yMMMd().format(startDate);
}
final String startDateText = (startDate.year == endDate.year ? DateFormat.MMMd() : DateFormat.yMMMd()).format(
startDate,
);
final String endDateText = DateFormat.yMMMd().format(endDate);
return "$startDateText - $endDateText";
}
}

View file

@ -0,0 +1,42 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
class AlbumDescription extends ConsumerWidget {
const AlbumDescription({super.key, required this.descriptionFocusNode});
final FocusNode descriptionFocusNode;
@override
Widget build(BuildContext context, WidgetRef ref) {
final userId = ref.watch(authProvider).userId;
final (isOwner, isRemote, albumDescription) = ref.watch(
currentAlbumProvider.select((album) {
if (album == null) {
return const (false, false, '');
}
return (album.ownerId == userId, album.isRemote, album.description);
}),
);
if (isOwner && isRemote) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: AlbumViewerEditableDescription(
albumDescription: albumDescription ?? 'add_a_description'.tr(),
descriptionFocusNode: descriptionFocusNode,
),
);
}
return Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Text(albumDescription ?? 'add_a_description'.tr(), style: context.textTheme.bodyLarge),
);
}
}

View file

@ -0,0 +1,192 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.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/user.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@RoutePage()
class AlbumOptionsPage extends HookConsumerWidget {
const AlbumOptionsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final album = ref.watch(currentAlbumProvider);
if (album == null) {
return const SizedBox();
}
final sharedUsers = useState(album.sharedUsers.map((u) => u.toDto()).toList());
final owner = album.owner.value;
final userId = ref.watch(authProvider).userId;
final activityEnabled = useState(album.activityEnabled);
final isProcessing = useProcessingOverlay();
final isOwner = owner?.id == userId;
void showErrorMessage() {
context.pop();
ImmichToast.show(
context: context,
msg: "shared_album_section_people_action_error".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
void leaveAlbum() async {
isProcessing.value = true;
try {
final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album);
if (isSuccess) {
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
} else {
showErrorMessage();
}
} catch (_) {
showErrorMessage();
}
isProcessing.value = false;
}
void removeUserFromAlbum(UserDto user) async {
isProcessing.value = true;
try {
await ref.read(albumProvider.notifier).removeUser(album, user);
album.sharedUsers.remove(entity.User.fromDto(user));
sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList();
} catch (error) {
showErrorMessage();
}
context.pop();
isProcessing.value = false;
}
void handleUserClick(UserDto user) {
var actions = [];
if (user.id == userId) {
actions = [
ListTile(
leading: const Icon(Icons.exit_to_app_rounded),
title: const Text("shared_album_section_people_action_leave").tr(),
onTap: leaveAlbum,
),
];
}
if (isOwner) {
actions = [
ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: const Text("shared_album_section_people_action_remove_user").tr(),
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() {
return ListTile(
leading: owner != null ? UserCircleAvatar(user: owner.toDto()) : const SizedBox(),
title: Text(album.owner.value?.name ?? "", style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(album.owner.value?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
trailing: Text("owner", style: context.textTheme.labelLarge).tr(),
);
}
buildSharedUsersList() {
return ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: sharedUsers.value.length,
itemBuilder: (context, index) {
final user = sharedUsers.value[index];
return ListTile(
leading: UserCircleAvatar(user: user, radius: 22),
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,
);
},
);
}
buildSectionTitle(String text) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Text(text, style: context.textTheme.bodySmall),
);
}
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () => context.maybePop(null),
),
centerTitle: true,
title: Text("options".tr()),
),
body: ListView(
children: [
if (isOwner && album.shared)
SwitchListTile.adaptive(
value: activityEnabled.value,
onChanged: (bool value) async {
activityEnabled.value = value;
if (await ref.read(albumProvider.notifier).setActivitystatus(album, value)) {
album.activityEnabled = value;
}
},
activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
dense: true,
title: Text(
"comments_and_likes",
style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
).tr(),
subtitle: Text(
"let_others_respond",
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
).tr(),
),
buildSectionTitle("shared_album_section_people_title".tr()),
buildOwnerInfo(),
buildSharedUsersList(),
],
),
);
}
}

View file

@ -0,0 +1,52 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
class AlbumSharedUserIcons extends HookConsumerWidget {
const AlbumSharedUserIcons({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedUsers = useRef<List<UserDto>>(const []);
sharedUsers.value = ref.watch(
currentAlbumProvider.select((album) {
if (album == null) {
return const [];
}
if (album.sharedUsers.length == sharedUsers.value.length) {
return sharedUsers.value;
}
return album.sharedUsers.map((u) => u.toDto()).toList(growable: false);
}),
);
if (sharedUsers.value.isEmpty) {
return const SizedBox();
}
return GestureDetector(
onTap: () => context.pushRoute(const AlbumOptionsRoute()),
child: SizedBox(
height: 50,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16, bottom: 8),
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: UserCircleAvatar(user: sharedUsers.value[index], radius: 18, size: 36),
);
}),
itemCount: sharedUsers.value.length,
),
),
);
}
}

View file

@ -0,0 +1,140 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_title.provider.dart';
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@RoutePage()
class AlbumSharedUserSelectionPage extends HookConsumerWidget {
const AlbumSharedUserSelectionPage({super.key, required this.assets});
final Set<Asset> assets;
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedUsersList = useState<Set<UserDto>>({});
final suggestedShareUsers = ref.watch(otherUsersProvider);
createSharedAlbum() async {
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(ref.watch(albumTitleProvider), assets);
if (newAlbum != null) {
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
unawaited(context.maybePop(true));
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
}
ScaffoldMessenger(
child: SnackBar(
content: Text(
'select_user_for_sharing_page_err_album',
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
).tr(),
),
);
}
buildTileIcon(UserDto user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25));
} else {
return UserCircleAvatar(user: user);
}
}
buildUserList(List<UserDto> users) {
List<Widget> usersChip = [];
for (var user in sharedUsersList.value) {
usersChip.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip(
backgroundColor: context.primaryColor.withValues(alpha: 0.15),
label: Text(
user.email,
style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold),
),
),
),
);
}
return ListView(
children: [
Wrap(children: [...usersChip]),
Padding(
padding: const EdgeInsets.all(16.0),
child: const Text(
'suggestions',
style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
).tr(),
),
ListView.builder(
primary: false,
shrinkWrap: true,
itemBuilder: ((context, index) {
return ListTile(
leading: buildTileIcon(users[index]),
title: Text(users[index].email, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
onTap: () {
if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value
.where((selectedUser) => selectedUser.id != users[index].id)
.toSet();
} else {
sharedUsersList.value = {...sharedUsersList.value, users[index]};
}
},
);
}),
itemCount: users.length,
),
],
);
}
return Scaffold(
appBar: AppBar(
title: Text('invite_to_album', style: TextStyle(color: context.primaryColor)).tr(),
elevation: 0,
centerTitle: false,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
unawaited(context.maybePop());
},
),
actions: [
TextButton(
style: TextButton.styleFrom(foregroundColor: context.primaryColor),
onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum,
child: const Text(
"create_album",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
// color: context.primaryColor,
),
).tr(),
),
],
),
body: suggestedShareUsers.widgetWhen(
onData: (users) {
return buildUserList(users);
},
),
);
}
}

View file

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
class AlbumTitle extends ConsumerWidget {
const AlbumTitle({super.key, required this.titleFocusNode});
final FocusNode titleFocusNode;
@override
Widget build(BuildContext context, WidgetRef ref) {
final userId = ref.watch(authProvider).userId;
final (isOwner, isRemote, albumName) = ref.watch(
currentAlbumProvider.select((album) {
if (album == null) {
return const (false, false, '');
}
return (album.ownerId == userId, album.isRemote, album.name);
}),
);
if (isOwner && isRemote) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: AlbumViewerEditableTitle(albumName: albumName, titleFocusNode: titleFocusNode),
);
}
return Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Text(albumName, style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700)),
);
}
}

View file

@ -0,0 +1,165 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
import 'package:immich_mobile/pages/album/album_control_button.dart';
import 'package:immich_mobile/pages/album/album_date_range.dart';
import 'package:immich_mobile/pages/album/album_description.dart';
import 'package:immich_mobile/pages/album/album_shared_user_icons.dart';
import 'package:immich_mobile/pages/album/album_title.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/timeline.provider.dart';
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumViewer extends HookConsumerWidget {
const AlbumViewer({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final album = ref.watch(currentAlbumProvider);
if (album == null) {
return const SizedBox();
}
final titleFocusNode = useFocusNode();
final descriptionFocusNode = useFocusNode();
final userId = ref.watch(authProvider).userId;
final isMultiselecting = ref.watch(multiselectProvider);
final isProcessing = useProcessingOverlay();
final isOwner = ref.watch(
currentAlbumProvider.select((album) {
return album?.ownerId == userId;
}),
);
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
final bool isSuccess = await ref.read(albumProvider.notifier).removeAsset(album, assets);
if (!isSuccess) {
ImmichToast.show(
context: context,
msg: "album_viewer_appbar_share_err_remove".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
return isSuccess;
}
/// Find out if the assets in album exist on the device
/// If they exist, add to selected asset state to show they are already selected.
void onAddPhotosPressed() async {
AssetSelectionPageResult? returnPayload = await context.pushRoute<AssetSelectionPageResult?>(
AlbumAssetSelectionRoute(existingAssets: album.assets, canDeselect: false),
);
if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) {
// Check if there is new assets add
isProcessing.value = true;
await ref.watch(albumProvider.notifier).addAssets(album, returnPayload.selectedAssets);
isProcessing.value = false;
}
}
void onAddUsersPressed() async {
List<String>? sharedUserIds = await context.pushRoute<List<String>?>(
AlbumAdditionalSharedUserSelectionRoute(album: album),
);
if (sharedUserIds != null) {
isProcessing.value = true;
await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds);
isProcessing.value = false;
}
}
onActivitiesPressed() {
if (album.remoteId != null) {
ref.read(currentAssetProvider.notifier).set(null);
context.pushRoute(const ActivitiesRoute());
}
}
return Stack(
children: [
MultiselectGrid(
key: const ValueKey("albumViewerMultiselectGrid"),
renderListProvider: albumTimelineProvider(album.id),
topWidget: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
context.primaryColor.withValues(alpha: 0.06),
context.primaryColor.withValues(alpha: 0.04),
Colors.indigo.withValues(alpha: 0.02),
Colors.transparent,
],
stops: const [0.0, 0.3, 0.7, 1.0],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 32),
const AlbumDateRange(),
AlbumTitle(key: const ValueKey("albumTitle"), titleFocusNode: titleFocusNode),
AlbumDescription(key: const ValueKey("albumDescription"), descriptionFocusNode: descriptionFocusNode),
const AlbumSharedUserIcons(),
if (album.isRemote)
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: AlbumControlButton(
key: const ValueKey("albumControlButton"),
onAddPhotosPressed: onAddPhotosPressed,
onAddUsersPressed: isOwner ? onAddUsersPressed : null,
),
),
const SizedBox(height: 8),
],
),
),
onRemoveFromAlbum: onRemoveFromAlbumPressed,
editEnabled: album.ownerId == userId,
),
AnimatedPositioned(
key: const ValueKey("albumViewerAppbarPositioned"),
duration: const Duration(milliseconds: 300),
top: isMultiselecting ? -(kToolbarHeight + context.padding.top) : 0,
left: 0,
right: 0,
child: AlbumViewerAppbar(
key: const ValueKey("albumViewerAppbar"),
titleFocusNode: titleFocusNode,
descriptionFocusNode: descriptionFocusNode,
userId: userId,
onAddPhotos: onAddPhotosPressed,
onAddUsers: onAddUsersPressed,
onActivities: onActivitiesPressed,
),
),
],
);
}
}

View file

@ -0,0 +1,29 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/pages/album/album_viewer.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/timeline.provider.dart';
@RoutePage()
class AlbumViewerPage extends HookConsumerWidget {
final int albumId;
const AlbumViewerPage({super.key, required this.albumId});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Listen provider to prevent autoDispose when navigating to other routes from within the viewer page
ref.listen(currentAlbumProvider, (_, __) {});
// This call helps rendering the asset selection instantly
ref.listen(assetSelectionTimelineProvider, (_, __) {});
ref.listen(albumWatcher(albumId), (_, albumFuture) {
albumFuture.whenData((value) => ref.read(currentAlbumProvider.notifier).set(value));
});
return const Scaffold(body: AlbumViewer());
}
}