diff --git a/i18n/en.json b/i18n/en.json index 57a380579e..50e3cb67e1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1195,6 +1195,7 @@ "library_page_sort_title": "Album title", "licenses": "Licenses", "light": "Light", + "like": "Like", "like_deleted": "Like deleted", "link_motion_video": "Link motion video", "link_to_oauth": "Link to OAuth", diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart index a199bce129..4558e1d5be 100644 --- a/mobile/lib/domain/models/album/album.model.dart +++ b/mobile/lib/domain/models/album/album.model.dart @@ -23,6 +23,7 @@ class RemoteAlbum { final AlbumAssetOrder order; final int assetCount; final String ownerName; + final bool isShared; const RemoteAlbum({ required this.id, @@ -36,6 +37,7 @@ class RemoteAlbum { required this.order, required this.assetCount, required this.ownerName, + required this.isShared, }); @override @@ -52,6 +54,7 @@ class RemoteAlbum { thumbnailAssetId: ${thumbnailAssetId ?? ""} assetCount: $assetCount ownerName: $ownerName + isShared: $isShared }'''; } @@ -69,7 +72,8 @@ class RemoteAlbum { isActivityEnabled == other.isActivityEnabled && order == other.order && assetCount == other.assetCount && - ownerName == other.ownerName; + ownerName == other.ownerName && + isShared == other.isShared; } @override @@ -84,7 +88,8 @@ class RemoteAlbum { isActivityEnabled.hashCode ^ order.hashCode ^ assetCount.hashCode ^ - ownerName.hashCode; + ownerName.hashCode ^ + isShared.hashCode; } RemoteAlbum copyWith({ @@ -99,6 +104,7 @@ class RemoteAlbum { AlbumAssetOrder? order, int? assetCount, String? ownerName, + bool? isShared, }) { return RemoteAlbum( id: id ?? this.id, @@ -112,6 +118,7 @@ class RemoteAlbum { order: order ?? this.order, assetCount: assetCount ?? this.assetCount, ownerName: ownerName ?? this.ownerName, + isShared: isShared ?? this.isShared, ); } } diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index 4e5d53a49b..41ce131871 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -31,11 +31,17 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { useColumns: false, ), leftOuterJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), useColumns: false), + leftOuterJoin( + _db.remoteAlbumUserEntity, + _db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), ]); query ..where(_db.remoteAssetEntity.deletedAt.isNull()) ..addColumns([assetCount]) ..addColumns([_db.userEntity.name]) + ..addColumns([_db.remoteAlbumUserEntity.userId.count()]) ..groupBy([_db.remoteAlbumEntity.id]); if (sortBy.isNotEmpty) { @@ -53,7 +59,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { .map( (row) => row .readTable(_db.remoteAlbumEntity) - .toDto(assetCount: row.read(assetCount) ?? 0, ownerName: row.read(_db.userEntity.name)!), + .toDto( + assetCount: row.read(assetCount) ?? 0, + ownerName: row.read(_db.userEntity.name)!, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2, + ), ) .get(); } @@ -78,17 +88,27 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), useColumns: false, ), + leftOuterJoin( + _db.remoteAlbumUserEntity, + _db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), ]) ..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull()) ..addColumns([assetCount]) ..addColumns([_db.userEntity.name]) + ..addColumns([_db.remoteAlbumUserEntity.userId.count()]) ..groupBy([_db.remoteAlbumEntity.id]); return query .map( (row) => row .readTable(_db.remoteAlbumEntity) - .toDto(assetCount: row.read(assetCount) ?? 0, ownerName: row.read(_db.userEntity.name)!), + .toDto( + assetCount: row.read(assetCount) ?? 0, + ownerName: row.read(_db.userEntity.name)!, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2, + ), ) .getSingleOrNull(); } @@ -254,13 +274,24 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), useColumns: false, ), + leftOuterJoin( + _db.remoteAlbumUserEntity, + _db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), + useColumns: false, + ), ]) ..where(_db.remoteAlbumEntity.id.equals(albumId)) ..addColumns([_db.userEntity.name]) + ..addColumns([_db.remoteAlbumUserEntity.userId.count()]) ..groupBy([_db.remoteAlbumEntity.id]); return query.map((row) { - final album = row.readTable(_db.remoteAlbumEntity).toDto(ownerName: row.read(_db.userEntity.name)!); + final album = row + .readTable(_db.remoteAlbumEntity) + .toDto( + ownerName: row.read(_db.userEntity.name)!, + isShared: row.read(_db.remoteAlbumUserEntity.userId.count())! > 2, + ); return album; }).watchSingleOrNull(); } @@ -293,7 +324,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { } extension on RemoteAlbumEntityData { - RemoteAlbum toDto({int assetCount = 0, required String ownerName}) { + RemoteAlbum toDto({int assetCount = 0, required String ownerName, required bool isShared}) { return RemoteAlbum( id: id, name: name, @@ -306,6 +337,7 @@ extension on RemoteAlbumEntityData { order: order, assetCount: assetCount, ownerName: ownerName, + isShared: isShared, ); } } diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart new file mode 100644 index 0000000000..8e67d85884 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -0,0 +1,104 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' hide Store; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/album/drift_activity_text_field.dart'; +import 'package:immich_mobile/providers/activity.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/activities/activity_tile.dart'; +import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; + +@RoutePage() +class DriftActivitiesPage extends HookConsumerWidget { + const DriftActivitiesPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final album = ref.watch(currentRemoteAlbumProvider)!; + final asset = ref.watch(currentAssetNotifier) as RemoteAsset?; + final user = ref.watch(currentUserProvider); + + final activityNotifier = ref.read(albumActivityProvider(album.id, asset?.id).notifier); + final activities = ref.watch(albumActivityProvider(album.id, asset?.id)); + final listViewScrollController = useScrollController(); + + void scrollToBottom() { + listViewScrollController.animateTo( + listViewScrollController.position.maxScrollExtent + 80, + duration: const Duration(milliseconds: 600), + curve: Curves.fastOutSlowIn, + ); + } + + Future onAddComment(String comment) async { + await activityNotifier.addComment(comment); + scrollToBottom(); + } + + return Scaffold( + appBar: AppBar( + title: asset == null ? Text(album.name) : null, + actions: [const LikeActivityActionButton(menuItem: true)], + actionsPadding: const EdgeInsets.only(right: 8), + ), + body: activities.widgetWhen( + onData: (data) { + final liked = data.firstWhereOrNull( + (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.id, + ); + + return SafeArea( + child: Stack( + children: [ + ListView.builder( + controller: listViewScrollController, + itemCount: data.length + 1, + itemBuilder: (context, index) { + if (index == data.length) { + return const SizedBox(height: 80); + } + final activity = data[index]; + final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; + return Padding( + padding: const EdgeInsets.all(5), + child: DismissibleActivity( + activity.id, + ActivityTile(activity), + onDismiss: canDelete + ? (activityId) async => await activityNotifier.removeActivity(activity.id) + : null, + ), + ); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + decoration: BoxDecoration( + color: context.scaffoldBackgroundColor, + border: Border(top: BorderSide(color: context.colorScheme.secondaryContainer, width: 1)), + ), + child: DriftActivityTextField( + isEnabled: album.isActivityEnabled, + likeId: liked?.id, + onSubmit: onAddComment, + ), + ), + ), + ], + ), + ); + }, + ), + resizeToAvoidBottomInset: true, + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index eecc4b1e1e..e0fe5ee62b 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -165,6 +165,10 @@ class _RemoteAlbumPageState extends ConsumerState { } } + Future showActivity(BuildContext context) async { + context.pushRoute(const DriftActivitiesRoute()); + } + void showOptionSheet(BuildContext context) { final user = ref.watch(currentUserProvider); final isOwner = user != null ? user.id == _album.ownerId : false; @@ -241,6 +245,7 @@ class _RemoteAlbumPageState extends ConsumerState { onShowOptions: () => showOptionSheet(context), onToggleAlbumOrder: () => toggleAlbumOrder(), onEditTitle: () => showEditTitleAndDescription(context), + onActivity: () => showActivity(context), ), bottomSheet: RemoteAlbumBottomSheet(album: _album), ), diff --git a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart new file mode 100644 index 0000000000..4fec4e24db --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart @@ -0,0 +1,63 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/activity.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; + +class LikeActivityActionButton extends ConsumerWidget { + const LikeActivityActionButton({super.key, this.menuItem = false}); + + final bool menuItem; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final album = ref.watch(currentRemoteAlbumProvider); + final asset = ref.watch(currentAssetNotifier) as RemoteAsset?; + final user = ref.watch(currentUserProvider); + + final activities = ref.watch(albumActivityProvider(album?.id ?? "", asset?.id)); + + onTap(Activity? liked) async { + if (user == null) { + return; + } + + if (liked != null) { + await ref.read(albumActivityProvider(album?.id ?? "", asset?.id).notifier).removeActivity(liked.id); + } else { + await ref.read(albumActivityProvider(album?.id ?? "", asset?.id).notifier).addLike(); + } + + ref.invalidate(albumActivityProvider(album?.id ?? "", asset?.id)); + } + + return activities.when( + data: (data) { + final liked = data.firstWhereOrNull( + (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.id, + ); + + return BaseActionButton( + iconData: liked != null ? Icons.favorite : Icons.favorite_border, + label: "like".t(context: context), + onPressed: () => onTap(liked), + menuItem: menuItem, + ); + }, + + // default to empty heart during loading + loading: () => BaseActionButton( + iconData: Icons.favorite_border, + label: "like".t(context: context), + menuItem: menuItem, + ), + error: (error, stack) => Text("Error: $error"), + ); + } +} diff --git a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart new file mode 100644 index 0000000000..a49ac9551a --- /dev/null +++ b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart @@ -0,0 +1,109 @@ +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/user.provider.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; + +class DriftActivityTextField extends ConsumerStatefulWidget { + final bool isEnabled; + final String? likeId; + final Function(String) onSubmit; + final Function()? onKeyboardFocus; + + const DriftActivityTextField({ + required this.onSubmit, + this.isEnabled = true, + this.likeId, + this.onKeyboardFocus, + super.key, + }); + + @override + ConsumerState createState() => _DriftActivityTextFieldState(); +} + +class _DriftActivityTextFieldState extends ConsumerState { + late FocusNode inputFocusNode; + late TextEditingController inputController; + bool sendEnabled = false; + + @override + void initState() { + super.initState(); + inputController = TextEditingController(); + inputFocusNode = FocusNode(); + + inputFocusNode.requestFocus(); + + inputFocusNode.addListener(() { + if (inputFocusNode.hasFocus) { + widget.onKeyboardFocus?.call(); + } + }); + + inputController.addListener(() { + setState(() { + sendEnabled = inputController.text.trim().isNotEmpty; + }); + }); + } + + @override + void dispose() { + inputController.dispose(); + inputFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final user = ref.watch(currentUserProvider); + + // Pass text to callback and reset controller + void onEditingComplete() { + if (inputController.text.trim().isEmpty) { + return; + } + + widget.onSubmit(inputController.text); + inputController.clear(); + inputFocusNode.unfocus(); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: TextField( + controller: inputController, + enabled: widget.isEnabled, + focusNode: inputFocusNode, + textInputAction: TextInputAction.send, + autofocus: false, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 12), // Adjust as needed + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + prefixIcon: user != null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: UserCircleAvatar(user: user, size: 30, radius: 15), + ) + : null, + suffixIcon: IconButton( + onPressed: sendEnabled ? onEditingComplete : null, + icon: const Icon(Icons.send), + iconSize: 24, + color: context.primaryColor, + disabledColor: context.colorScheme.secondaryContainer, + ), + hintText: !widget.isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), + hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]), + ), + onEditingComplete: onEditingComplete, + onTapOutside: (_) => inputFocusNode.unfocus(), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 6baac82889..f531a29b2d 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -8,9 +8,10 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; @@ -49,6 +50,8 @@ class AssetDetailBottomSheet extends ConsumerWidget { final actions = [ const ShareActionButton(source: ActionSource.viewer), + if (currentAlbum != null && currentAlbum.isActivityEnabled && currentAlbum.isShared) + const LikeActivityActionButton(), if (asset.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.viewer), const ArchiveActionButton(source: ActionSource.viewer), diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index bd6e6bfb3e..dace3d53a3 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_act import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -27,6 +28,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { return const SizedBox.shrink(); } + final album = ref.watch(currentRemoteAlbumProvider); + final user = ref.watch(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final isInLockedView = ref.watch(inLockedViewProvider); @@ -49,6 +52,13 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final actions = [ if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true), + if (album != null && album.isActivityEnabled && album.isShared) + IconButton( + icon: const Icon(Icons.chat_outlined), + onPressed: () { + context.navigateTo(const DriftActivitiesRoute()); + }, + ), if (showViewInTimelineButton) IconButton( onPressed: () async { diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index 11fc1537c5..525f0906ba 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -165,6 +165,7 @@ class AlbumApiRepository extends ApiRepository { order: dto.order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, assetCount: dto.assetCount, ownerName: dto.owner.name, + isShared: dto.albumUsers.length > 2, ); } } diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index 10d8a54e72..557050323a 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -112,6 +112,7 @@ extension on AlbumResponseDto { order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, assetCount: assetCount, ownerName: owner.name, + isShared: albumUsers.length > 2, ); } } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 5f8e4fe53c..22bcd11aa4 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -80,6 +80,7 @@ import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_activities.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart'; import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; @@ -339,6 +340,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftEditImageRoute.page), AutoRoute(page: DriftCropImageRoute.page), AutoRoute(page: DriftFilterImageRoute.page), + AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 1abe49b14f..30b09b47ce 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -667,6 +667,22 @@ class CropImageRouteArgs { } } +/// generated route for +/// [DriftActivitiesPage] +class DriftActivitiesRoute extends PageRouteInfo { + const DriftActivitiesRoute({List? children}) + : super(DriftActivitiesRoute.name, initialChildren: children); + + static const String name = 'DriftActivitiesRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftActivitiesPage(); + }, + ); +} + /// generated route for /// [DriftAlbumOptionsPage] class DriftAlbumOptionsRoute extends PageRouteInfo { diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 7a0a092e7d..1f09309947 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -1,3 +1,4 @@ +import 'package:immich_mobile/constants/errors.dart'; import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/repositories/activity_api.repository.dart'; @@ -30,7 +31,11 @@ class ActivityService with ErrorLoggerMixin { Future removeActivity(String id) async { return logError( () async { - await _activityApiRepository.delete(id); + try { + await _activityApiRepository.delete(id); + } on NoResponseDtoError { + return true; + } return true; }, defaultValue: false, diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index fb7acc8d0f..54497a10de 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -28,12 +28,14 @@ class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget { this.onShowOptions, this.onToggleAlbumOrder, this.onEditTitle, + this.onActivity, }); final IconData icon; final void Function()? onShowOptions; final void Function()? onToggleAlbumOrder; final void Function()? onEditTitle; + final void Function()? onActivity; @override ConsumerState createState() => _MesmerizingSliverAppBarState(); @@ -101,12 +103,33 @@ class _MesmerizingSliverAppBarState extends ConsumerState(); + final scrollProgress = _calculateScrollProgress(settings); + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: scrollProgress > 0.95 + ? Text( + currentAlbum.name, + style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18), + ) + : null, + ); + }, + ), flexibleSpace: Builder( builder: (context) { final settings = context.dependOnInheritedWidgetOfExactType(); @@ -122,16 +145,6 @@ class _MesmerizingSliverAppBarState extends ConsumerState 0.95 - ? Text( - currentAlbum.name, - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18), - ) - : null, - ), background: _ExpandedBackground( scrollProgress: scrollProgress, icon: widget.icon, diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart index 88e8b3824c..ebd94a9450 100644 --- a/mobile/test/domain/services/album.service_test.dart +++ b/mobile/test/domain/services/album.service_test.dart @@ -55,6 +55,7 @@ void main() { updatedAt: DateTime(2023, 1, 2), ownerId: 'owner1', ownerName: "Test User", + isShared: false, ); final albumB = RemoteAlbum( @@ -68,6 +69,7 @@ void main() { updatedAt: DateTime(2023, 2, 2), ownerId: 'owner2', ownerName: "Test User", + isShared: false, ); group('sortAlbums', () {