mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: add to album on new beta timeline (#20119)
* feat: add to album on new beta timeline * handle add album button * tune --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
de67d22bc0
commit
06c78dfa91
@ -182,8 +182,7 @@ class _BetaLandscapeToggle extends HookWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 100, child: BetaTimelineListTile()),
|
||||
if (Store.isBetaTimelineEnabled)
|
||||
const Expanded(child: BetaSyncSettings()),
|
||||
if (Store.isBetaTimelineEnabled) const Expanded(child: BetaSyncSettings()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftAlbumsPage extends ConsumerStatefulWidget {
|
||||
@ -29,67 +18,12 @@ class DriftAlbumsPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
||||
bool isGrid = false;
|
||||
final searchController = TextEditingController();
|
||||
QuickFilterMode filterMode = QuickFilterMode.all;
|
||||
final searchFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Load albums when component mounts
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||
});
|
||||
|
||||
searchController.addListener(() {
|
||||
onSearch(searchController.text, filterMode);
|
||||
});
|
||||
}
|
||||
|
||||
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode);
|
||||
}
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
await ref.read(remoteAlbumProvider.notifier).refresh();
|
||||
}
|
||||
|
||||
void toggleViewMode() {
|
||||
setState(() {
|
||||
isGrid = !isGrid;
|
||||
});
|
||||
}
|
||||
|
||||
void changeFilter(QuickFilterMode sortMode) {
|
||||
setState(() {
|
||||
filterMode = sortMode;
|
||||
});
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
setState(() {
|
||||
filterMode = QuickFilterMode.all;
|
||||
searchController.clear();
|
||||
ref.read(remoteAlbumProvider.notifier).clearSearch();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));
|
||||
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
edgeOffset: 100,
|
||||
@ -112,616 +46,16 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
||||
],
|
||||
showUploadButton: false,
|
||||
),
|
||||
_SearchBar(
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearch: onSearch,
|
||||
filterMode: filterMode,
|
||||
onClearSearch: clearSearch,
|
||||
AlbumSelector(
|
||||
onAlbumSelected: (album) {
|
||||
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
|
||||
context.router.push(
|
||||
RemoteAlbumRoute(album: album),
|
||||
);
|
||||
},
|
||||
),
|
||||
_QuickFilterButtonRow(
|
||||
filterMode: filterMode,
|
||||
onChangeFilter: changeFilter,
|
||||
onSearch: onSearch,
|
||||
searchController: searchController,
|
||||
),
|
||||
_QuickSortAndViewMode(
|
||||
isGrid: isGrid,
|
||||
onToggleViewMode: toggleViewMode,
|
||||
),
|
||||
isGrid
|
||||
? _AlbumGrid(
|
||||
albums: albums,
|
||||
userId: userId,
|
||||
)
|
||||
: _AlbumList(
|
||||
albums: albums,
|
||||
userId: userId,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortButton extends ConsumerStatefulWidget {
|
||||
const _SortButton();
|
||||
|
||||
@override
|
||||
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||
}
|
||||
|
||||
class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
RemoteAlbumSortMode albumSortOption = RemoteAlbumSortMode.lastModified;
|
||||
bool albumSortIsReverse = true;
|
||||
|
||||
void onMenuTapped(RemoteAlbumSortMode sortMode) {
|
||||
final selected = albumSortOption == sortMode;
|
||||
// Switch direction
|
||||
if (selected) {
|
||||
setState(() {
|
||||
albumSortIsReverse = !albumSortIsReverse;
|
||||
});
|
||||
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
|
||||
sortMode,
|
||||
isReverse: albumSortIsReverse,
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
albumSortOption = sortMode;
|
||||
});
|
||||
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
|
||||
sortMode,
|
||||
isReverse: albumSortIsReverse,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MenuAnchor(
|
||||
style: MenuStyle(
|
||||
elevation: const WidgetStatePropertyAll(1),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.all(4),
|
||||
),
|
||||
),
|
||||
consumeOutsideTap: true,
|
||||
menuChildren: RemoteAlbumSortMode.values
|
||||
.map(
|
||||
(sortMode) => MenuItemButton(
|
||||
leadingIcon: albumSortOption == sortMode
|
||||
? albumSortIsReverse
|
||||
? Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: albumSortOption == sortMode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface,
|
||||
)
|
||||
: Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
color: albumSortOption == sortMode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface,
|
||||
)
|
||||
: const Icon(Icons.abc, color: Colors.transparent),
|
||||
onPressed: () => onMenuTapped(sortMode),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.fromLTRB(16, 16, 32, 16),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
sortMode.key.t(context: context),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: albumSortOption == sortMode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface.withAlpha(185),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
builder: (context, controller, child) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: albumSortIsReverse
|
||||
? const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
albumSortOption.key.t(context: context),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.onSurface.withAlpha(225),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchBar extends StatelessWidget {
|
||||
const _SearchBar({
|
||||
required this.searchController,
|
||||
required this.searchFocusNode,
|
||||
required this.onSearch,
|
||||
required this.filterMode,
|
||||
required this.onClearSearch,
|
||||
});
|
||||
|
||||
final TextEditingController searchController;
|
||||
final FocusNode searchFocusNode;
|
||||
final void Function(String, QuickFilterMode) onSearch;
|
||||
final QuickFilterMode filterMode;
|
||||
final VoidCallback onClearSearch;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: context.colorScheme.onSurface.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||
context.colorScheme.primary.withValues(alpha: 0.09),
|
||||
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
transform: const GradientRotation(0.5 * pi),
|
||||
),
|
||||
),
|
||||
child: SearchField(
|
||||
autofocus: false,
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
hintText: 'search_albums'.tr(),
|
||||
prefixIcon: const Icon(Icons.search_rounded),
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear_rounded),
|
||||
onPressed: onClearSearch,
|
||||
)
|
||||
: null,
|
||||
controller: searchController,
|
||||
onChanged: (_) => onSearch(searchController.text, filterMode),
|
||||
focusNode: searchFocusNode,
|
||||
onTapOutside: (_) => searchFocusNode.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickFilterButtonRow extends StatelessWidget {
|
||||
const _QuickFilterButtonRow({
|
||||
required this.filterMode,
|
||||
required this.onChangeFilter,
|
||||
required this.onSearch,
|
||||
required this.searchController,
|
||||
});
|
||||
|
||||
final QuickFilterMode filterMode;
|
||||
final void Function(QuickFilterMode) onChangeFilter;
|
||||
final void Function(String, QuickFilterMode) onSearch;
|
||||
final TextEditingController searchController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_QuickFilterButton(
|
||||
label: 'all'.tr(),
|
||||
isSelected: filterMode == QuickFilterMode.all,
|
||||
onTap: () {
|
||||
onChangeFilter(QuickFilterMode.all);
|
||||
onSearch(searchController.text, QuickFilterMode.all);
|
||||
},
|
||||
),
|
||||
_QuickFilterButton(
|
||||
label: 'shared_with_me'.tr(),
|
||||
isSelected: filterMode == QuickFilterMode.sharedWithMe,
|
||||
onTap: () {
|
||||
onChangeFilter(QuickFilterMode.sharedWithMe);
|
||||
onSearch(
|
||||
searchController.text,
|
||||
QuickFilterMode.sharedWithMe,
|
||||
);
|
||||
},
|
||||
),
|
||||
_QuickFilterButton(
|
||||
label: 'my_albums'.tr(),
|
||||
isSelected: filterMode == QuickFilterMode.myAlbums,
|
||||
onTap: () {
|
||||
onChangeFilter(QuickFilterMode.myAlbums);
|
||||
onSearch(
|
||||
searchController.text,
|
||||
QuickFilterMode.myAlbums,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickFilterButton extends StatelessWidget {
|
||||
const _QuickFilterButton({
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: onTap,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
isSelected ? context.colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.onSurface.withAlpha(25),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickSortAndViewMode extends StatelessWidget {
|
||||
const _QuickSortAndViewMode({
|
||||
required this.isGrid,
|
||||
required this.onToggleViewMode,
|
||||
});
|
||||
|
||||
final bool isGrid;
|
||||
final VoidCallback onToggleViewMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const _SortButton(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
|
||||
size: 24,
|
||||
),
|
||||
onPressed: onToggleViewMode,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumList extends ConsumerWidget {
|
||||
const _AlbumList({
|
||||
required this.albums,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final List<RemoteAlbum> albums;
|
||||
final String? userId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: Text('No albums found'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (_, index) {
|
||||
final album = albums[index];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: LargeLeadingTile(
|
||||
title: Text(
|
||||
album.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${'items_count'.t(
|
||||
context: context,
|
||||
args: {
|
||||
'count': album.assetCount,
|
||||
},
|
||||
)} • ${album.ownerId != userId ? 'shared_by_user'.t(
|
||||
context: context,
|
||||
args: {
|
||||
'user': album.ownerName,
|
||||
},
|
||||
) : 'owned'.t(context: context)}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
|
||||
context.router.push(
|
||||
RemoteAlbumRoute(album: album),
|
||||
);
|
||||
},
|
||||
leadingPadding: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
leading: album.thumbnailAssetId != null
|
||||
? ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(15),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Thumbnail(
|
||||
remoteId: album.thumbnailAssetId,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
border: Border.all(
|
||||
color: context.colorScheme.outline.withAlpha(50),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.photo_album_rounded,
|
||||
size: 24,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: albums.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumGrid extends StatelessWidget {
|
||||
const _AlbumGrid({
|
||||
required this.albums,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final List<RemoteAlbum> albums;
|
||||
final String? userId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: Text('No albums found'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final album = albums[index];
|
||||
return _GridAlbumCard(
|
||||
album: album,
|
||||
userId: userId,
|
||||
);
|
||||
},
|
||||
childCount: albums.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridAlbumCard extends ConsumerWidget {
|
||||
const _GridAlbumCard({
|
||||
required this.album,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final RemoteAlbum album;
|
||||
final String? userId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
|
||||
context.router.push(
|
||||
RemoteAlbumRoute(album: album),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: context.colorScheme.surfaceBright,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.onSurface.withAlpha(25),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(15),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: album.thumbnailAssetId != null
|
||||
? Thumbnail(
|
||||
remoteId: album.thumbnailAssetId,
|
||||
)
|
||||
: Container(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
child: const Icon(
|
||||
Icons.photo_album_rounded,
|
||||
size: 40,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
album.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${'items_count'.t(
|
||||
context: context,
|
||||
args: {
|
||||
'count': album.assetCount,
|
||||
},
|
||||
)} • ${album.ownerId != userId ? 'shared_by_user'.t(
|
||||
context: context,
|
||||
args: {
|
||||
'user': album.ownerName,
|
||||
},
|
||||
) : 'owned'.t(context: context)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
778
mobile/lib/presentation/widgets/album/album_selector.widget.dart
Normal file
778
mobile/lib/presentation/widgets/album/album_selector.widget.dart
Normal file
@ -0,0 +1,778 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
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/remote_album.utils.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
typedef AlbumSelectorCallback = void Function(RemoteAlbum album);
|
||||
|
||||
class AlbumSelector extends ConsumerStatefulWidget {
|
||||
final AlbumSelectorCallback onAlbumSelected;
|
||||
|
||||
const AlbumSelector({
|
||||
super.key,
|
||||
required this.onAlbumSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<AlbumSelector> createState() => _AlbumSelectorState();
|
||||
}
|
||||
|
||||
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
bool isGrid = false;
|
||||
final searchController = TextEditingController();
|
||||
QuickFilterMode filterMode = QuickFilterMode.all;
|
||||
final searchFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Load albums when component mounts
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||
});
|
||||
|
||||
searchController.addListener(() {
|
||||
onSearch(searchController.text, filterMode);
|
||||
});
|
||||
}
|
||||
|
||||
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode);
|
||||
}
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
await ref.read(remoteAlbumProvider.notifier).refresh();
|
||||
}
|
||||
|
||||
void toggleViewMode() {
|
||||
setState(() {
|
||||
isGrid = !isGrid;
|
||||
});
|
||||
}
|
||||
|
||||
void changeFilter(QuickFilterMode sortMode) {
|
||||
setState(() {
|
||||
filterMode = sortMode;
|
||||
});
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
setState(() {
|
||||
filterMode = QuickFilterMode.all;
|
||||
searchController.clear();
|
||||
ref.read(remoteAlbumProvider.notifier).clearSearch();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));
|
||||
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
|
||||
return MultiSliver(
|
||||
children: [
|
||||
_SearchBar(
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearch: onSearch,
|
||||
filterMode: filterMode,
|
||||
onClearSearch: clearSearch,
|
||||
),
|
||||
_QuickFilterButtonRow(
|
||||
filterMode: filterMode,
|
||||
onChangeFilter: changeFilter,
|
||||
onSearch: onSearch,
|
||||
searchController: searchController,
|
||||
),
|
||||
_QuickSortAndViewMode(
|
||||
isGrid: isGrid,
|
||||
onToggleViewMode: toggleViewMode,
|
||||
),
|
||||
isGrid
|
||||
? _AlbumGrid(
|
||||
albums: albums,
|
||||
userId: userId,
|
||||
onAlbumSelected: widget.onAlbumSelected,
|
||||
)
|
||||
: _AlbumList(
|
||||
albums: albums,
|
||||
userId: userId,
|
||||
onAlbumSelected: widget.onAlbumSelected,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortButton extends ConsumerStatefulWidget {
|
||||
const _SortButton();
|
||||
|
||||
@override
|
||||
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||
}
|
||||
|
||||
class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
RemoteAlbumSortMode albumSortOption = RemoteAlbumSortMode.lastModified;
|
||||
bool albumSortIsReverse = true;
|
||||
|
||||
void onMenuTapped(RemoteAlbumSortMode sortMode) {
|
||||
final selected = albumSortOption == sortMode;
|
||||
// Switch direction
|
||||
if (selected) {
|
||||
setState(() {
|
||||
albumSortIsReverse = !albumSortIsReverse;
|
||||
});
|
||||
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
|
||||
sortMode,
|
||||
isReverse: albumSortIsReverse,
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
albumSortOption = sortMode;
|
||||
});
|
||||
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
|
||||
sortMode,
|
||||
isReverse: albumSortIsReverse,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MenuAnchor(
|
||||
style: MenuStyle(
|
||||
elevation: const WidgetStatePropertyAll(1),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.all(4),
|
||||
),
|
||||
),
|
||||
consumeOutsideTap: true,
|
||||
menuChildren: RemoteAlbumSortMode.values
|
||||
.map(
|
||||
(sortMode) => MenuItemButton(
|
||||
leadingIcon: albumSortOption == sortMode
|
||||
? albumSortIsReverse
|
||||
? Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: albumSortOption == sortMode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface,
|
||||
)
|
||||
: Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
color: albumSortOption == sortMode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface,
|
||||
)
|
||||
: const Icon(Icons.abc, color: Colors.transparent),
|
||||
onPressed: () => onMenuTapped(sortMode),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.fromLTRB(16, 16, 32, 16),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
sortMode.key.t(context: context),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: albumSortOption == sortMode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface.withAlpha(185),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
builder: (context, controller, child) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: albumSortIsReverse
|
||||
? const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
albumSortOption.key.t(context: context),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colorScheme.onSurface.withAlpha(225),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchBar extends StatelessWidget {
|
||||
const _SearchBar({
|
||||
required this.searchController,
|
||||
required this.searchFocusNode,
|
||||
required this.onSearch,
|
||||
required this.filterMode,
|
||||
required this.onClearSearch,
|
||||
});
|
||||
|
||||
final TextEditingController searchController;
|
||||
final FocusNode searchFocusNode;
|
||||
final void Function(String, QuickFilterMode) onSearch;
|
||||
final QuickFilterMode filterMode;
|
||||
final VoidCallback onClearSearch;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: context.colorScheme.onSurface.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||
context.colorScheme.primary.withValues(alpha: 0.09),
|
||||
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
transform: const GradientRotation(0.5 * pi),
|
||||
),
|
||||
),
|
||||
child: SearchField(
|
||||
autofocus: false,
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
hintText: 'search_albums'.tr(),
|
||||
prefixIcon: const Icon(Icons.search_rounded),
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear_rounded),
|
||||
onPressed: onClearSearch,
|
||||
)
|
||||
: null,
|
||||
controller: searchController,
|
||||
onChanged: (_) => onSearch(searchController.text, filterMode),
|
||||
focusNode: searchFocusNode,
|
||||
onTapOutside: (_) => searchFocusNode.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickFilterButtonRow extends StatelessWidget {
|
||||
const _QuickFilterButtonRow({
|
||||
required this.filterMode,
|
||||
required this.onChangeFilter,
|
||||
required this.onSearch,
|
||||
required this.searchController,
|
||||
});
|
||||
|
||||
final QuickFilterMode filterMode;
|
||||
final void Function(QuickFilterMode) onChangeFilter;
|
||||
final void Function(String, QuickFilterMode) onSearch;
|
||||
final TextEditingController searchController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_QuickFilterButton(
|
||||
label: 'all'.tr(),
|
||||
isSelected: filterMode == QuickFilterMode.all,
|
||||
onTap: () {
|
||||
onChangeFilter(QuickFilterMode.all);
|
||||
onSearch(searchController.text, QuickFilterMode.all);
|
||||
},
|
||||
),
|
||||
_QuickFilterButton(
|
||||
label: 'shared_with_me'.tr(),
|
||||
isSelected: filterMode == QuickFilterMode.sharedWithMe,
|
||||
onTap: () {
|
||||
onChangeFilter(QuickFilterMode.sharedWithMe);
|
||||
onSearch(
|
||||
searchController.text,
|
||||
QuickFilterMode.sharedWithMe,
|
||||
);
|
||||
},
|
||||
),
|
||||
_QuickFilterButton(
|
||||
label: 'my_albums'.tr(),
|
||||
isSelected: filterMode == QuickFilterMode.myAlbums,
|
||||
onTap: () {
|
||||
onChangeFilter(QuickFilterMode.myAlbums);
|
||||
onSearch(
|
||||
searchController.text,
|
||||
QuickFilterMode.myAlbums,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickFilterButton extends StatelessWidget {
|
||||
const _QuickFilterButton({
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: onTap,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
isSelected ? context.colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.onSurface.withAlpha(25),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickSortAndViewMode extends StatelessWidget {
|
||||
const _QuickSortAndViewMode({
|
||||
required this.isGrid,
|
||||
required this.onToggleViewMode,
|
||||
});
|
||||
|
||||
final bool isGrid;
|
||||
final VoidCallback onToggleViewMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const _SortButton(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
|
||||
size: 24,
|
||||
),
|
||||
onPressed: onToggleViewMode,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumList extends ConsumerWidget {
|
||||
const _AlbumList({
|
||||
required this.albums,
|
||||
required this.userId,
|
||||
required this.onAlbumSelected,
|
||||
});
|
||||
|
||||
final List<RemoteAlbum> albums;
|
||||
final String? userId;
|
||||
final AlbumSelectorCallback onAlbumSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: Text('No albums found'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (_, index) {
|
||||
final album = albums[index];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: LargeLeadingTile(
|
||||
title: Text(
|
||||
album.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${'items_count'.t(
|
||||
context: context,
|
||||
args: {
|
||||
'count': album.assetCount,
|
||||
},
|
||||
)} • ${album.ownerId != userId ? 'shared_by_user'.t(
|
||||
context: context,
|
||||
args: {
|
||||
'user': album.ownerName,
|
||||
},
|
||||
) : 'owned'.t(context: context)}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
onTap: () => onAlbumSelected(album),
|
||||
leadingPadding: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
leading: album.thumbnailAssetId != null
|
||||
? ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(15),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Thumbnail(
|
||||
remoteId: album.thumbnailAssetId,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
border: Border.all(
|
||||
color: context.colorScheme.outline.withAlpha(50),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.photo_album_rounded,
|
||||
size: 24,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: albums.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumGrid extends StatelessWidget {
|
||||
const _AlbumGrid({
|
||||
required this.albums,
|
||||
required this.userId,
|
||||
required this.onAlbumSelected,
|
||||
});
|
||||
|
||||
final List<RemoteAlbum> albums;
|
||||
final String? userId;
|
||||
final AlbumSelectorCallback onAlbumSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (albums.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: Text('No albums found'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final album = albums[index];
|
||||
return _GridAlbumCard(
|
||||
album: album,
|
||||
userId: userId,
|
||||
onAlbumSelected: onAlbumSelected,
|
||||
);
|
||||
},
|
||||
childCount: albums.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridAlbumCard extends ConsumerWidget {
|
||||
const _GridAlbumCard({
|
||||
required this.album,
|
||||
required this.userId,
|
||||
required this.onAlbumSelected,
|
||||
});
|
||||
|
||||
final RemoteAlbum album;
|
||||
final String? userId;
|
||||
final AlbumSelectorCallback onAlbumSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return GestureDetector(
|
||||
onTap: () => onAlbumSelected(album),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: context.colorScheme.surfaceBright,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.onSurface.withAlpha(25),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(15),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: album.thumbnailAssetId != null
|
||||
? Thumbnail(
|
||||
remoteId: album.thumbnailAssetId,
|
||||
)
|
||||
: Container(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
child: const Icon(
|
||||
Icons.photo_album_rounded,
|
||||
size: 40,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
album.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${'items_count'.t(
|
||||
context: context,
|
||||
args: {
|
||||
'count': album.assetCount,
|
||||
},
|
||||
)} • ${album.ownerId != userId ? 'shared_by_user'.t(
|
||||
context: context,
|
||||
args: {
|
||||
'user': album.ownerName,
|
||||
},
|
||||
) : 'owned'.t(context: context)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddToAlbumHeader extends ConsumerWidget {
|
||||
const AddToAlbumHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Future<void> onCreateAlbum() async {
|
||||
final newAlbum = await ref.read(remoteAlbumProvider.notifier).createAlbum(
|
||||
title: "Untitled Album",
|
||||
assetIds: ref.read(multiSelectProvider).selectedAssets.map((e) => (e as RemoteAsset).id).toList(),
|
||||
);
|
||||
|
||||
if (newAlbum == null) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
toastType: ToastType.error,
|
||||
msg: 'errors.failed_to_create_album'.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context.pushRoute(RemoteAlbumRoute(album: newAlbum));
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"add_to_album",
|
||||
style: context.textTheme.titleSmall,
|
||||
).tr(),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
), // remove internal padding
|
||||
minimumSize: const Size(0, 0), // allow shrinking
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // remove extra height
|
||||
),
|
||||
onPressed: onCreateAlbum,
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
label: Text(
|
||||
"common_create_new_album",
|
||||
style: TextStyle(
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_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';
|
||||
@ -14,9 +17,12 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class GeneralBottomSheet extends ConsumerWidget {
|
||||
const GeneralBottomSheet({super.key});
|
||||
@ -28,9 +34,39 @@ class GeneralBottomSheet extends ConsumerWidget {
|
||||
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
||||
);
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
if (selectedAssets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets(
|
||||
album.id,
|
||||
selectedAssets.map((e) => (e as RemoteAsset).id).toList(),
|
||||
);
|
||||
|
||||
if (addedCount != selectedAssets.length) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.tr(
|
||||
namedArgs: {"album": album.name},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(
|
||||
namedArgs: {"album": album.name},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.4,
|
||||
initialChildSize: 0.45,
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
@ -59,6 +95,12 @@ class GeneralBottomSheet extends ConsumerWidget {
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
slivers: [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(
|
||||
onAlbumSelected: addAssetsToAlbum,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1689,6 +1689,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
sliver_tools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sliver_tools
|
||||
sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.12"
|
||||
socket_io_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -63,6 +63,7 @@ dependencies:
|
||||
scrollable_positioned_list: ^0.3.8
|
||||
share_handler: ^0.0.22
|
||||
share_plus: ^10.1.4
|
||||
sliver_tools: ^0.2.12
|
||||
socket_io_client: ^2.0.3+1
|
||||
stream_transform: ^2.1.1
|
||||
thumbhash: 0.1.0+1
|
||||
|
Loading…
x
Reference in New Issue
Block a user