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,98 @@
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/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/widgets/common/drag_sheet.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AddToAlbumBottomSheet extends HookConsumerWidget {
/// The asset to add to an album
final List<Asset> assets;
const AddToAlbumBottomSheet({super.key, required this.assets});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
final albumService = ref.watch(albumServiceProvider);
useEffect(() {
// Fetch album updates, e.g., cover image
ref.read(albumProvider.notifier).refreshRemoteAlbums();
return null;
}, []);
void addToAlbum(Album album) async {
final result = await albumService.addAssets(album, assets);
if (result != null) {
if (result.alreadyInAlbum.isNotEmpty) {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}),
);
} else {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}),
);
}
}
context.pop();
}
return Card(
elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),
),
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
const Align(alignment: Alignment.center, child: CustomDraggingHandle()),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('add_to_album'.tr(), style: context.textTheme.displayMedium),
TextButton.icon(
icon: Icon(Icons.add, color: context.primaryColor),
label: Text('common_create_new_album'.tr(), style: TextStyle(color: context.primaryColor)),
onPressed: () {
context.pushRoute(CreateAlbumRoute(assets: assets));
},
),
],
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: AddToAlbumSliverList(
albums: albums,
sharedAlbums: albums.where((a) => a.shared).toList(),
onAddToAlbum: addToAlbum,
),
),
],
),
);
}
}

View file

@ -0,0 +1,65 @@
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/album_sort_by_options.provider.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_listtile.dart';
import 'package:immich_mobile/entities/album.entity.dart';
class AddToAlbumSliverList extends HookConsumerWidget {
/// The asset to add to an album
final List<Album> albums;
final List<Album> sharedAlbums;
final void Function(Album) onAddToAlbum;
final bool enabled;
const AddToAlbumSliverList({
super.key,
required this.onAddToAlbum,
required this.albums,
required this.sharedAlbums,
this.enabled = true,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumSortMode = ref.watch(albumSortByOptionsProvider);
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
final sortedAlbums = albumSortMode.sortFn(albums, albumSortIsReverse);
final sortedSharedAlbums = albumSortMode.sortFn(sharedAlbums, albumSortIsReverse);
return SliverList(
delegate: SliverChildBuilderDelegate(childCount: albums.length + (sharedAlbums.isEmpty ? 0 : 1), (
context,
index,
) {
// Build shared expander
if (index == 0 && sortedSharedAlbums.isNotEmpty) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ExpansionTile(
title: Text('shared'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
leading: const Icon(Icons.group),
children: [
ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: sortedSharedAlbums.length,
itemBuilder: (context, index) => AlbumThumbnailListTile(
album: sortedSharedAlbums[index],
onTap: enabled ? () => onAddToAlbum(sortedSharedAlbums[index]) : () {},
),
),
],
),
);
}
// Build albums list
final offset = index - (sharedAlbums.isNotEmpty ? 1 : 0);
final album = sortedAlbums[offset];
return AlbumThumbnailListTile(album: album, onTap: enabled ? () => onAddToAlbum(album) : () {});
}),
);
}
}

View file

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class AlbumActionFilledButton extends StatelessWidget {
final VoidCallback? onPressed;
final String labelText;
final IconData iconData;
const AlbumActionFilledButton({super.key, this.onPressed, required this.labelText, required this.iconData});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))),
side: BorderSide(color: context.colorScheme.surfaceContainerHighest, width: 1),
backgroundColor: context.colorScheme.surfaceContainerHigh,
),
icon: Icon(iconData, size: 18, color: context.primaryColor),
label: Text(labelText, style: context.textTheme.labelLarge?.copyWith()),
onPressed: onPressed,
),
);
}
}

View file

