mirror of
https://github.com/immich-app/immich.git
synced 2025-06-03 13:46:47 -04:00
feat(mobile): Improve album UI and Interactions (#3754)
* fix: outlick editable field does not change edit icon * fix: unfocus on submit change album name * styling * styling * confirm dialog * Confirm deletion * render user * user avatar with image * use UserCircleAvatar * rights * stlying * remove/leave options * styling * state management --------- Co-authored-by: Alex Tran <Alex.Tran@conductix.com>
This commit is contained in:
parent
2ff71b0d27
commit
2de30e34f4
@ -300,5 +300,6 @@
|
|||||||
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
||||||
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
||||||
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
|
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
||||||
}
|
"translated_text_options": "Options"
|
||||||
|
}
|
||||||
|
@ -56,6 +56,16 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
return _albumService.removeAssetFromAlbum(album, assets);
|
return _albumService.removeAssetFromAlbum(album, assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> removeUserFromAlbum(Album album, User user) async {
|
||||||
|
final result = await _albumService.removeUserFromAlbum(album, user);
|
||||||
|
|
||||||
|
if (result && album.sharedUsers.isEmpty) {
|
||||||
|
state = state.where((element) => element.id != album.id).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_streamSub.cancel();
|
_streamSub.cancel();
|
||||||
|
@ -348,6 +348,26 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> removeUserFromAlbum(
|
||||||
|
Album album,
|
||||||
|
User user,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await _apiService.albumApi.removeUserFromAlbum(
|
||||||
|
album.remoteId!,
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
album.sharedUsers.remove(user);
|
||||||
|
await _db.writeTxn(() => album.sharedUsers.update(unlink: [user]));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error removeUserFromAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> changeTitleAlbum(
|
Future<bool> changeTitleAlbum(
|
||||||
Album album,
|
Album album,
|
||||||
String newAlbumTitle,
|
String newAlbumTitle,
|
||||||
|
@ -69,6 +69,11 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
hintText: 'share_add_title'.tr(),
|
hintText: 'share_add_title'.tr(),
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
focusColor: Colors.grey[300],
|
focusColor: Colors.grey[300],
|
||||||
fillColor: isDarkTheme
|
fillColor: isDarkTheme
|
||||||
? const Color.fromARGB(255, 32, 33, 35)
|
? const Color.fromARGB(255, 32, 33, 35)
|
||||||
|
@ -39,7 +39,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||||
|
|
||||||
void onDeleteAlbumPressed() async {
|
deleteAlbum() async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
final bool success;
|
final bool success;
|
||||||
@ -65,6 +65,52 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
ImmichLoadingOverlayController.appLoader.hide();
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> showConfirmationDialog() async {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false, // user must tap button!
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Delete album'),
|
||||||
|
content: const Text(
|
||||||
|
'Are you sure you want to delete this album from your account?',
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, 'Cancel'),
|
||||||
|
child: Text(
|
||||||
|
'Cancel',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, 'Confirm');
|
||||||
|
deleteAlbum();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Confirm',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).brightness == Brightness.light
|
||||||
|
? Colors.red
|
||||||
|
: Colors.red[300],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDeleteAlbumPressed() async {
|
||||||
|
showConfirmationDialog();
|
||||||
|
}
|
||||||
|
|
||||||
void onLeaveAlbumPressed() async {
|
void onLeaveAlbumPressed() async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
@ -152,43 +198,61 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
}
|
}
|
||||||
|
|
||||||
void buildBottomSheet() {
|
void buildBottomSheet() {
|
||||||
|
final ownerActions = [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_add_alt_rounded),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
onAddUsers!(album);
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"album_viewer_page_share_add_users",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.settings_rounded),
|
||||||
|
onTap: () =>
|
||||||
|
AutoRouter.of(context).navigate(AlbumOptionsRoute(album: album)),
|
||||||
|
title: const Text(
|
||||||
|
"translated_text_options",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final commonActions = [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.add_photo_alternate_outlined),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
onAddPhotos!(album);
|
||||||
|
},
|
||||||
|
title: const Text(
|
||||||
|
"share_add_photos",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
];
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
isScrollControlled: false,
|
isScrollControlled: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Column(
|
child: Padding(
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
children: [
|
child: Column(
|
||||||
buildBottomSheetActionButton(),
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (selected.isEmpty && onAddPhotos != null)
|
children: [
|
||||||
ListTile(
|
buildBottomSheetActionButton(),
|
||||||
leading: const Icon(Icons.add_photo_alternate_outlined),
|
if (selected.isEmpty && onAddPhotos != null) ...commonActions,
|
||||||
onTap: () {
|
if (selected.isEmpty &&
|
||||||
Navigator.pop(context);
|
onAddPhotos != null &&
|
||||||
onAddPhotos!(album);
|
userId == album.ownerId)
|
||||||
},
|
...ownerActions
|
||||||
title: const Text(
|
],
|
||||||
"share_add_photos",
|
),
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
if (selected.isEmpty &&
|
|
||||||
onAddPhotos != null &&
|
|
||||||
userId == album.ownerId)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.person_add_alt_rounded),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
onAddUsers!(album);
|
|
||||||
},
|
|
||||||
title: const Text(
|
|
||||||
"album_viewer_page_share_add_users",
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -217,6 +281,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
titleFocusNode.unfocus();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.check_rounded),
|
icon: const Icon(Icons.check_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
|
@ -84,6 +84,11 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
: Colors.grey[200],
|
: Colors.grey[200],
|
||||||
filled: titleFocusNode.hasFocus,
|
filled: titleFocusNode.hasFocus,
|
||||||
hintText: 'share_add_title'.tr(),
|
hintText: 'share_add_title'.tr(),
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
205
mobile/lib/modules/album/views/album_options_part.dart
Normal file
205
mobile/lib/modules/album/views/album_options_part.dart
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
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/modules/album/providers/shared_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
|
||||||
|
class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
|
final Album album;
|
||||||
|
|
||||||
|
const AlbumOptionsPage({super.key, required this.album});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final sharedUsers = useState(album.sharedUsers.toList());
|
||||||
|
final owner = album.owner.value;
|
||||||
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
|
final isOwner = owner?.id == userId;
|
||||||
|
|
||||||
|
void showErrorMessage() {
|
||||||
|
Navigator.pop(context);
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Error leaving/removing from album",
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void leaveAlbum() async {
|
||||||
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final isSuccess =
|
||||||
|
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
AutoRouter.of(context)
|
||||||
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
|
} else {
|
||||||
|
showErrorMessage();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
showErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeUserFromAlbum(User user) async {
|
||||||
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.read(sharedAlbumProvider.notifier)
|
||||||
|
.removeUserFromAlbum(album, user);
|
||||||
|
album.sharedUsers.remove(user);
|
||||||
|
sharedUsers.value = album.sharedUsers.toList();
|
||||||
|
} catch (error) {
|
||||||
|
showErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleUserClick(User user) {
|
||||||
|
var actions = [];
|
||||||
|
|
||||||
|
if (user.id == userId) {
|
||||||
|
actions = [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.exit_to_app_rounded),
|
||||||
|
title: const Text("Leave album"),
|
||||||
|
onTap: leaveAlbum,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOwner) {
|
||||||
|
actions = [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_remove_rounded),
|
||||||
|
title: const Text("Remove user from album"),
|
||||||
|
onTap: () => removeUserFromAlbum(user),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
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,
|
||||||
|
useRandomBackgroundColor: true,
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
title: Text(
|
||||||
|
album.owner.value?.firstName ?? "",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
album.owner.value?.email ?? "",
|
||||||
|
style: TextStyle(color: Colors.grey[500]),
|
||||||
|
),
|
||||||
|
trailing: const Text(
|
||||||
|
"Owner",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSharedUsersList() {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: sharedUsers.value.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = sharedUsers.value[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: UserCircleAvatar(
|
||||||
|
user: user,
|
||||||
|
useRandomBackgroundColor: true,
|
||||||
|
radius: 22,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
user.firstName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
user.email,
|
||||||
|
style: TextStyle(color: Colors.grey[500]),
|
||||||
|
),
|
||||||
|
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: Theme.of(context).textTheme.bodySmall),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).pop(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text("translated_text_options".tr()),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildSectionTitle("PEOPLE"),
|
||||||
|
buildOwnerInfo(),
|
||||||
|
buildSharedUsersList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
|
||||||
class AlbumViewerPage extends HookConsumerWidget {
|
class AlbumViewerPage extends HookConsumerWidget {
|
||||||
@ -116,7 +117,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget buildControlButton(Album album) {
|
Widget buildControlButton(Album album) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
|
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@ -141,7 +142,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget buildTitle(Album album) {
|
Widget buildTitle(Album album) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
padding: const EdgeInsets.only(left: 8, right: 8, top: 24),
|
||||||
child: userId == album.ownerId && album.isRemote
|
child: userId == album.ownerId && album.isRemote
|
||||||
? AlbumViewerEditableTitle(
|
? AlbumViewerEditableTitle(
|
||||||
album: album,
|
album: album,
|
||||||
@ -172,7 +173,6 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: 16.0,
|
left: 16.0,
|
||||||
top: 8.0,
|
|
||||||
bottom: album.shared ? 0.0 : 8.0,
|
bottom: album.shared ? 0.0 : 8.0,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -180,7 +180,34 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey,
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildSharedUserIconsRow(Album album) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
await AutoRouter.of(context).push(AlbumOptionsRoute(album: album));
|
||||||
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
|
},
|
||||||
|
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: album.sharedUsers.toList()[index],
|
||||||
|
radius: 18,
|
||||||
|
size: 36,
|
||||||
|
useRandomBackgroundColor: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
itemCount: album.sharedUsers.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -193,33 +220,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
buildTitle(album),
|
buildTitle(album),
|
||||||
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
||||||
if (album.shared)
|
if (album.shared) buildSharedUserIconsRow(album),
|
||||||
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: CircleAvatar(
|
|
||||||
backgroundColor: Colors.grey[300],
|
|
||||||
radius: 18,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(2.0),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
|
||||||
child: Image.asset(
|
|
||||||
'assets/immich-logo-no-outline.png',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
itemCount: album.sharedUsers.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -73,9 +73,12 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
.popForced<AssetSelectionPageResult>(payload);
|
.popForced<AssetSelectionPageResult>(payload);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
"share_add",
|
"share_add",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -30,7 +30,8 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
final albumTitleTextFieldFocusNode = useFocusNode();
|
final albumTitleTextFieldFocusNode = useFocusNode();
|
||||||
final isAlbumTitleTextFieldFocus = useState(false);
|
final isAlbumTitleTextFieldFocus = useState(false);
|
||||||
final isAlbumTitleEmpty = useState(true);
|
final isAlbumTitleEmpty = useState(true);
|
||||||
final selectedAssets = useState<Set<Asset>>(initialAssets != null ? Set.from(initialAssets!) : const {});
|
final selectedAssets = useState<Set<Asset>>(
|
||||||
|
initialAssets != null ? Set.from(initialAssets!) : const {},);
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
showSelectUserPage() async {
|
showSelectUserPage() async {
|
||||||
@ -248,8 +249,9 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
: null,
|
: null,
|
||||||
child: Text(
|
child: Text(
|
||||||
'create_shared_album_page_create'.tr(),
|
'create_shared_album_page_create'.tr(),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/modules/album/providers/suggested_shared_users.pro
|
|||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
|
|
||||||
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
final Album album;
|
final Album album;
|
||||||
@ -35,10 +36,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return CircleAvatar(
|
return UserCircleAvatar(
|
||||||
backgroundImage:
|
user: user,
|
||||||
const AssetImage('assets/immich-logo-no-outline.png'),
|
|
||||||
backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
|
|
||||||
class SelectUserForSharingPage extends HookConsumerWidget {
|
class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
const SelectUserForSharingPage({Key? key, required this.assets})
|
const SelectUserForSharingPage({Key? key, required this.assets})
|
||||||
@ -56,10 +57,8 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return CircleAvatar(
|
return UserCircleAvatar(
|
||||||
backgroundImage:
|
user: user,
|
||||||
const AssetImage('assets/immich-logo-no-outline.png'),
|
|
||||||
backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/user_circle_avatar.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ class HomePageAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
backupState.backgroundBackup || backupState.autoBackup;
|
backupState.backgroundBackup || backupState.autoBackup;
|
||||||
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
|
||||||
AuthenticationState authState = ref.watch(authenticationProvider);
|
AuthenticationState authState = ref.watch(authenticationProvider);
|
||||||
|
final user = Store.get(StoreKey.currentUser);
|
||||||
buildProfilePhoto() {
|
buildProfilePhoto() {
|
||||||
if (authState.profileImagePath.isEmpty) {
|
if (authState.profileImagePath.isEmpty) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
@ -47,9 +48,10 @@ class HomePageAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
Scaffold.of(context).openDrawer();
|
Scaffold.of(context).openDrawer();
|
||||||
},
|
},
|
||||||
child: const UserCircleAvatar(
|
child: UserCircleAvatar(
|
||||||
radius: 18,
|
radius: 18,
|
||||||
size: 33,
|
size: 33,
|
||||||
|
user: user,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/user_circle_avatar.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
@ -19,11 +20,13 @@ class ProfileDrawerHeader extends HookConsumerWidget {
|
|||||||
final uploadProfileImageStatus =
|
final uploadProfileImageStatus =
|
||||||
ref.watch(uploadProfileImageProvider).status;
|
ref.watch(uploadProfileImageProvider).status;
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final user = Store.get(StoreKey.currentUser);
|
||||||
|
|
||||||
buildUserProfileImage() {
|
buildUserProfileImage() {
|
||||||
var userImage = const UserCircleAvatar(
|
var userImage = UserCircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
size: 66,
|
size: 66,
|
||||||
|
user: user,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (authState.profileImagePath.isEmpty) {
|
if (authState.profileImagePath.isEmpty) {
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/transparent_image.dart';
|
|
||||||
|
|
||||||
class UserCircleAvatar extends ConsumerWidget {
|
|
||||||
final double radius;
|
|
||||||
final double size;
|
|
||||||
const UserCircleAvatar({super.key, required this.radius, required this.size});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
AuthenticationState authState = ref.watch(authenticationProvider);
|
|
||||||
|
|
||||||
var profileImageUrl =
|
|
||||||
'${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${authState.userId}?d=${Random().nextInt(1024)}';
|
|
||||||
return CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
radius: radius,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(50),
|
|
||||||
child: FadeInImage(
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
placeholder: MemoryImage(kTransparentImage),
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
image: NetworkImage(
|
|
||||||
profileImageUrl,
|
|
||||||
headers: {
|
|
||||||
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
|
|
||||||
},
|
|
||||||
),
|
|
||||||
fadeInDuration: const Duration(milliseconds: 200),
|
|
||||||
imageErrorBuilder: (context, error, stackTrace) =>
|
|
||||||
Image.memory(kTransparentImage),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/views/album_options_part.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
|
import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
||||||
@ -152,6 +153,7 @@ part 'router.gr.dart';
|
|||||||
),
|
),
|
||||||
AutoRoute(page: AllPeoplePage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: AllPeoplePage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
AutoRoute(page: MemoryPage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: MemoryPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
|
AutoRoute(page: AlbumOptionsPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
@ -296,6 +296,16 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
AlbumOptionsRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<AlbumOptionsRouteArgs>();
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: AlbumOptionsPage(
|
||||||
|
key: args.key,
|
||||||
|
album: args.album,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
HomeRoute.name: (routeData) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
@ -595,6 +605,14 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
duplicateGuard,
|
duplicateGuard,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
RouteConfig(
|
||||||
|
AlbumOptionsRoute.name,
|
||||||
|
path: '/album-options-page',
|
||||||
|
guards: [
|
||||||
|
authGuard,
|
||||||
|
duplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1319,6 +1337,40 @@ class MemoryRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AlbumOptionsPage]
|
||||||
|
class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
|
||||||
|
AlbumOptionsRoute({
|
||||||
|
Key? key,
|
||||||
|
required Album album,
|
||||||
|
}) : super(
|
||||||
|
AlbumOptionsRoute.name,
|
||||||
|
path: '/album-options-page',
|
||||||
|
args: AlbumOptionsRouteArgs(
|
||||||
|
key: key,
|
||||||
|
album: album,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'AlbumOptionsRoute';
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlbumOptionsRouteArgs {
|
||||||
|
const AlbumOptionsRouteArgs({
|
||||||
|
this.key,
|
||||||
|
required this.album,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final Album album;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AlbumOptionsRouteArgs{key: $key, album: $album}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
75
mobile/lib/shared/ui/user_circle_avatar.dart
Normal file
75
mobile/lib/shared/ui/user_circle_avatar.dart
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/transparent_image.dart';
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class UserCircleAvatar extends ConsumerWidget {
|
||||||
|
final User user;
|
||||||
|
double radius;
|
||||||
|
double size;
|
||||||
|
bool useRandomBackgroundColor;
|
||||||
|
|
||||||
|
UserCircleAvatar({
|
||||||
|
super.key,
|
||||||
|
this.radius = 22,
|
||||||
|
this.size = 44,
|
||||||
|
this.useRandomBackgroundColor = false,
|
||||||
|
required this.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final randomColors = [
|
||||||
|
Colors.red[200],
|
||||||
|
Colors.blue[200],
|
||||||
|
Colors.green[200],
|
||||||
|
Colors.yellow[200],
|
||||||
|
Colors.purple[200],
|
||||||
|
Colors.orange[200],
|
||||||
|
Colors.pink[200],
|
||||||
|
Colors.teal[200],
|
||||||
|
Colors.indigo[200],
|
||||||
|
Colors.cyan[200],
|
||||||
|
Colors.brown[200],
|
||||||
|
];
|
||||||
|
|
||||||
|
final profileImageUrl =
|
||||||
|
'${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}';
|
||||||
|
return CircleAvatar(
|
||||||
|
backgroundColor: useRandomBackgroundColor
|
||||||
|
? randomColors[Random().nextInt(randomColors.length)]
|
||||||
|
: Theme.of(context).primaryColor,
|
||||||
|
radius: radius,
|
||||||
|
child: user.profileImagePath == ""
|
||||||
|
? Text(
|
||||||
|
user.firstName[0],
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
child: FadeInImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: MemoryImage(kTransparentImage),
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
image: NetworkImage(
|
||||||
|
profileImageUrl,
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
|
||||||
|
},
|
||||||
|
),
|
||||||
|
fadeInDuration: const Duration(milliseconds: 200),
|
||||||
|
imageErrorBuilder: (context, error, stackTrace) =>
|
||||||
|
Image.memory(kTransparentImage),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user