feat(album): album view sort order (#14648)
* feat(mobile): album view sort order * feat: add error message * refactor(mobile): album page (#14659) * refactor album page * update lint rule * const record * fix: updating sort order when pull to refresh --------- Co-authored-by: Alex <alex.tran1502@gmail.com> * Move sort toggle button to bottom sheet menu * chore: revert multiselectgrid loading status * chore: revert multiselectgrid loading status --------- Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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/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/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.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<User>> suggestedShareUsers =
|
||||
ref.watch(otherUsersProvider);
|
||||
final sharedUsersList = useState<Set<User>>({});
|
||||
|
||||
addNewUsersHandler() {
|
||||
context.maybePop(sharedUsersList.value.map((e) => e.id).toList());
|
||||
}
|
||||
|
||||
buildTileIcon(User 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<User> 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.withOpacity(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(
|
||||
'select_additional_user_for_sharing_page_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(
|
||||
'share_invite',
|
||||
).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(
|
||||
"share_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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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/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/asset_viewer/render_list.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';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AlbumAssetSelectionPage extends HookConsumerWidget {
|
||||
const AlbumAssetSelectionPage({
|
||||
super.key,
|
||||
required this.existingAssets,
|
||||
this.canDeselect = false,
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final Set<Asset> existingAssets;
|
||||
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
|
||||
final bool canDeselect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final renderList = ref.watch(renderListQueryProvider(query));
|
||||
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(
|
||||
'share_add_photos',
|
||||
style: TextStyle(fontSize: 18),
|
||||
).tr()
|
||||
: const Text(
|
||||
'share_assets_selected',
|
||||
style: TextStyle(fontSize: 18),
|
||||
).tr(args: [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 ? "share_done" : "share_add",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: renderList.widgetWhen(
|
||||
onData: (data) => buildBody(data),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class AlbumControlButton extends ConsumerWidget {
|
||||
void Function() onAddPhotosPressed;
|
||||
void Function() onAddUsersPressed;
|
||||
|
||||
AlbumControlButton({
|
||||
super.key,
|
||||
required this.onAddPhotosPressed,
|
||||
required this.onAddUsersPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final userId = ref.watch(authProvider).userId;
|
||||
final isOwner = ref.watch(
|
||||
currentAlbumProvider.select((album) {
|
||||
return album?.ownerId == userId;
|
||||
}),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
AlbumActionFilledButton(
|
||||
key: const ValueKey('add_photos_button'),
|
||||
iconData: Icons.add_photo_alternate_outlined,
|
||||
onPressed: onAddPhotosPressed,
|
||||
labelText: "share_add_photos".tr(),
|
||||
),
|
||||
if (isOwner)
|
||||
AlbumActionFilledButton(
|
||||
key: const ValueKey('add_users_button'),
|
||||
iconData: Icons.person_add_alt_rounded,
|
||||
onPressed: onAddUsersPressed,
|
||||
labelText: "album_viewer_page_share_add_users".tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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: shared
|
||||
? const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
bottom: 0.0,
|
||||
)
|
||||
: const EdgeInsets.only(left: 16.0, bottom: 8.0),
|
||||
child: Text(
|
||||
_getDateRangeText(startDate, endDate),
|
||||
style: context.textTheme.labelLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
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/extensions/theme_extensions.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/auth.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.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.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) {
|
||||
context.navigateTo(
|
||||
const TabControllerRoute(children: [AlbumsRoute()]),
|
||||
);
|
||||
} else {
|
||||
showErrorMessage();
|
||||
}
|
||||
} catch (_) {
|
||||
showErrorMessage();
|
||||
}
|
||||
|
||||
isProcessing.value = false;
|
||||
}
|
||||
|
||||
void removeUserFromAlbum(User user) async {
|
||||
isProcessing.value = true;
|
||||
|
||||
try {
|
||||
await ref.read(albumProvider.notifier).removeUser(album, user);
|
||||
album.sharedUsers.remove(user);
|
||||
sharedUsers.value = album.sharedUsers.toList();
|
||||
} catch (error) {
|
||||
showErrorMessage();
|
||||
}
|
||||
|
||||
context.pop();
|
||||
isProcessing.value = false;
|
||||
}
|
||||
|
||||
void handleUserClick(User 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) : 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(
|
||||
"shared_album_section_people_owner_label",
|
||||
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("translated_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;
|
||||
}
|
||||
},
|
||||
activeColor: activityEnabled.value
|
||||
? context.primaryColor
|
||||
: context.themeData.disabledColor,
|
||||
dense: true,
|
||||
title: Text(
|
||||
"shared_album_activity_setting_title",
|
||||
style: context.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
).tr(),
|
||||
subtitle: Text(
|
||||
"shared_album_activity_setting_subtitle",
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
buildSectionTitle("shared_album_section_people_title".tr()),
|
||||
buildOwnerInfo(),
|
||||
buildSharedUsersList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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/entities/user.entity.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<User>>(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.toList(growable: false);
|
||||
}),
|
||||
);
|
||||
|
||||
if (sharedUsers.value.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(AlbumOptionsRoute()),
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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/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/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.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<User>>({});
|
||||
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();
|
||||
context.maybePop(true);
|
||||
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(User 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<User> 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.withOpacity(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(
|
||||
'select_user_for_sharing_page_share_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(
|
||||
'share_invite',
|
||||
style: TextStyle(color: context.primaryColor),
|
||||
).tr(),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () async {
|
||||
context.maybePop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: context.primaryColor,
|
||||
),
|
||||
onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum,
|
||||
child: const Text(
|
||||
"share_create_album",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
// color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: suggestedShareUsers.widgetWhen(
|
||||
onData: (users) {
|
||||
return buildUserList(users);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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.headlineMedium),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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_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/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/providers/asset.provider.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 userId = ref.watch(authProvider).userId;
|
||||
final isMultiselecting = ref.watch(multiselectProvider);
|
||||
final isProcessing = useProcessingOverlay();
|
||||
|
||||
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,
|
||||
query: getRemoteAssetQuery(ref),
|
||||
),
|
||||
);
|
||||
|
||||
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) {
|
||||
context.pushRoute(
|
||||
const ActivitiesRoute(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
MultiselectGrid(
|
||||
key: const ValueKey("albumViewerMultiselectGrid"),
|
||||
renderListProvider: albumRenderlistProvider(album.id),
|
||||
topWidget: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AlbumTitle(
|
||||
key: const ValueKey("albumTitle"),
|
||||
titleFocusNode: titleFocusNode,
|
||||
),
|
||||
const AlbumDateRange(),
|
||||
const AlbumSharedUserIcons(),
|
||||
if (album.isRemote)
|
||||
AlbumControlButton(
|
||||
key: const ValueKey("albumControlButton"),
|
||||
onAddPhotosPressed: onAddPhotosPressed,
|
||||
onAddUsersPressed: onAddUsersPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
userId: userId,
|
||||
onAddPhotos: onAddPhotosPressed,
|
||||
onAddUsers: onAddUsersPressed,
|
||||
onActivities: onActivitiesPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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';
|
||||
|
||||
@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, (_, __) {});
|
||||
|
||||
ref.listen(albumWatcher(albumId), (_, albumFuture) {
|
||||
albumFuture.whenData(
|
||||
(value) => ref.read(currentAlbumProvider.notifier).set(value),
|
||||
);
|
||||
});
|
||||
|
||||
return const Scaffold(body: AlbumViewer());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user