@ -0,0 +1,111 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
class AlbumThumbnailCard extends ConsumerWidget {
final Function()? onTap;
/// Whether or not to show the owner of the album (or "Owned")
/// in the subtitle of the album
final bool showOwner;
final bool showTitle;
const AlbumThumbnailCard({super.key, required this.album, this.onTap, this.showOwner = false, this.showTitle = true});
final Album album;
@override
Widget build(BuildContext context, WidgetRef ref) {
return LayoutBuilder(
builder: (context, constraints) {
var cardSize = constraints.maxWidth;
buildEmptyThumbnail() {
return Container(
height: cardSize,
width: cardSize,
decoration: BoxDecoration(color: context.colorScheme.surfaceContainerHigh),
child: Center(
child: Icon(Icons.no_photography, size: cardSize * .15, color: context.colorScheme.primary),
),
);
}
buildAlbumThumbnail() => ImmichThumbnail(asset: album.thumbnail.value, width: cardSize, height: cardSize);
buildAlbumTextRow() {
// Add the owner name to the subtitle
String? owner;
if (showOwner) {
if (album.ownerId == ref.read(currentUserProvider)?.id) {
owner = 'owned'.tr();
} else if (album.ownerName != null) {
owner = 'shared_by_user'.t(context: context, args: {'user': album.ownerName!});
}
}
return Text.rich(
TextSpan(
children: [
TextSpan(
text: 'items_count'.t(context: context, args: {'count': album.assetCount}),
),
if (owner != null) const TextSpan(text: ''),
if (owner != null) TextSpan(text: owner),
],
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
overflow: TextOverflow.fade,
);
}
return GestureDetector(
onTap: onTap,
child: Flex(
direction: Axis.vertical,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: cardSize,
height: cardSize,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20)),
child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(),
),
),
if (showTitle) ...[
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SizedBox(
width: cardSize,
child: Text(
album.name,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
),
buildAlbumTextRow(),
],
],
),
),
],
),
);
},
);
}
}

View file

@ -0,0 +1,96 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class AlbumThumbnailListTile extends StatelessWidget {
const AlbumThumbnailListTile({super.key, required this.album, this.onTap});
final Album album;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
var cardSize = 68.0;
buildEmptyThumbnail() {
return Container(
decoration: BoxDecoration(color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200]),
child: SizedBox(
height: cardSize,
width: cardSize,
child: const Center(child: Icon(Icons.no_photography)),
),
);
}
buildAlbumThumbnail() {
return CachedNetworkImage(
width: cardSize,
height: cardSize,
fit: BoxFit.cover,
fadeInDuration: const Duration(milliseconds: 200),
imageUrl: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail),
httpHeaders: ApiService.getRequestHeaders(),
cacheKey: getAlbumThumbNailCacheKey(album, type: AssetMediaSize.thumbnail),
errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined),
);
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap:
onTap ??
() {
context.pushRoute(AlbumViewerRoute(albumId: album.id));
},
child: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: album.thumbnail.value == null ? buildEmptyThumbnail() : buildAlbumThumbnail(),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
album.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'items_count'.t(context: context, args: {'count': album.assetCount}),
style: const TextStyle(fontSize: 12),
),
if (album.shared) ...[
const Text('', style: TextStyle(fontSize: 12)),
Text('shared'.tr(), style: const TextStyle(fontSize: 12)),
],
],
),
],
),
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,74 @@
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/album_title.provider.dart';
class AlbumTitleTextField extends ConsumerWidget {
const AlbumTitleTextField({
super.key,
required this.isAlbumTitleEmpty,
required this.albumTitleTextFieldFocusNode,
required this.albumTitleController,
required this.isAlbumTitleTextFieldFocus,
});
final ValueNotifier<bool> isAlbumTitleEmpty;
final FocusNode albumTitleTextFieldFocusNode;
final TextEditingController albumTitleController;
final ValueNotifier<bool> isAlbumTitleTextFieldFocus;
@override
Widget build(BuildContext context, WidgetRef ref) {
return TextField(
onChanged: (v) {
if (v.isEmpty) {
isAlbumTitleEmpty.value = true;
} else {
isAlbumTitleEmpty.value = false;
}
ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
},
focusNode: albumTitleTextFieldFocusNode,
style: TextStyle(fontSize: 28, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold),
controller: albumTitleController,
onTap: () {
isAlbumTitleTextFieldFocus.value = true;
if (albumTitleController.text == 'Untitled') {
albumTitleController.clear();
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
suffixIcon: !isAlbumTitleEmpty.value && isAlbumTitleTextFieldFocus.value
? IconButton(
onPressed: () {
albumTitleController.clear();
isAlbumTitleEmpty.value = true;
},
icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
splashRadius: 10,
)
: null,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
hintText: 'add_a_title'.tr(),
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
fontSize: 28,
fontWeight: FontWeight.bold,
),
focusColor: Colors.grey[300],
fillColor: context.colorScheme.surfaceContainerHigh,
filled: isAlbumTitleTextFieldFocus.value,
),
);
}
}

