Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 76 additions & 20 deletions mobile/lib/presentation/widgets/album/album_selector.widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/album_filter.utils.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/search_field.dart';
Expand All @@ -39,8 +40,12 @@ class AlbumSelector extends ConsumerStatefulWidget {
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
bool isGrid = false;
final searchController = TextEditingController();
QuickFilterMode filterMode = QuickFilterMode.all;
final searchFocusNode = FocusNode();
List<RemoteAlbum> sortedAlbums = [];
List<RemoteAlbum> shownAlbums = [];

AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all);
AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true);

@override
void initState() {
Expand All @@ -52,7 +57,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
});

searchController.addListener(() {
onSearch(searchController.text, filterMode);
onSearch(searchController.text, filter.mode);
});

searchFocusNode.addListener(() {
Expand All @@ -62,9 +67,11 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
});
}

void onSearch(String searchTerm, QuickFilterMode sortMode) {
void onSearch(String searchTerm, QuickFilterMode filterMode) {
final userId = ref.watch(currentUserProvider)?.id;
ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode);
filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode);

filterAlbums();
}

Future<void> onRefresh() async {
Expand All @@ -77,17 +84,60 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
});
}

void changeFilter(QuickFilterMode sortMode) {
void changeFilter(QuickFilterMode mode) {
setState(() {
filter = filter.copyWith(mode: mode);
});

filterAlbums();
}

Future<void> changeSort(AlbumSort sort) async {
setState(() {
filterMode = sortMode;
this.sort = sort;
});

await sortAlbums();
}

void clearSearch() {
setState(() {
filterMode = QuickFilterMode.all;
filter = filter.copyWith(mode: QuickFilterMode.all, query: null);
searchController.clear();
ref.read(remoteAlbumProvider.notifier).clearSearch();
});

filterAlbums();
}

Future<void> sortAlbums() async {
final sorted = await ref
.read(remoteAlbumProvider.notifier)
.sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse);

setState(() {
sortedAlbums = sorted;
});

// we need to re-filter the albums after sorting
// so shownAlbums gets updated
filterAlbums();
}

Future<void> filterAlbums() async {
if (filter.query == null) {
setState(() {
shownAlbums = sortedAlbums;
});

return;
}

final filteredAlbums = ref
.read(remoteAlbumProvider.notifier)
.searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode);

setState(() {
shownAlbums = filteredAlbums;
});
}

Expand All @@ -100,36 +150,41 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {

@override
Widget build(BuildContext context) {
final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));

final userId = ref.watch(currentUserProvider)?.id;

// refilter and sort when albums change
ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async {
await sortAlbums();
});

return MultiSliver(
children: [
_SearchBar(
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearch: onSearch,
filterMode: filterMode,
filterMode: filter.mode,
onClearSearch: clearSearch,
),
_QuickFilterButtonRow(
filterMode: filterMode,
filterMode: filter.mode,
onChangeFilter: changeFilter,
onSearch: onSearch,
searchController: searchController,
),
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode),
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode, onSortChanged: changeSort),
isGrid
? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
: _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
],
);
}
}

class _SortButton extends ConsumerStatefulWidget {
const _SortButton();
const _SortButton(this.onSortChanged);

final Future<void> Function(AlbumSort) onSortChanged;

@override
ConsumerState<_SortButton> createState() => _SortButtonState();
Expand All @@ -148,15 +203,15 @@ class _SortButtonState extends ConsumerState<_SortButton> {
albumSortIsReverse = !albumSortIsReverse;
isSorting = true;
});
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
} else {
setState(() {
albumSortOption = sortMode;
isSorting = true;
});
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
}

await widget.onSortChanged.call(AlbumSort(mode: albumSortOption, isReverse: albumSortIsReverse));

setState(() {
isSorting = false;
});
Expand Down Expand Up @@ -394,10 +449,11 @@ class _QuickFilterButton extends StatelessWidget {
}

class _QuickSortAndViewMode extends StatelessWidget {
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode});
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode, required this.onSortChanged});

final bool isGrid;
final VoidCallback onToggleViewMode;
final Future<void> Function(AlbumSort) onSortChanged;

