mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:05:39 -04:00
feat(album): album view sort order (#14648)
* feat(mobile): album view sort order * feat: add error message * refactor(mobile): album page (#14659) * refactor album page * update lint rule * const record * fix: updating sort order when pull to refresh --------- Co-authored-by: Alex <alex.tran1502@gmail.com> * Move sort toggle button to bottom sheet menu * chore: revert multiselectgrid loading status * chore: revert multiselectgrid loading status --------- Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
parent
364b717fde
commit
9503bf479b
@ -69,7 +69,7 @@ custom_lint:
|
|||||||
# acceptable exceptions for the time being (until Isar is fully replaced)
|
# acceptable exceptions for the time being (until Isar is fully replaced)
|
||||||
- integration_test/test_utils/general_helper.dart
|
- integration_test/test_utils/general_helper.dart
|
||||||
- lib/main.dart
|
- lib/main.dart
|
||||||
- lib/pages/common/album_asset_selection.page.dart
|
- lib/pages/album/album_asset_selection.page.dart
|
||||||
- lib/routing/router.dart
|
- lib/routing/router.dart
|
||||||
- lib/services/immich_logger.service.dart # not really a service... more a util
|
- lib/services/immich_logger.service.dart # not really a service... more a util
|
||||||
- lib/utils/{db,migration,renderlist_generator}.dart
|
- lib/utils/{db,migration,renderlist_generator}.dart
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"change_display_order": "Change display order",
|
||||||
|
"error_change_sort_album": "Failed to change album sort order",
|
||||||
"action_common_back": "Back",
|
"action_common_back": "Back",
|
||||||
"action_common_cancel": "Cancel",
|
"action_common_cancel": "Cancel",
|
||||||
"action_common_clear": "Clear",
|
"action_common_clear": "Clear",
|
||||||
|
@ -86,7 +86,7 @@ class BackgroundSyncWorker {
|
|||||||
result(false)
|
result(false)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
result(FlutterError())
|
result(FlutterError())
|
||||||
self.complete(UIBackgroundFetchResult.failed)
|
self.complete(UIBackgroundFetchResult.failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
mobile/lib/constants/enums.dart
Normal file
4
mobile/lib/constants/enums.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
enum SortOrder {
|
||||||
|
asc,
|
||||||
|
desc,
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||||
@ -23,6 +24,7 @@ class Album {
|
|||||||
this.lastModifiedAssetTimestamp,
|
this.lastModifiedAssetTimestamp,
|
||||||
required this.shared,
|
required this.shared,
|
||||||
required this.activityEnabled,
|
required this.activityEnabled,
|
||||||
|
this.sortOrder = SortOrder.desc,
|
||||||
});
|
});
|
||||||
|
|
||||||
// fields stored in DB
|
// fields stored in DB
|
||||||
@ -39,6 +41,8 @@ class Album {
|
|||||||
DateTime? lastModifiedAssetTimestamp;
|
DateTime? lastModifiedAssetTimestamp;
|
||||||
bool shared;
|
bool shared;
|
||||||
bool activityEnabled;
|
bool activityEnabled;
|
||||||
|
@enumerated
|
||||||
|
SortOrder sortOrder;
|
||||||
final IsarLink<User> owner = IsarLink<User>();
|
final IsarLink<User> owner = IsarLink<User>();
|
||||||
final IsarLink<Asset> thumbnail = IsarLink<Asset>();
|
final IsarLink<Asset> thumbnail = IsarLink<Asset>();
|
||||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||||
@ -154,6 +158,11 @@ class Album {
|
|||||||
);
|
);
|
||||||
a.remoteAssetCount = dto.assetCount;
|
a.remoteAssetCount = dto.assetCount;
|
||||||
a.owner.value = await db.users.getById(dto.ownerId);
|
a.owner.value = await db.users.getById(dto.ownerId);
|
||||||
|
if (dto.order != null) {
|
||||||
|
a.sortOrder =
|
||||||
|
dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc;
|
||||||
|
}
|
||||||
|
|
||||||
if (dto.albumThumbnailAssetId != null) {
|
if (dto.albumThumbnailAssetId != null) {
|
||||||
a.thumbnail.value = await db.assets
|
a.thumbnail.value = await db.assets
|
||||||
.where()
|
.where()
|
||||||
|
116
mobile/lib/entities/album.entity.g.dart
generated
116
mobile/lib/entities/album.entity.g.dart
generated
@ -62,8 +62,14 @@ const AlbumSchema = CollectionSchema(
|
|||||||
name: r'shared',
|
name: r'shared',
|
||||||
type: IsarType.bool,
|
type: IsarType.bool,
|
||||||
),
|
),
|
||||||
r'startDate': PropertySchema(
|
r'sortOrder': PropertySchema(
|
||||||
id: 9,
|
id: 9,
|
||||||
|
name: r'sortOrder',
|
||||||
|
type: IsarType.byte,
|
||||||
|
enumMap: _AlbumsortOrderEnumValueMap,
|
||||||
|
),
|
||||||
|
r'startDate': PropertySchema(
|
||||||
|
id: 10,
|
||||||
name: r'startDate',
|
name: r'startDate',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
)
|
)
|
||||||
@ -171,7 +177,8 @@ void _albumSerialize(
|
|||||||
writer.writeString(offsets[6], object.name);
|
writer.writeString(offsets[6], object.name);
|
||||||
writer.writeString(offsets[7], object.remoteId);
|
writer.writeString(offsets[7], object.remoteId);
|
||||||
writer.writeBool(offsets[8], object.shared);
|
writer.writeBool(offsets[8], object.shared);
|
||||||
writer.writeDateTime(offsets[9], object.startDate);
|
writer.writeByte(offsets[9], object.sortOrder.index);
|
||||||
|
writer.writeDateTime(offsets[10], object.startDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
Album _albumDeserialize(
|
Album _albumDeserialize(
|
||||||
@ -190,7 +197,9 @@ Album _albumDeserialize(
|
|||||||
name: reader.readString(offsets[6]),
|
name: reader.readString(offsets[6]),
|
||||||
remoteId: reader.readStringOrNull(offsets[7]),
|
remoteId: reader.readStringOrNull(offsets[7]),
|
||||||
shared: reader.readBool(offsets[8]),
|
shared: reader.readBool(offsets[8]),
|
||||||
startDate: reader.readDateTimeOrNull(offsets[9]),
|
sortOrder: _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[9])] ??
|
||||||
|
SortOrder.desc,
|
||||||
|
startDate: reader.readDateTimeOrNull(offsets[10]),
|
||||||
);
|
);
|
||||||
object.id = id;
|
object.id = id;
|
||||||
return object;
|
return object;
|
||||||
@ -222,12 +231,24 @@ P _albumDeserializeProp<P>(
|
|||||||
case 8:
|
case 8:
|
||||||
return (reader.readBool(offset)) as P;
|
return (reader.readBool(offset)) as P;
|
||||||
case 9:
|
case 9:
|
||||||
|
return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||||
|
SortOrder.desc) as P;
|
||||||
|
case 10:
|
||||||
return (reader.readDateTimeOrNull(offset)) as P;
|
return (reader.readDateTimeOrNull(offset)) as P;
|
||||||
default:
|
default:
|
||||||
throw IsarError('Unknown property with id $propertyId');
|
throw IsarError('Unknown property with id $propertyId');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _AlbumsortOrderEnumValueMap = {
|
||||||
|
'asc': 0,
|
||||||
|
'desc': 1,
|
||||||
|
};
|
||||||
|
const _AlbumsortOrderValueEnumMap = {
|
||||||
|
0: SortOrder.asc,
|
||||||
|
1: SortOrder.desc,
|
||||||
|
};
|
||||||
|
|
||||||
Id _albumGetId(Album object) {
|
Id _albumGetId(Album object) {
|
||||||
return object.id;
|
return object.id;
|
||||||
}
|
}
|
||||||
@ -1191,6 +1212,59 @@ extension AlbumQueryFilter on QueryBuilder<Album, Album, QFilterCondition> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> sortOrderEqualTo(
|
||||||
|
SortOrder value) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'sortOrder',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> sortOrderGreaterThan(
|
||||||
|
SortOrder value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'sortOrder',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> sortOrderLessThan(
|
||||||
|
SortOrder value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'sortOrder',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> sortOrderBetween(
|
||||||
|
SortOrder lower,
|
||||||
|
SortOrder upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'sortOrder',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, Album, QAfterFilterCondition> startDateIsNull() {
|
QueryBuilder<Album, Album, QAfterFilterCondition> startDateIsNull() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addFilterCondition(const FilterCondition.isNull(
|
return query.addFilterCondition(const FilterCondition.isNull(
|
||||||
@ -1513,6 +1587,18 @@ extension AlbumQuerySortBy on QueryBuilder<Album, Album, QSortBy> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterSortBy> sortBySortOrder() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'sortOrder', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterSortBy> sortBySortOrderDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'sortOrder', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, Album, QAfterSortBy> sortByStartDate() {
|
QueryBuilder<Album, Album, QAfterSortBy> sortByStartDate() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'startDate', Sort.asc);
|
return query.addSortBy(r'startDate', Sort.asc);
|
||||||
@ -1648,6 +1734,18 @@ extension AlbumQuerySortThenBy on QueryBuilder<Album, Album, QSortThenBy> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterSortBy> thenBySortOrder() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'sortOrder', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterSortBy> thenBySortOrderDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'sortOrder', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, Album, QAfterSortBy> thenByStartDate() {
|
QueryBuilder<Album, Album, QAfterSortBy> thenByStartDate() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'startDate', Sort.asc);
|
return query.addSortBy(r'startDate', Sort.asc);
|
||||||
@ -1719,6 +1817,12 @@ extension AlbumQueryWhereDistinct on QueryBuilder<Album, Album, QDistinct> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QDistinct> distinctBySortOrder() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addDistinctBy(r'sortOrder');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, Album, QDistinct> distinctByStartDate() {
|
QueryBuilder<Album, Album, QDistinct> distinctByStartDate() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addDistinctBy(r'startDate');
|
return query.addDistinctBy(r'startDate');
|
||||||
@ -1788,6 +1892,12 @@ extension AlbumQueryProperty on QueryBuilder<Album, Album, QQueryProperty> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, SortOrder, QQueryOperations> sortOrderProperty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addPropertyName(r'sortOrder');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, DateTime?, QQueryOperations> startDateProperty() {
|
QueryBuilder<Album, DateTime?, QQueryOperations> startDateProperty() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addPropertyName(r'startDate');
|
return query.addPropertyName(r'startDate');
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
|
||||||
abstract interface class IAlbumApiRepository {
|
abstract interface class IAlbumApiRepository {
|
||||||
@ -17,6 +18,7 @@ abstract interface class IAlbumApiRepository {
|
|||||||
String? thumbnailAssetId,
|
String? thumbnailAssetId,
|
||||||
String? description,
|
String? description,
|
||||||
bool? activityEnabled,
|
bool? activityEnabled,
|
||||||
|
SortOrder? sortOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> delete(String albumId);
|
Future<void> delete(String albumId);
|
||||||
|
53
mobile/lib/pages/album/album_control_button.dart
Normal file
53
mobile/lib/pages/album/album_control_button.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class AlbumControlButton extends ConsumerWidget {
|
||||||
|
void Function() onAddPhotosPressed;
|
||||||
|
void Function() onAddUsersPressed;
|
||||||
|
|
||||||
|
AlbumControlButton({
|
||||||
|
super.key,
|
||||||
|
required this.onAddPhotosPressed,
|
||||||
|
required this.onAddUsersPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final userId = ref.watch(authProvider).userId;
|
||||||
|
final isOwner = ref.watch(
|
||||||
|
currentAlbumProvider.select((album) {
|
||||||
|
return album?.ownerId == userId;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: [
|
||||||
|
AlbumActionFilledButton(
|
||||||
|
key: const ValueKey('add_photos_button'),
|
||||||
|
iconData: Icons.add_photo_alternate_outlined,
|
||||||
|
onPressed: onAddPhotosPressed,
|
||||||
|
labelText: "share_add_photos".tr(),
|
||||||
|
),
|
||||||
|
if (isOwner)
|
||||||
|
AlbumActionFilledButton(
|
||||||
|
key: const ValueKey('add_users_button'),
|
||||||
|
iconData: Icons.person_add_alt_rounded,
|
||||||
|
onPressed: onAddUsersPressed,
|
||||||
|
labelText: "album_viewer_page_share_add_users".tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
61
mobile/lib/pages/album/album_date_range.dart
Normal file
61
mobile/lib/pages/album/album_date_range.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
|
|
||||||
|
class AlbumDateRange extends ConsumerWidget {
|
||||||
|
const AlbumDateRange({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final data = ref.watch(
|
||||||
|
currentAlbumProvider.select((album) {
|
||||||
|
if (album == null || album.assets.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final startDate = album.startDate;
|
||||||
|
final endDate = album.endDate;
|
||||||
|
if (startDate == null || endDate == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (startDate, endDate, album.shared);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
final (startDate, endDate, shared) = data;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: shared
|
||||||
|
? const EdgeInsets.only(
|
||||||
|
left: 16.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
)
|
||||||
|
: const EdgeInsets.only(left: 16.0, bottom: 8.0),
|
||||||
|
child: Text(
|
||||||
|
_getDateRangeText(startDate, endDate),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
String _getDateRangeText(DateTime startDate, DateTime endDate) {
|
||||||
|
if (startDate.day == endDate.day &&
|
||||||
|
startDate.month == endDate.month &&
|
||||||
|
startDate.year == endDate.year) {
|
||||||
|
return DateFormat.yMMMd().format(startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String startDateText = (startDate.year == endDate.year
|
||||||
|
? DateFormat.MMMd()
|
||||||
|
: DateFormat.yMMMd())
|
||||||
|
.format(startDate);
|
||||||
|
final String endDateText = DateFormat.yMMMd().format(endDate);
|
||||||
|
return "$startDateText - $endDateText";
|
||||||
|
}
|
||||||
|
}
|
@ -7,22 +7,25 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class AlbumOptionsPage extends HookConsumerWidget {
|
class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
final Album album;
|
const AlbumOptionsPage({super.key});
|
||||||
|
|
||||||
const AlbumOptionsPage({super.key, required this.album});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final album = ref.watch(currentAlbumProvider);
|
||||||
|
if (album == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
final sharedUsers = useState(album.sharedUsers.toList());
|
final sharedUsers = useState(album.sharedUsers.toList());
|
||||||
final owner = album.owner.value;
|
final owner = album.owner.value;
|
||||||
final userId = ref.watch(authProvider).userId;
|
final userId = ref.watch(authProvider).userId;
|
56
mobile/lib/pages/album/album_shared_user_icons.dart
Normal file
56
mobile/lib/pages/album/album_shared_user_icons.dart
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||||
|
|
||||||
|
class AlbumSharedUserIcons extends HookConsumerWidget {
|
||||||
|
const AlbumSharedUserIcons({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final sharedUsers = useRef<List<User>>(const []);
|
||||||
|
sharedUsers.value = ref.watch(
|
||||||
|
currentAlbumProvider.select((album) {
|
||||||
|
if (album == null) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (album.sharedUsers.length == sharedUsers.value.length) {
|
||||||
|
return sharedUsers.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return album.sharedUsers.toList(growable: false);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sharedUsers.value.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.pushRoute(AlbumOptionsRoute()),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: UserCircleAvatar(
|
||||||
|
user: sharedUsers.value[index],
|
||||||
|
radius: 18,
|
||||||
|
size: 36,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
itemCount: sharedUsers.value.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
41
mobile/lib/pages/album/album_title.dart
Normal file
41
mobile/lib/pages/album/album_title.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
|
||||||
|
class AlbumTitle extends ConsumerWidget {
|
||||||
|
const AlbumTitle({super.key, required this.titleFocusNode});
|
||||||
|
|
||||||
|
final FocusNode titleFocusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final userId = ref.watch(authProvider).userId;
|
||||||
|
final (isOwner, isRemote, albumName) = ref.watch(
|
||||||
|
currentAlbumProvider.select((album) {
|
||||||
|
if (album == null) {
|
||||||
|
return const (false, false, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (album.ownerId == userId, album.isRemote, album.name);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isOwner && isRemote) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 8),
|
||||||
|
child: AlbumViewerEditableTitle(
|
||||||
|
albumName: albumName,
|
||||||
|
titleFocusNode: titleFocusNode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 8),
|
||||||
|
child: Text(albumName, style: context.textTheme.headlineMedium),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
147
mobile/lib/pages/album/album_viewer.dart
Normal file
147
mobile/lib/pages/album/album_viewer.dart
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||||
|
import 'package:immich_mobile/pages/album/album_control_button.dart';
|
||||||
|
import 'package:immich_mobile/pages/album/album_date_range.dart';
|
||||||
|
import 'package:immich_mobile/pages/album/album_shared_user_icons.dart';
|
||||||
|
import 'package:immich_mobile/pages/album/album_title.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
|
class AlbumViewer extends HookConsumerWidget {
|
||||||
|
const AlbumViewer({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final album = ref.watch(currentAlbumProvider);
|
||||||
|
if (album == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final titleFocusNode = useFocusNode();
|
||||||
|
final userId = ref.watch(authProvider).userId;
|
||||||
|
final isMultiselecting = ref.watch(multiselectProvider);
|
||||||
|
final isProcessing = useProcessingOverlay();
|
||||||
|
|
||||||
|
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
||||||
|
final bool isSuccess =
|
||||||
|
await ref.read(albumProvider.notifier).removeAsset(album, assets);
|
||||||
|
|
||||||
|
if (!isSuccess) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "album_viewer_appbar_share_err_remove".tr(),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return isSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find out if the assets in album exist on the device
|
||||||
|
/// If they exist, add to selected asset state to show they are already selected.
|
||||||
|
void onAddPhotosPressed() async {
|
||||||
|
AssetSelectionPageResult? returnPayload =
|
||||||
|
await context.pushRoute<AssetSelectionPageResult?>(
|
||||||
|
AlbumAssetSelectionRoute(
|
||||||
|
existingAssets: album.assets,
|
||||||
|
canDeselect: false,
|
||||||
|
query: getRemoteAssetQuery(ref),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) {
|
||||||
|
// Check if there is new assets add
|
||||||
|
isProcessing.value = true;
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.watch(albumProvider.notifier)
|
||||||
|
.addAssets(album, returnPayload.selectedAssets);
|
||||||
|
|
||||||
|
isProcessing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddUsersPressed() async {
|
||||||
|
List<String>? sharedUserIds = await context.pushRoute<List<String>?>(
|
||||||
|
AlbumAdditionalSharedUserSelectionRoute(album: album),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sharedUserIds != null) {
|
||||||
|
isProcessing.value = true;
|
||||||
|
|
||||||
|
await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds);
|
||||||
|
|
||||||
|
isProcessing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivitiesPressed() {
|
||||||
|
if (album.remoteId != null) {
|
||||||
|
context.pushRoute(
|
||||||
|
const ActivitiesRoute(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
MultiselectGrid(
|
||||||
|
key: const ValueKey("albumViewerMultiselectGrid"),
|
||||||
|
renderListProvider: albumRenderlistProvider(album.id),
|
||||||
|
topWidget: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AlbumTitle(
|
||||||
|
key: const ValueKey("albumTitle"),
|
||||||
|
titleFocusNode: titleFocusNode,
|
||||||
|
),
|
||||||
|
const AlbumDateRange(),
|
||||||
|
const AlbumSharedUserIcons(),
|
||||||
|
if (album.isRemote)
|
||||||
|
AlbumControlButton(
|
||||||
|
key: const ValueKey("albumControlButton"),
|
||||||
|
onAddPhotosPressed: onAddPhotosPressed,
|
||||||
|
onAddUsersPressed: onAddUsersPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||||
|
editEnabled: album.ownerId == userId,
|
||||||
|
),
|
||||||
|
AnimatedPositioned(
|
||||||
|
key: const ValueKey("albumViewerAppbarPositioned"),
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
top: isMultiselecting ? -(kToolbarHeight + context.padding.top) : 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: AlbumViewerAppbar(
|
||||||
|
key: const ValueKey("albumViewerAppbar"),
|
||||||
|
titleFocusNode: titleFocusNode,
|
||||||
|
userId: userId,
|
||||||
|
onAddPhotos: onAddPhotosPressed,
|
||||||
|
onAddUsers: onAddUsersPressed,
|
||||||
|
onActivities: onActivitiesPressed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
27
mobile/lib/pages/album/album_viewer.page.dart
Normal file
27
mobile/lib/pages/album/album_viewer.page.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/pages/album/album_viewer.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class AlbumViewerPage extends HookConsumerWidget {
|
||||||
|
final int albumId;
|
||||||
|
|
||||||
|
const AlbumViewerPage({super.key, required this.albumId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Listen provider to prevent autoDispose when navigating to other routes from within the viewer page
|
||||||
|
ref.listen(currentAlbumProvider, (_, __) {});
|
||||||
|
|
||||||
|
ref.listen(albumWatcher(albumId), (_, albumFuture) {
|
||||||
|
albumFuture.whenData(
|
||||||
|
(value) => ref.read(currentAlbumProvider.notifier).set(value),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return const Scaffold(body: AlbumViewer());
|
||||||
|
}
|
||||||
|
}
|
@ -1,267 +0,0 @@
|
|||||||
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/asyncvalue_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/album_viewer_appbar.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class AlbumViewerPage extends HookConsumerWidget {
|
|
||||||
final int albumId;
|
|
||||||
|
|
||||||
const AlbumViewerPage({super.key, required this.albumId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
FocusNode titleFocusNode = useFocusNode();
|
|
||||||
final album = ref.watch(albumWatcher(albumId));
|
|
||||||
// Listen provider to prevent autoDispose when navigating to other routes from within the viewer page
|
|
||||||
ref.listen(currentAlbumProvider, (_, __) {});
|
|
||||||
album.whenData(
|
|
||||||
(value) => Future.microtask(
|
|
||||||
() => ref.read(currentAlbumProvider.notifier).set(value),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final userId = ref.watch(authProvider).userId;
|
|
||||||
final isProcessing = useProcessingOverlay();
|
|
||||||
|
|
||||||
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
|
||||||
final a = album.valueOrNull;
|
|
||||||
final bool isSuccess = a != null &&
|
|
||||||
await ref.read(albumProvider.notifier).removeAsset(a, assets);
|
|
||||||
|
|
||||||
if (!isSuccess) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: "album_viewer_appbar_share_err_remove".tr(),
|
|
||||||
toastType: ToastType.error,
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return isSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find out if the assets in album exist on the device
|
|
||||||
/// If they exist, add to selected asset state to show they are already selected.
|
|
||||||
void onAddPhotosPressed(Album albumInfo) async {
|
|
||||||
AssetSelectionPageResult? returnPayload =
|
|
||||||
await context.pushRoute<AssetSelectionPageResult?>(
|
|
||||||
AlbumAssetSelectionRoute(
|
|
||||||
existingAssets: albumInfo.assets,
|
|
||||||
canDeselect: false,
|
|
||||||
query: getRemoteAssetQuery(ref),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) {
|
|
||||||
// Check if there is new assets add
|
|
||||||
isProcessing.value = true;
|
|
||||||
|
|
||||||
await ref.watch(albumProvider.notifier).addAssets(
|
|
||||||
albumInfo,
|
|
||||||
returnPayload.selectedAssets,
|
|
||||||
);
|
|
||||||
|
|
||||||
isProcessing.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAddUsersPressed(Album album) async {
|
|
||||||
List<String>? sharedUserIds = await context.pushRoute<List<String>?>(
|
|
||||||
AlbumAdditionalSharedUserSelectionRoute(album: album),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sharedUserIds != null) {
|
|
||||||
isProcessing.value = true;
|
|
||||||
|
|
||||||
await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds);
|
|
||||||
|
|
||||||
isProcessing.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildControlButton(Album album) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 40,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children: [
|
|
||||||
AlbumActionFilledButton(
|
|
||||||
iconData: Icons.add_photo_alternate_outlined,
|
|
||||||
onPressed: () => onAddPhotosPressed(album),
|
|
||||||
labelText: "share_add_photos".tr(),
|
|
||||||
),
|
|
||||||
if (userId == album.ownerId)
|
|
||||||
AlbumActionFilledButton(
|
|
||||||
iconData: Icons.person_add_alt_rounded,
|
|
||||||
onPressed: () => onAddUsersPressed(album),
|
|
||||||
labelText: "album_viewer_page_share_add_users".tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildTitle(Album album) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8),
|
|
||||||
child: userId == album.ownerId && album.isRemote
|
|
||||||
? AlbumViewerEditableTitle(
|
|
||||||
album: album,
|
|
||||||
titleFocusNode: titleFocusNode,
|
|
||||||
)
|
|
||||||
: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: Text(
|
|
||||||
album.name,
|
|
||||||
style: context.textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildAlbumDateRange(Album album) {
|
|
||||||
final DateTime? startDate = album.startDate;
|
|
||||||
final DateTime? endDate = album.endDate;
|
|
||||||
|
|
||||||
if (startDate == null || endDate == null) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
final String dateRangeText;
|
|
||||||
if (startDate.day == endDate.day &&
|
|
||||||
startDate.month == endDate.month &&
|
|
||||||
startDate.year == endDate.year) {
|
|
||||||
dateRangeText = DateFormat.yMMMd().format(startDate);
|
|
||||||
} else {
|
|
||||||
final String startDateText = (startDate.year == endDate.year
|
|
||||||
? DateFormat.MMMd()
|
|
||||||
: DateFormat.yMMMd())
|
|
||||||
.format(startDate);
|
|
||||||
final String endDateText = DateFormat.yMMMd().format(endDate);
|
|
||||||
dateRangeText = "$startDateText - $endDateText";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 16.0,
|
|
||||||
bottom: album.shared ? 0.0 : 8.0,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
dateRangeText,
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildSharedUserIconsRow(Album album) {
|
|
||||||
return album.sharedUsers.isNotEmpty
|
|
||||||
? GestureDetector(
|
|
||||||
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
itemCount: album.sharedUsers.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildHeader(Album album) {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
buildTitle(album),
|
|
||||||
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
|
||||||
buildSharedUserIconsRow(album),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onActivitiesPressed(Album album) {
|
|
||||||
if (album.remoteId != null) {
|
|
||||||
context.pushRoute(
|
|
||||||
const ActivitiesRoute(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
album.widgetWhen(
|
|
||||||
onData: (albumInfo) => MultiselectGrid(
|
|
||||||
renderListProvider: albumRenderlistProvider(albumId),
|
|
||||||
topWidget: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
buildHeader(albumInfo),
|
|
||||||
if (albumInfo.isRemote) buildControlButton(albumInfo),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
|
||||||
editEnabled: albumInfo.ownerId == userId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
AnimatedPositioned(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
top: ref.watch(multiselectProvider)
|
|
||||||
? -(kToolbarHeight + context.padding.top)
|
|
||||||
: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: album.when(
|
|
||||||
data: (data) => AlbumViewerAppbar(
|
|
||||||
titleFocusNode: titleFocusNode,
|
|
||||||
album: data,
|
|
||||||
userId: userId,
|
|
||||||
onAddPhotos: onAddPhotosPressed,
|
|
||||||
onAddUsers: onAddUsersPressed,
|
|
||||||
onActivities: onActivitiesPressed,
|
|
||||||
),
|
|
||||||
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
|
||||||
loading: () => AppBar(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
@ -106,6 +107,13 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
return _albumService.setActivityStatus(album, enabled);
|
return _albumService.setActivityStatus(album, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Album?> toggleSortOrder(Album album) {
|
||||||
|
final order =
|
||||||
|
album.sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
|
||||||
|
|
||||||
|
return _albumService.updateSortOrder(album, order);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_streamSub.cancel();
|
_streamSub.cancel();
|
||||||
@ -135,11 +143,22 @@ final albumWatcher =
|
|||||||
final albumRenderlistProvider =
|
final albumRenderlistProvider =
|
||||||
StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
|
StreamProvider.autoDispose.family<RenderList, int>((ref, albumId) {
|
||||||
final album = ref.watch(albumWatcher(albumId)).value;
|
final album = ref.watch(albumWatcher(albumId)).value;
|
||||||
|
|
||||||
if (album != null) {
|
if (album != null) {
|
||||||
final query =
|
final query = album.assets.filter().isTrashedEqualTo(false);
|
||||||
album.assets.filter().isTrashedEqualTo(false).sortByFileCreatedAtDesc();
|
if (album.sortOrder == SortOrder.asc) {
|
||||||
return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none);
|
return renderListGeneratorWithGroupBy(
|
||||||
|
query.sortByFileCreatedAt(),
|
||||||
|
GroupAssetsBy.none,
|
||||||
|
);
|
||||||
|
} else if (album.sortOrder == SortOrder.desc) {
|
||||||
|
return renderListGeneratorWithGroupBy(
|
||||||
|
query.sortByFileCreatedAtDesc(),
|
||||||
|
GroupAssetsBy.none,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return const Stream.empty();
|
return const Stream.empty();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
enum RenderListStatusEnum { complete, empty, error, loading }
|
||||||
|
|
||||||
|
final renderListStatusProvider =
|
||||||
|
StateNotifierProvider<RenderListStatus, RenderListStatusEnum>((ref) {
|
||||||
|
return RenderListStatus(ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
class RenderListStatus extends StateNotifier<RenderListStatusEnum> {
|
||||||
|
RenderListStatus(this.ref) : super(RenderListStatusEnum.complete);
|
||||||
|
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
RenderListStatusEnum get status => state;
|
||||||
|
|
||||||
|
set status(RenderListStatusEnum value) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ part of 'backup_verification.provider.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$backupVerificationHash() =>
|
String _$backupVerificationHash() =>
|
||||||
r'021dfdf65e1903c932e4a1c14967b786dd3516fb';
|
r'b204e43ab575d5fa5b2ee663297f32bcee9074f5';
|
||||||
|
|
||||||
/// See also [BackupVerification].
|
/// See also [BackupVerification].
|
||||||
@ProviderFor(BackupVerification)
|
@ProviderFor(BackupVerification)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
@ -56,7 +57,13 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
|||||||
String? thumbnailAssetId,
|
String? thumbnailAssetId,
|
||||||
String? description,
|
String? description,
|
||||||
bool? activityEnabled,
|
bool? activityEnabled,
|
||||||
|
SortOrder? sortOrder,
|
||||||
}) async {
|
}) async {
|
||||||
|
AssetOrder? order;
|
||||||
|
if (sortOrder != null) {
|
||||||
|
order = sortOrder == SortOrder.asc ? AssetOrder.asc : AssetOrder.desc;
|
||||||
|
}
|
||||||
|
|
||||||
final response = await checkNull(
|
final response = await checkNull(
|
||||||
_api.updateAlbumInfo(
|
_api.updateAlbumInfo(
|
||||||
albumId,
|
albumId,
|
||||||
@ -65,9 +72,11 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
|||||||
albumThumbnailAssetId: thumbnailAssetId,
|
albumThumbnailAssetId: thumbnailAssetId,
|
||||||
description: description,
|
description: description,
|
||||||
isActivityEnabled: activityEnabled,
|
isActivityEnabled: activityEnabled,
|
||||||
|
order: order,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return _toAlbum(response);
|
return _toAlbum(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,6 +161,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
|||||||
startDate: dto.startDate,
|
startDate: dto.startDate,
|
||||||
endDate: dto.endDate,
|
endDate: dto.endDate,
|
||||||
activityEnabled: dto.isActivityEnabled,
|
activityEnabled: dto.isActivityEnabled,
|
||||||
|
sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
|
||||||
);
|
);
|
||||||
album.remoteAssetCount = dto.assetCount;
|
album.remoteAssetCount = dto.assetCount;
|
||||||
album.owner.value = User.fromSimpleUserDto(dto.owner);
|
album.owner.value = User.fromSimpleUserDto(dto.owner);
|
||||||
|
@ -20,11 +20,11 @@ import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
|||||||
import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
|
import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
import 'package:immich_mobile/pages/album/album_asset_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_options.page.dart';
|
import 'package:immich_mobile/pages/album/album_options.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_shared_user_selection.page.dart';
|
import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_viewer.page.dart';
|
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
||||||
|
@ -139,13 +139,11 @@ class AlbumAssetSelectionRouteArgs {
|
|||||||
class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
|
class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
|
||||||
AlbumOptionsRoute({
|
AlbumOptionsRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required Album album,
|
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
AlbumOptionsRoute.name,
|
AlbumOptionsRoute.name,
|
||||||
args: AlbumOptionsRouteArgs(
|
args: AlbumOptionsRouteArgs(
|
||||||
key: key,
|
key: key,
|
||||||
album: album,
|
|
||||||
),
|
),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
@ -158,25 +156,19 @@ class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
|
|||||||
final args = data.argsAs<AlbumOptionsRouteArgs>();
|
final args = data.argsAs<AlbumOptionsRouteArgs>();
|
||||||
return AlbumOptionsPage(
|
return AlbumOptionsPage(
|
||||||
key: args.key,
|
key: args.key,
|
||||||
album: args.album,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumOptionsRouteArgs {
|
class AlbumOptionsRouteArgs {
|
||||||
const AlbumOptionsRouteArgs({
|
const AlbumOptionsRouteArgs({this.key});
|
||||||
this.key,
|
|
||||||
required this.album,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final Album album;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AlbumOptionsRouteArgs{key: $key, album: $album}';
|
return 'AlbumOptionsRouteArgs{key: $key}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import 'dart:io';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
@ -436,4 +437,17 @@ class AlbumService {
|
|||||||
) async {
|
) async {
|
||||||
return _albumRepository.search(searchTerm, filterMode);
|
return _albumRepository.search(searchTerm, filterMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Album?> updateSortOrder(Album album, SortOrder order) async {
|
||||||
|
try {
|
||||||
|
final updateAlbum =
|
||||||
|
await _albumApiRepository.update(album.remoteId!, sortOrder: order);
|
||||||
|
album.sortOrder = updateAlbum.sortOrder;
|
||||||
|
|
||||||
|
return _albumRepository.update(album);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_log.severe("Error updating album sort order", error, stackTrace);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -403,6 +403,8 @@ class SyncService {
|
|||||||
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
||||||
album.shared = dto.shared;
|
album.shared = dto.shared;
|
||||||
album.activityEnabled = dto.activityEnabled;
|
album.activityEnabled = dto.activityEnabled;
|
||||||
|
album.sortOrder = dto.sortOrder;
|
||||||
|
|
||||||
final remoteThumbnailAssetId = dto.remoteThumbnailAssetId;
|
final remoteThumbnailAssetId = dto.remoteThumbnailAssetId;
|
||||||
if (remoteThumbnailAssetId != null &&
|
if (remoteThumbnailAssetId != null &&
|
||||||
album.thumbnail.value?.remoteId != remoteThumbnailAssetId) {
|
album.thumbnail.value?.remoteId != remoteThumbnailAssetId) {
|
||||||
|
@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/utils/db.dart';
|
import 'package:immich_mobile/utils/db.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
const int targetVersion = 7;
|
const int targetVersion = 8;
|
||||||
|
|
||||||
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||||
final int version = Store.get(StoreKey.version, 1);
|
final int version = Store.get(StoreKey.version, 1);
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/activity_statistics.provider.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.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class AlbumViewerAppbar extends HookConsumerWidget
|
class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
implements PreferredSizeWidget {
|
implements PreferredSizeWidget {
|
||||||
const AlbumViewerAppbar({
|
const AlbumViewerAppbar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.album,
|
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.titleFocusNode,
|
required this.titleFocusNode,
|
||||||
this.onAddPhotos,
|
this.onAddPhotos,
|
||||||
@ -24,34 +23,48 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
required this.onActivities,
|
required this.onActivities,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Album album;
|
|
||||||
final String userId;
|
final String userId;
|
||||||
final FocusNode titleFocusNode;
|
final FocusNode titleFocusNode;
|
||||||
final Function(Album album)? onAddPhotos;
|
final void Function()? onAddPhotos;
|
||||||
final Function(Album album)? onAddUsers;
|
final void Function()? onAddUsers;
|
||||||
final Function(Album album) onActivities;
|
final void Function() onActivities;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
final albumState = useState(ref.read(currentAlbumProvider));
|
||||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
final album = albumState.value;
|
||||||
final isProcessing = useProcessingOverlay();
|
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 isEditAlbum = albumViewer.isEditAlbum;
|
||||||
|
|
||||||
final comments = album.shared
|
final comments = album.shared
|
||||||
? ref.watch(activityStatisticsProvider(album.remoteId!))
|
? ref.watch(activityStatisticsProvider(album.remoteId!))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
deleteAlbum() async {
|
deleteAlbum() async {
|
||||||
isProcessing.value = true;
|
final bool success =
|
||||||
|
await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||||
|
|
||||||
final bool success;
|
|
||||||
if (album.shared) {
|
if (album.shared) {
|
||||||
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
|
||||||
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
|
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
|
||||||
} else {
|
} else {
|
||||||
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
|
||||||
context
|
context
|
||||||
.navigateTo(const TabControllerRoute(children: [LibraryRoute()]));
|
.navigateTo(const TabControllerRoute(children: [LibraryRoute()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@ -60,11 +73,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isProcessing.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showConfirmationDialog() async {
|
Future<void> onDeleteAlbumPressed() {
|
||||||
return showDialog<void>(
|
return showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false, // user must tap button!
|
barrierDismissible: false, // user must tap button!
|
||||||
@ -102,13 +113,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDeleteAlbumPressed() async {
|
|
||||||
showConfirmationDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onLeaveAlbumPressed() async {
|
void onLeaveAlbumPressed() async {
|
||||||
isProcessing.value = true;
|
|
||||||
|
|
||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
await ref.watch(albumProvider.notifier).leaveAlbum(album);
|
await ref.watch(albumProvider.notifier).leaveAlbum(album);
|
||||||
|
|
||||||
@ -123,8 +128,6 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isProcessing.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildBottomSheetActions() {
|
buildBottomSheetActions() {
|
||||||
@ -136,7 +139,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
'album_viewer_appbar_share_delete',
|
'album_viewer_appbar_share_delete',
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
).tr(),
|
).tr(),
|
||||||
onTap: () => onDeleteAlbumPressed(),
|
onTap: onDeleteAlbumPressed,
|
||||||
)
|
)
|
||||||
: ListTile(
|
: ListTile(
|
||||||
leading: const Icon(Icons.person_remove_rounded),
|
leading: const Icon(Icons.person_remove_rounded),
|
||||||
@ -144,25 +147,52 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
'album_viewer_appbar_share_leave',
|
'album_viewer_appbar_share_leave',
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
).tr(),
|
).tr(),
|
||||||
onTap: () => onLeaveAlbumPressed(),
|
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() {
|
void buildBottomSheet() {
|
||||||
final ownerActions = [
|
final ownerActions = [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.person_add_alt_rounded),
|
leading: const Icon(Icons.person_add_alt_rounded),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
onAddUsers!(album);
|
final onAddUsers = this.onAddUsers;
|
||||||
|
if (onAddUsers != null) {
|
||||||
|
onAddUsers();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"album_viewer_page_share_add_users",
|
"album_viewer_page_share_add_users",
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.swap_vert_rounded),
|
||||||
|
onTap: onSortOrderToggled,
|
||||||
|
title: const Text(
|
||||||
|
"change_display_order",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.share_rounded),
|
leading: const Icon(Icons.share_rounded),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -176,7 +206,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.settings_rounded),
|
leading: const Icon(Icons.settings_rounded),
|
||||||
onTap: () => context.navigateTo(AlbumOptionsRoute(album: album)),
|
onTap: () => context.navigateTo(AlbumOptionsRoute()),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"translated_text_options",
|
"translated_text_options",
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
@ -189,7 +219,10 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
leading: const Icon(Icons.add_photo_alternate_outlined),
|
leading: const Icon(Icons.add_photo_alternate_outlined),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
onAddPhotos!(album);
|
final onAddPhotos = this.onAddPhotos;
|
||||||
|
if (onAddPhotos != null) {
|
||||||
|
onAddPhotos();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"share_add_photos",
|
"share_add_photos",
|
||||||
@ -222,9 +255,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
|
|
||||||
Widget buildActivitiesButton() {
|
Widget buildActivitiesButton() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: onActivities,
|
||||||
onActivities(album);
|
|
||||||
},
|
|
||||||
icon: Row(
|
icon: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -271,7 +302,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async => await context.maybePop(),
|
onPressed: context.maybePop,
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
);
|
);
|
||||||
@ -285,12 +316,13 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
actions: [
|
actions: [
|
||||||
if (album.shared && (album.activityEnabled || comments != 0))
|
if (album.shared && (album.activityEnabled || comments != 0))
|
||||||
buildActivitiesButton(),
|
buildActivitiesButton(),
|
||||||
if (album.isRemote)
|
if (album.isRemote) ...[
|
||||||
IconButton(
|
IconButton(
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
onPressed: buildBottomSheet,
|
onPressed: buildBottomSheet,
|
||||||
icon: const Icon(Icons.more_horiz_rounded),
|
icon: const Icon(Icons.more_horiz_rounded),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,20 +4,19 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
|
||||||
|
|
||||||
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||||
final Album album;
|
final String albumName;
|
||||||
final FocusNode titleFocusNode;
|
final FocusNode titleFocusNode;
|
||||||
const AlbumViewerEditableTitle({
|
const AlbumViewerEditableTitle({
|
||||||
super.key,
|
super.key,
|
||||||
required this.album,
|
required this.albumName,
|
||||||
required this.titleFocusNode,
|
required this.titleFocusNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final titleTextEditController = useTextEditingController(text: album.name);
|
final titleTextEditController = useTextEditingController(text: albumName);
|
||||||
|
|
||||||
void onFocusModeChange() {
|
void onFocusModeChange() {
|
||||||
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
||||||
@ -51,7 +50,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
context.focusScope.requestFocus(titleFocusNode);
|
context.focusScope.requestFocus(titleFocusNode);
|
||||||
|
|
||||||
ref.watch(albumViewerProvider.notifier).setEditTitleText(album.name);
|
ref.watch(albumViewerProvider.notifier).setEditTitleText(albumName);
|
||||||
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
||||||
|
|
||||||
if (titleTextEditController.text == 'Untitled') {
|
if (titleTextEditController.text == 'Untitled') {
|
||||||
|
@ -433,6 +433,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (selectionEnabledHook.value)
|
if (selectionEnabledHook.value)
|
||||||
ControlBottomAppBar(
|
ControlBottomAppBar(
|
||||||
|
key: const ValueKey("controlBottomAppBar"),
|
||||||
onShare: onShareAssets,
|
onShare: onShareAssets,
|
||||||
onFavorite: favoriteEnabled ? onFavoriteAssets : null,
|
onFavorite: favoriteEnabled ? onFavoriteAssets : null,
|
||||||
onArchive: archiveEnabled ? onArchiveAsset : null,
|
onArchive: archiveEnabled ? onArchiveAsset : null,
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/render_list_status_provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
|
||||||
|
|
||||||
|
class MultiselectGridStatusIndicator extends HookConsumerWidget {
|
||||||
|
const MultiselectGridStatusIndicator({
|
||||||
|
super.key,
|
||||||
|
this.buildLoadingIndicator,
|
||||||
|
this.emptyIndicator,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget Function()? buildLoadingIndicator;
|
||||||
|
final Widget? emptyIndicator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final renderListStatus = ref.watch(renderListStatusProvider);
|
||||||
|
return switch (renderListStatus) {
|
||||||
|
RenderListStatusEnum.loading => buildLoadingIndicator == null
|
||||||
|
? const Center(
|
||||||
|
child: DelayedLoadingIndicator(
|
||||||
|
delay: Duration(milliseconds: 500),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: buildLoadingIndicator!(),
|
||||||
|
RenderListStatusEnum.empty =>
|
||||||
|
emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()),
|
||||||
|
RenderListStatusEnum.error =>
|
||||||
|
Center(child: const Text("error_loading_assets").tr()),
|
||||||
|
RenderListStatusEnum.complete => const SizedBox()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
3
mobile/openapi/devtools_options.yaml
Normal file
3
mobile/openapi/devtools_options.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
Loading…
x
Reference in New Issue
Block a user