View file

@ -0,0 +1,307 @@
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/providers/activity_statistics.provider.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidget {
const AlbumViewerAppbar({
super.key,
required this.userId,
required this.titleFocusNode,
required this.descriptionFocusNode,
this.onAddPhotos,
this.onAddUsers,
required this.onActivities,
});
final String userId;
final FocusNode titleFocusNode;
final FocusNode descriptionFocusNode;
final void Function()? onAddPhotos;
final void Function()? onAddUsers;
final void Function() onActivities;
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumState = useState(ref.read(currentAlbumProvider));
final album = albumState.value;
ref.listen(currentAlbumProvider, (_, newAlbum) {
final oldAlbum = albumState.value;
if (oldAlbum != null && newAlbum != null && oldAlbum.id == newAlbum.id) {
return;
}
albumState.value = newAlbum;
});
if (album == null) {
return const SizedBox();
}
final albumViewer = ref.watch(albumViewerProvider);
final newAlbumTitle = albumViewer.editTitleText;
final newAlbumDescription = albumViewer.editDescriptionText;
final isEditAlbum = albumViewer.isEditAlbum;
final comments = album.shared ? ref.watch(activityStatisticsProvider(album.remoteId!)) : 0;
deleteAlbum() async {
final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
if (!success) {
ImmichToast.show(
context: context,
msg: "album_viewer_appbar_share_err_delete".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
}
Future<void> onDeleteAlbumPressed() {
return showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: const Text('delete_album').tr(),
content: const Text('album_viewer_appbar_delete_confirm').tr(),
actions: <Widget>[
TextButton(
onPressed: () => context.pop('Cancel'),
child: Text(
'cancel',
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
onPressed: () {
context.pop('Confirm');
deleteAlbum();
},
child: Text(
'confirm',
style: TextStyle(fontWeight: FontWeight.bold, color: context.colorScheme.error),
).tr(),
),
],
);
},
);
}
void onLeaveAlbumPressed() async {
bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album);
if (isSuccess) {
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
} else {
context.pop();
ImmichToast.show(
context: context,
msg: "album_viewer_appbar_share_err_leave".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
}
buildBottomSheetActions() {
return [
album.ownerId == userId
? ListTile(
leading: const Icon(Icons.delete_forever_rounded),
title: const Text('delete_album', style: TextStyle(fontWeight: FontWeight.w500)).tr(),
onTap: onDeleteAlbumPressed,
)
: ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: const Text(
'album_viewer_appbar_share_leave',
style: TextStyle(fontWeight: FontWeight.w500),
).tr(),
onTap: onLeaveAlbumPressed,
),
];
// }
}
void onSortOrderToggled() async {
final updatedAlbum = await ref.read(albumProvider.notifier).toggleSortOrder(album);
if (updatedAlbum == null) {
ImmichToast.show(
context: context,
msg: "error_change_sort_album".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
context.pop();
}
void buildBottomSheet() {
final ownerActions = [
ListTile(
leading: const Icon(Icons.person_add_alt_rounded),
onTap: () {
context.pop();
final onAddUsers = this.onAddUsers;
if (onAddUsers != null) {
onAddUsers();
}
},
title: const Text("album_viewer_page_share_add_users", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
ListTile(
leading: const Icon(Icons.swap_vert_rounded),
onTap: onSortOrderToggled,
title: const Text("change_display_order", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
ListTile(
leading: const Icon(Icons.link_rounded),
onTap: () {
context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId));
context.pop();
},
title: const Text("control_bottom_app_bar_share_link", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
ListTile(
leading: const Icon(Icons.settings_rounded),
onTap: () => context.navigateTo(const AlbumOptionsRoute()),
title: const Text("options", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
];
final commonActions = [
ListTile(
leading: const Icon(Icons.add_photo_alternate_outlined),
onTap: () {
context.pop();
final onAddPhotos = this.onAddPhotos;
if (onAddPhotos != null) {
onAddPhotos();
}
},
title: const Text("add_photos", style: TextStyle(fontWeight: FontWeight.w500)).tr(),
),
];
showModalBottomSheet(
backgroundColor: context.scaffoldBackgroundColor,
isScrollControlled: false,
context: context,
builder: (context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 24.0),
child: ListView(
shrinkWrap: true,
children: [
...buildBottomSheetActions(),
if (onAddPhotos != null) ...commonActions,
if (onAddPhotos != null && userId == album.ownerId) ...ownerActions,
],
),
),
);
},
);
}
Widget buildActivitiesButton() {
return IconButton(
onPressed: onActivities,
icon: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.mode_comment_outlined),
if (comments != 0)
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
comments.toString(),
style: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor),
),
),
],
),
);
}
buildLeadingButton() {
if (isEditAlbum) {
return IconButton(
onPressed: () async {
if (newAlbumTitle.isNotEmpty) {
bool isSuccess = await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(album, newAlbumTitle);
if (!isSuccess) {
ImmichToast.show(
context: context,
msg: "album_viewer_appbar_share_err_title".tr(),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
titleFocusNode.unfocus();
} else if (newAlbumDescription.isNotEmpty) {
bool isSuccessDescription = await ref
.watch(albumViewerProvider.notifier)
.changeAlbumDescription(album, newAlbumDescription);
if (!isSuccessDescription) {
ImmichToast.show(
context: context,
msg: "album_viewer_appbar_share_err_description".tr(),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
descriptionFocusNode.unfocus();
} else {
titleFocusNode.unfocus();
descriptionFocusNode.unfocus();
ref.read(albumViewerProvider.notifier).disableEditAlbum();
}
},
icon: const Icon(Icons.check_rounded),
splashRadius: 25,
);
} else {
return IconButton(
onPressed: context.maybePop,
icon: const Icon(Icons.arrow_back_ios_rounded),
splashRadius: 25,
);
}
}
return AppBar(
elevation: 0,
backgroundColor: context.scaffoldBackgroundColor,
leading: buildLeadingButton(),
centerTitle: false,
actions: [
if (album.shared && (album.activityEnabled || comments != 0)) buildActivitiesButton(),
if (album.isRemote) ...[
IconButton(splashRadius: 25, onPressed: buildBottomSheet, icon: const Icon(Icons.more_horiz_rounded)),
],
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View file

@ -0,0 +1,82 @@
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/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
class AlbumViewerEditableDescription extends HookConsumerWidget {
final String albumDescription;
final FocusNode descriptionFocusNode;
const AlbumViewerEditableDescription({super.key, required this.albumDescription, required this.descriptionFocusNode});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumViewerState = ref.watch(albumViewerProvider);
final descriptionTextEditController = useTextEditingController(
text: albumViewerState.isEditAlbum && albumViewerState.editDescriptionText.isNotEmpty
? albumViewerState.editDescriptionText
: albumDescription,
);
void onFocusModeChange() {
if (!descriptionFocusNode.hasFocus && descriptionTextEditController.text.isEmpty) {
ref.watch(albumViewerProvider.notifier).setEditDescriptionText("");
descriptionTextEditController.text = "";
}
}
useEffect(() {
descriptionFocusNode.addListener(onFocusModeChange);
return () {
descriptionFocusNode.removeListener(onFocusModeChange);
};
}, []);
return Material(
color: Colors.transparent,
child: TextField(
onChanged: (value) {
if (value.isEmpty) {
} else {
ref.watch(albumViewerProvider.notifier).setEditDescriptionText(value);
}
},
focusNode: descriptionFocusNode,
style: context.textTheme.bodyLarge,
maxLines: 3,
minLines: 1,
controller: descriptionTextEditController,
onTap: () {
context.focusScope.requestFocus(descriptionFocusNode);
ref.watch(albumViewerProvider.notifier).setEditDescriptionText(albumDescription);
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
if (descriptionTextEditController.text == '') {
descriptionTextEditController.clear();
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8),
suffixIcon: descriptionFocusNode.hasFocus
? IconButton(
onPressed: () {
descriptionTextEditController.clear();
},
icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
splashRadius: 10,
)
: null,
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)),
focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)),
focusColor: Colors.grey[300],
fillColor: context.scaffoldBackgroundColor,
filled: descriptionFocusNode.hasFocus,
hintText: 'add_a_description'.tr(),
),
),
);
}
}

View file

@ -0,0 +1,81 @@
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/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
class AlbumViewerEditableTitle extends HookConsumerWidget {
final String albumName;
final FocusNode titleFocusNode;
const AlbumViewerEditableTitle({super.key, required this.albumName, required this.titleFocusNode});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumViewerState = ref.watch(albumViewerProvider);
final titleTextEditController = useTextEditingController(
text: albumViewerState.isEditAlbum && albumViewerState.editTitleText.isNotEmpty
? albumViewerState.editTitleText
: albumName,
);
void onFocusModeChange() {
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
ref.watch(albumViewerProvider.notifier).setEditTitleText("Untitled");
titleTextEditController.text = "Untitled";
}
}
useEffect(() {
titleFocusNode.addListener(onFocusModeChange);
return () {
titleFocusNode.removeListener(onFocusModeChange);
};
}, []);
return Material(
color: Colors.transparent,
child: TextField(
onChanged: (value) {
if (value.isEmpty) {
} else {
ref.watch(albumViewerProvider.notifier).setEditTitleText(value);
}
},
focusNode: titleFocusNode,
style: context.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700),
controller: titleTextEditController,
onTap: () {
context.focusScope.requestFocus(titleFocusNode);
ref.watch(albumViewerProvider.notifier).setEditTitleText(albumName);
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
if (titleTextEditController.text == 'Untitled') {
titleTextEditController.clear();
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
suffixIcon: titleFocusNode.hasFocus
? IconButton(
onPressed: () {
titleTextEditController.clear();
},
icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
splashRadius: 10,
)
: null,
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)),
focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)),
focusColor: Colors.grey[300],
fillColor: context.scaffoldBackgroundColor,
filled: titleFocusNode.hasFocus,
hintText: 'add_a_title'.tr(),
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(fontSize: 28),
),
),
);
}
}

View file

@ -0,0 +1,47 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
class RemoteAlbumSharedUserIcons extends ConsumerWidget {
const RemoteAlbumSharedUserIcons({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
if (currentAlbum == null) {
return const SizedBox();
}
final sharedUsersAsync = ref.watch(remoteAlbumSharedUsersProvider(currentAlbum.id));
return sharedUsersAsync.maybeWhen(
data: (sharedUsers) {
if (sharedUsers.isEmpty) {
return const SizedBox();
}
return GestureDetector(
onTap: () => context.pushRoute(DriftAlbumOptionsRoute(album: currentAlbum)),
child: SizedBox(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: UserCircleAvatar(user: sharedUsers[index], radius: 18, size: 36, hasBorder: true),
);
}),
itemCount: sharedUsers.length,
),
),
);
},
orElse: () => const SizedBox(),
);
}
}

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
class SharedAlbumThumbnailImage extends HookConsumerWidget {
final Asset asset;
const SharedAlbumThumbnailImage({super.key, required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
return GestureDetector(
onTap: () {
// debugPrint("View ${asset.id}");
},
child: Stack(children: [ImmichThumbnail(asset: asset, width: 500, height: 500)]),
);
}
}