@override
Widget build(BuildContext context) {
Expand All @@ -407,7 +463,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const _SortButton(),
_SortButton(onSortChanged),
IconButton(
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
onPressed: onToggleViewMode,
Expand Down
59 changes: 26 additions & 33 deletions mobile/lib/providers/infrastructure/remote_album.provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,42 @@ import 'album.provider.dart';

class RemoteAlbumState {
final List<RemoteAlbum> albums;
final List<RemoteAlbum> filteredAlbums;

const RemoteAlbumState({required this.albums, List<RemoteAlbum>? filteredAlbums})
: filteredAlbums = filteredAlbums ?? albums;
const RemoteAlbumState({required this.albums});

RemoteAlbumState copyWith({List<RemoteAlbum>? albums, List<RemoteAlbum>? filteredAlbums}) {
return RemoteAlbumState(albums: albums ?? this.albums, filteredAlbums: filteredAlbums ?? this.filteredAlbums);
RemoteAlbumState copyWith({List<RemoteAlbum>? albums}) {
return RemoteAlbumState(albums: albums ?? this.albums);
}

@override
String toString() => 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})';
String toString() => 'RemoteAlbumState(albums: ${albums.length})';

@override
bool operator ==(covariant RemoteAlbumState other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;

return listEquals(other.albums, albums) && listEquals(other.filteredAlbums, filteredAlbums);
return listEquals(other.albums, albums);
}

@override
int get hashCode => albums.hashCode ^ filteredAlbums.hashCode;
int get hashCode => albums.hashCode;
}

class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
late RemoteAlbumService _remoteAlbumService;
final _logger = Logger('RemoteAlbumNotifier');

@override
RemoteAlbumState build() {
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
return const RemoteAlbumState(albums: [], filteredAlbums: []);
return const RemoteAlbumState(albums: []);
}

Future<List<RemoteAlbum>> _getAll() async {
try {
final albums = await _remoteAlbumService.getAll();
state = state.copyWith(albums: albums, filteredAlbums: albums);
state = state.copyWith(albums: albums);
return albums;
} catch (error, stack) {
_logger.severe('Failed to fetch albums', error, stack);
Expand All @@ -60,19 +59,21 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
await _getAll();
}

void searchAlbums(String query, String? userId, [QuickFilterMode filterMode = QuickFilterMode.all]) {
final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode);

state = state.copyWith(filteredAlbums: filtered);
List<RemoteAlbum> searchAlbums(
List<RemoteAlbum> albums,
String query,
String? userId, [
QuickFilterMode filterMode = QuickFilterMode.all,
]) {
return _remoteAlbumService.searchAlbums(albums, query, userId, filterMode);
}

void clearSearch() {
state = state.copyWith(filteredAlbums: state.albums);
}

Future<void> sortFilteredAlbums(RemoteAlbumSortMode sortMode, {bool isReverse = false}) async {
final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
state = state.copyWith(filteredAlbums: sortedAlbums);
Future<List<RemoteAlbum>> sortAlbums(
List<RemoteAlbum> albums,
RemoteAlbumSortMode sortMode, {
bool isReverse = false,
}) async {
return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse);
}

Future<RemoteAlbum?> createAlbum({
Expand All @@ -83,7 +84,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
try {
final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds);

state = state.copyWith(albums: [...state.albums, album], filteredAlbums: [...state.filteredAlbums, album]);
state = state.copyWith(albums: [...state.albums, album]);

return album;
} catch (error, stack) {
Expand Down Expand Up @@ -114,11 +115,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
return album.id == albumId ? updatedAlbum : album;
}).toList();

final updatedFilteredAlbums = state.filteredAlbums.map((album) {
return album.id == albumId ? updatedAlbum : album;
}).toList();

state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
state = state.copyWith(albums: updatedAlbums);

return updatedAlbum;
} catch (error, stack) {
Expand All @@ -139,9 +136,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
await _remoteAlbumService.deleteAlbum(albumId);

final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();

state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
state = state.copyWith(albums: updatedAlbums);
}

Future<List<RemoteAsset>> getAssets(String albumId) {
Expand All @@ -164,9 +159,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
await _remoteAlbumService.removeUser(albumId, userId: userId);

final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();

state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
state = state.copyWith(albums: updatedAlbums);
}

Future<void> setActivityStatus(String albumId, bool enabled) {
Expand Down
25 changes: 25 additions & 0 deletions mobile/lib/utils/album_filter.utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';

class AlbumFilter {
String? userId;
String? query;
QuickFilterMode mode;

AlbumFilter({required this.mode, this.userId, this.query});

AlbumFilter copyWith({String? userId, String? query, QuickFilterMode? mode}) {
return AlbumFilter(userId: userId ?? this.userId, query: query ?? this.query, mode: mode ?? this.mode);
}
}

class AlbumSort {
RemoteAlbumSortMode mode;
bool isReverse;

AlbumSort({required this.mode, this.isReverse = false});

AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) {
return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse);
}
}