Merge 39017922a9555b938e6d6d12396ea17e7b20908f into 63437529e1224f5e7879ce567b1a4502fb97573b

This commit is contained in:
Alex 2024-10-01 23:12:57 +02:00 committed by GitHub
commit 09a2dcca7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1618 additions and 91 deletions

View File

@ -1,4 +1,15 @@
{ {
"collections": "Collections",
"on_this_device": "On this device",
"add_a_name": "Add a name",
"places": "Places",
"albums": "Albums",
"people": "People",
"shared_links": "Shared links",
"trash": "Trash",
"archived": "Archived",
"favorites": "Favorites",
"search_albums": "Search albums",
"action_common_back": "Back", "action_common_back": "Back",
"action_common_cancel": "Cancel", "action_common_cancel": "Cancel",
"action_common_clear": "Clear", "action_common_clear": "Clear",

View File

@ -0,0 +1,405 @@
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:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/album/albumv2.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
enum QuickFilterMode {
all,
sharedWithMe,
myAlbums,
}
@RoutePage()
class AlbumsCollectionPage extends HookConsumerWidget {
const AlbumsCollectionPage({super.key, this.showImmichAppbar = false});
final bool showImmichAppbar;
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums =
ref.watch(albumProviderV2).where((album) => album.isRemote).toList();
final albumSortOption = ref.watch(albumSortByOptionsProvider);
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
final sorted = albumSortOption.sortFn(albums, albumSortIsReverse);
final isGrid = useState(false);
final searchController = useTextEditingController();
final debounceTimer = useRef<Timer?>(null);
final filterMode = useState(QuickFilterMode.all);
final userId = ref.watch(currentUserProvider)?.id;
toggleViewMode() {
isGrid.value = !isGrid.value;
}
onSearch(String value) {
debounceTimer.value?.cancel();
debounceTimer.value = Timer(const Duration(milliseconds: 300), () {
filterMode.value = QuickFilterMode.all;
ref.read(albumProviderV2.notifier).searchAlbums(value);
});
}
changeFilter(QuickFilterMode mode) {
filterMode.value = mode;
ref.read(albumProviderV2.notifier).filterAlbums(mode);
}
useEffect(
() {
searchController.addListener(() {
onSearch(searchController.text);
});
return () {
searchController.removeListener(() {
onSearch(searchController.text);
});
debounceTimer.value?.cancel();
};
},
[],
);
return Scaffold(
appBar: AppBar(
title: showImmichAppbar
? null
: Text(
"${'albums'.tr()} ${albums.length}",
),
bottom: showImmichAppbar
? const PreferredSize(
preferredSize: Size.fromHeight(0),
child: ImmichAppBar(),
)
: null,
),
body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(18.0),
children: [
SearchBar(
backgroundColor: WidgetStatePropertyAll(
context.colorScheme.surfaceContainer,
),
autoFocus: false,
hintText: "search_albums".tr(),
onChanged: onSearch,
elevation: const WidgetStatePropertyAll(0.25),
controller: searchController,
leading: const Icon(Icons.search_rounded),
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 16),
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(10),
width: 0.5,
),
),
),
),
const SizedBox(height: 16),
Wrap(
spacing: 4,
runSpacing: 4,
children: [
QuickFilterButton(
label: 'All',
isSelected: filterMode.value == QuickFilterMode.all,
onTap: () => changeFilter(QuickFilterMode.all),
),
QuickFilterButton(
label: 'Shared with me',
isSelected: filterMode.value == QuickFilterMode.sharedWithMe,
onTap: () => changeFilter(QuickFilterMode.sharedWithMe),
),
QuickFilterButton(
label: 'My albums',
isSelected: filterMode.value == QuickFilterMode.myAlbums,
onTap: () => changeFilter(QuickFilterMode.myAlbums),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SortButton(),
IconButton(
icon: Icon(
isGrid.value
? Icons.view_list_rounded
: Icons.grid_view_outlined,
size: 24,
),
onPressed: toggleViewMode,
),
],
),
const SizedBox(height: 5),
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: isGrid.value
? GridView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: .7,
),
itemBuilder: (context, index) {
return AlbumThumbnailCard(
album: sorted[index],
onTap: () => context.pushRoute(
AlbumViewerRoute(albumId: sorted[index].id),
),
showOwner: true,
);
},
itemCount: sorted.length,
)
: ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: sorted.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: LargeLeadingTile(
title: Text(
sorted[index].name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: sorted[index].ownerId == userId
? Text(
'${sorted[index].assetCount} items',
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(
color:
context.colorScheme.onSurfaceSecondary,
),
)
: sorted[index].ownerName != null
? Text(
'${sorted[index].assetCount} items • ${'album_thumbnail_shared_by'.tr(
args: [
sorted[index].ownerName!,
],
)}',
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium
?.copyWith(
color: context
.colorScheme.onSurfaceSecondary,
),
)
: null,
onTap: () => context.pushRoute(
AlbumViewerRoute(albumId: sorted[index].id),
),
leadingPadding: const EdgeInsets.only(
right: 16,
),
leading: ClipRRect(
borderRadius:
const BorderRadius.all(Radius.circular(15)),
child: ImmichThumbnail(
asset: sorted[index].thumbnail.value,
width: 80,
height: 80,
),
),
// minVerticalPadding: 1,
),
);
},
),
),
],
),
);
}
}
class QuickFilterButton extends StatelessWidget {
const QuickFilterButton({
super.key,
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.icon(
onPressed: onTap,
icon: isSelected
? Icon(
Icons.check_rounded,
color: context.colorScheme.onPrimary,
size: 18,
)
: const SizedBox.shrink(),
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
isSelected ? context.colorScheme.primary : Colors.transparent,
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(25),
width: 1,
),
),
),
),
label: Text(
label,
style: TextStyle(
color: isSelected
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
fontSize: 14,
),
),
);
}
}
class SortButton extends ConsumerWidget {
const SortButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albumSortOption = ref.watch(albumSortByOptionsProvider);
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
return MenuAnchor(
style: MenuStyle(
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
consumeOutsideTap: true,
menuChildren: AlbumSortMode.values
.map(
(mode) => MenuItemButton(
leadingIcon: albumSortOption == mode
? albumSortIsReverse
? Icon(
Icons.keyboard_arrow_down,
color: albumSortOption == mode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: Icon(
Icons.keyboard_arrow_up_rounded,
color: albumSortOption == mode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: const Icon(Icons.abc, color: Colors.transparent),
onPressed: () {
final selected = albumSortOption == mode;
// Switch direction
if (selected) {
ref
.read(albumSortOrderProvider.notifier)
.changeSortDirection(!albumSortIsReverse);
} else {
ref
.read(albumSortByOptionsProvider.notifier)
.changeSortMode(mode);
}
},
style: ButtonStyle(
padding: WidgetStateProperty.all(const EdgeInsets.all(8)),
backgroundColor: WidgetStateProperty.all(
albumSortOption == mode
? context.colorScheme.primary
: Colors.transparent,
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
child: Text(
mode.label.tr(),
style: context.textTheme.bodyMedium?.copyWith(
color: albumSortOption == mode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
),
),
),
)
.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: Transform.rotate(
angle: 90 * pi / 180,
child: Icon(
Icons.compare_arrows_rounded,
size: 18,
color: context.colorScheme.onSurface.withAlpha(225),
),
),
),
Text(
albumSortOption.label.tr(),
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: context.colorScheme.onSurface.withAlpha(225),
),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,55 @@
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/albumv2.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
@RoutePage()
class LocalAlbumsCollectionPage extends HookConsumerWidget {
const LocalAlbumsCollectionPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(localAlbumsProvider);
return Scaffold(
appBar: AppBar(
title: Text('on_this_device'.tr()),
),
body: ListView.builder(
padding: const EdgeInsets.all(18.0),
itemCount: albums.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: ListTile(
contentPadding: const EdgeInsets.all(0),
dense: false,
visualDensity: VisualDensity.comfortable,
leading: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(15)),
child: ImmichThumbnail(
asset: albums[index].thumbnail.value,
width: 60,
height: 90,
),
),
minVerticalPadding: 1,
title: Text(
albums[index].name,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: Text('${albums[index].assetCount} items'),
onTap: () => context
.pushRoute(AlbumViewerRoute(albumId: albums[index].id)),
),
);
},
),
);
}
}

View File

@ -0,0 +1,310 @@
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/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/albumv2.provider.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
import 'package:immich_mobile/widgets/common/share_partner_button.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@RoutePage()
class CollectionsPage extends ConsumerWidget {
const CollectionsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
return Scaffold(
appBar: const ImmichAppBar(
showUploadButton: false,
actions: [CreateNewButton(), SharePartnerButton()],
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16),
child: ListView(
shrinkWrap: true,
children: [
Row(
children: [
ActionButton(
onPressed: () => context.pushRoute(const FavoritesRoute()),
icon: Icons.favorite_outline_rounded,
label: 'favorites'.tr(),
),
const SizedBox(width: 8),
ActionButton(
onPressed: () => context.pushRoute(const ArchiveRoute()),
icon: Icons.archive_outlined,
label: 'archived'.tr(),
),
],
),
const SizedBox(height: 8),
Row(
children: [
ActionButton(
onPressed: () => context.pushRoute(const SharedLinkRoute()),
icon: Icons.link_outlined,
label: 'shared_links'.tr(),
),
const SizedBox(width: 8),
trashEnabled
? ActionButton(
onPressed: () => context.pushRoute(const TrashRoute()),
icon: Icons.delete_outline_rounded,
label: 'trash'.tr(),
)
: const SizedBox.shrink(),
],
),
const SizedBox(height: 24),
const Wrap(
spacing: 8,
runSpacing: 16,
children: [
PeopleCollectionCard(),
AlbumsCollectionCard(),
AlbumsCollectionCard(isLocal: true),
PlacesCollectionCard(),
],
),
],
),
),
);
}
}
class PeopleCollectionCard extends ConsumerWidget {
const PeopleCollectionCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final people = ref.watch(getAllPeopleProvider);
final size = MediaQuery.of(context).size.width * 0.5 - 20;
return GestureDetector(
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: size,
width: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: context.colorScheme.secondaryContainer.withAlpha(100),
),
child: people.widgetWhen(
onData: (people) {
return GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
physics: const NeverScrollableScrollPhysics(),
children: people.take(4).map((person) {
return CircleAvatar(
backgroundImage: NetworkImage(
getFaceThumbnailUrl(person.id),
headers: ApiService.getRequestHeaders(),
),
);
}).toList(),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'people'.tr(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}
class AlbumsCollectionCard extends HookConsumerWidget {
final bool isLocal;
const AlbumsCollectionCard({super.key, this.isLocal = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = isLocal
? ref.watch(localAlbumsProvider)
: ref.watch(remoteAlbumsProvider);
final size = MediaQuery.of(context).size.width * 0.5 - 20;
return GestureDetector(
onTap: () => isLocal
? context.pushRoute(
const LocalAlbumsCollectionRoute(),
)
: context.pushRoute(AlbumsCollectionRoute()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: size,
width: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: context.colorScheme.secondaryContainer.withAlpha(100),
),
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
physics: const NeverScrollableScrollPhysics(),
children: albums.take(4).map((album) {
return AlbumThumbnailCard(
album: album,
showTitle: false,
);
}).toList(),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
isLocal ? 'on_this_device'.tr() : 'albums'.tr(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}
class PlacesCollectionCard extends StatelessWidget {
const PlacesCollectionCard({super.key});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size.width * 0.5 - 20;
return GestureDetector(
onTap: () => context.pushRoute(const PlacesCollectionRoute()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: size,
width: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: context.colorScheme.secondaryContainer.withAlpha(100),
),
child: IgnorePointer(
child: MapThumbnail(
zoom: 8,
centre: const LatLng(
21.44950,
-157.91959,
),
showAttribution: false,
themeMode:
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'places'.tr(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}
class ActionButton extends StatelessWidget {
final VoidCallback onPressed;
final IconData icon;
final String label;
const ActionButton({
super.key,
required this.onPressed,
required this.icon,
required this.label,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: FilledButton.icon(
onPressed: onPressed,
label: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
label,
style: TextStyle(
color: context.colorScheme.onSurface,
fontSize: 14,
),
),
),
style: FilledButton.styleFrom(
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
backgroundColor: context.colorScheme.surfaceContainerLow,
alignment: Alignment.centerLeft,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(25)),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
),
),
icon: Icon(
icon,
color: context.primaryColor,
),
),
);
}
}
class CreateNewButton extends StatelessWidget {
const CreateNewButton({super.key});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {},
borderRadius: const BorderRadius.all(Radius.circular(25)),
child: const Icon(
Icons.add,
size: 32,
),
);
}
}

View File

@ -0,0 +1,104 @@
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
@RoutePage()
class PeopleCollectionPage extends HookConsumerWidget {
const PeopleCollectionPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final people = ref.watch(getAllPeopleProvider);
final headers = ApiService.getRequestHeaders();
showNameEditModel(
String personId,
String personName,
) {
return showDialog(
context: context,
builder: (BuildContext context) {
return PersonNameEditForm(personId: personId, personName: personName);
},
);
}
return Scaffold(
appBar: AppBar(
title: Text('people'.tr()),
),
body: people.when(
data: (people) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 0.85,
),
padding: const EdgeInsets.symmetric(vertical: 32),
itemCount: people.length,
itemBuilder: (context, index) {
final person = people[index];
return Column(
children: [
GestureDetector(
onTap: () {
context.pushRoute(
PersonResultRoute(
personId: person.id,
personName: person.name,
),
);
},
child: Material(
shape: const CircleBorder(side: BorderSide.none),
elevation: 3,
child: CircleAvatar(
maxRadius: 96 / 2,
backgroundImage: NetworkImage(
getFaceThumbnailUrl(person.id),
headers: headers,
),
),
),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () => showNameEditModel(person.id, person.name),
child: person.name.isEmpty
? Text(
'add_a_name'.tr(),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
color: context.colorScheme.primary,
),
)
: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
person.name,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
),
],
);
},
);
},
error: (error, stack) => const Text("error"),
loading: () => const CircularProgressIndicator(),
),
);
}
}

View File

@ -0,0 +1,125 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@RoutePage()
class PlacesCollectionPage extends HookConsumerWidget {
const PlacesCollectionPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final places = ref.watch(getAllPlacesProvider);
return Scaffold(
appBar: AppBar(
title: Text('places'.tr()),
),
body: ListView(
shrinkWrap: true,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
height: 200,
width: context.width,
child: MapThumbnail(
onTap: (_, __) => context.pushRoute(const MapRoute()),
zoom: 8,
centre: const LatLng(
21.44950,
-157.91959,
),
showAttribution: false,
themeMode:
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
),
),
),
places.when(
data: (places) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: places.length,
itemBuilder: (context, index) {
final place = places[index];
return PlaceTile(id: place.id, name: place.label);
},
);
},
error: (error, stask) => const Text('Error getting places'),
loading: () => const CircularProgressIndicator(),
),
],
),
);
}
}
class PlaceTile extends StatelessWidget {
const PlaceTile({super.key, required this.id, required this.name});
final String id;
final String name;
@override
Widget build(BuildContext context) {
final thumbnailUrl =
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail';
void navigateToPlace() {
context.pushRoute(
SearchInputRoute(
prefilter: SearchFilter(
people: {},
location: SearchLocationFilter(
city: name,
),
camera: SearchCameraFilter(),
date: SearchDateFilter(),
display: SearchDisplayFilters(
isNotInAlbum: false,
isArchive: false,
isFavorite: false,
),
mediaType: AssetType.other,
),
),
);
}
return LargeLeadingTile(
onTap: () => navigateToPlace(),
title: Text(
name,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: CachedNetworkImage(
width: 80,
height: 80,
fit: BoxFit.cover,
imageUrl: thumbnailUrl,
httpHeaders: ApiService.getRequestHeaders(),
errorWidget: (context, url, error) =>
const Icon(Icons.image_not_supported_outlined),
),
),
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class LargeLeadingTile extends StatelessWidget {
const LargeLeadingTile({
super.key,
required this.leading,
required this.onTap,
required this.title,
this.subtitle,
this.leadingPadding = const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16.0,
),
});
final Widget leading;
final VoidCallback onTap;
final Widget title;
final Widget? subtitle;
final EdgeInsetsGeometry leadingPadding;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: leadingPadding,
child: leading,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.6,
child: title,
),
subtitle ?? const SizedBox.shrink(),
],
),
],
),
);
}
}

View File

@ -76,24 +76,36 @@ class TabControllerPage extends HookConsumerWidget {
selectedIcon: const Icon(Icons.photo_library), selectedIcon: const Icon(Icons.photo_library),
label: const Text('tab_controller_nav_photos').tr(), label: const Text('tab_controller_nav_photos').tr(),
), ),
NavigationRailDestination(
padding: const EdgeInsets.all(4),
icon: const Icon(Icons.photo_album_outlined),
selectedIcon: const Icon(Icons.photo_album),
label: const Text('albums').tr(),
),
NavigationRailDestination(
padding: const EdgeInsets.all(4),
icon: const Icon(Icons.space_dashboard_outlined),
selectedIcon: const Icon(Icons.space_dashboard_rounded),
label: const Text('collections').tr(),
),
NavigationRailDestination( NavigationRailDestination(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
icon: const Icon(Icons.search_rounded), icon: const Icon(Icons.search_rounded),
selectedIcon: const Icon(Icons.search), selectedIcon: const Icon(Icons.search),
label: const Text('tab_controller_nav_search').tr(), label: const Text('tab_controller_nav_search').tr(),
), ),
NavigationRailDestination( // NavigationRailDestination(
padding: const EdgeInsets.all(4), // padding: const EdgeInsets.all(4),
icon: const Icon(Icons.share_rounded), // icon: const Icon(Icons.share_rounded),
selectedIcon: const Icon(Icons.share), // selectedIcon: const Icon(Icons.share),
label: const Text('tab_controller_nav_sharing').tr(), // label: const Text('tab_controller_nav_sharing').tr(),
), // ),
NavigationRailDestination( // NavigationRailDestination(
padding: const EdgeInsets.all(4), // padding: const EdgeInsets.all(4),
icon: const Icon(Icons.photo_album_outlined), // icon: const Icon(Icons.photo_album_outlined),
selectedIcon: const Icon(Icons.photo_album), // selectedIcon: const Icon(Icons.photo_album),
label: const Text('tab_controller_nav_library').tr(), // label: const Text('tab_controller_nav_library').tr(),
), // ),
], ],
); );
} }
@ -125,27 +137,7 @@ class TabControllerPage extends HookConsumerWidget {
), ),
), ),
NavigationDestination( NavigationDestination(
label: 'tab_controller_nav_search'.tr(), label: 'albums'.tr(),
icon: const Icon(
Icons.search_rounded,
),
selectedIcon: Icon(
Icons.search,
color: context.primaryColor,
),
),
NavigationDestination(
label: 'tab_controller_nav_sharing'.tr(),
icon: const Icon(
Icons.group_outlined,
),
selectedIcon: Icon(
Icons.group,
color: context.primaryColor,
),
),
NavigationDestination(
label: 'tab_controller_nav_library'.tr(),
icon: const Icon( icon: const Icon(
Icons.photo_album_outlined, Icons.photo_album_outlined,
), ),
@ -156,17 +148,63 @@ class TabControllerPage extends HookConsumerWidget {
), ),
), ),
), ),
NavigationDestination(
label: 'collections'.tr(),
icon: const Icon(
Icons.space_dashboard_outlined,
),
selectedIcon: buildIcon(
Icon(
Icons.space_dashboard_rounded,
color: context.primaryColor,
),
),
),
NavigationDestination(
label: 'tab_controller_nav_search'.tr(),
icon: const Icon(
Icons.search_rounded,
),
selectedIcon: Icon(
Icons.search,
color: context.primaryColor,
),
),
// NavigationDestination(
// label: 'tab_controller_nav_sharing'.tr(),
// icon: const Icon(
// Icons.group_outlined,
// ),
// selectedIcon: Icon(
// Icons.group,
// color: context.primaryColor,
// ),
// ),
// NavigationDestination(
// label: 'tab_controller_nav_library'.tr(),
// icon: const Icon(
// Icons.photo_album_outlined,
// ),
// selectedIcon: buildIcon(
// Icon(
// Icons.photo_album_rounded,
// color: context.primaryColor,
// ),
// ),
// ),
], ],
); );
} }
final multiselectEnabled = ref.watch(multiselectProvider); final multiselectEnabled = ref.watch(multiselectProvider);
return AutoTabsRouter( return AutoTabsRouter(
routes: const [ routes: [
PhotosRoute(), const PhotosRoute(),
SearchRoute(), AlbumsCollectionRoute(showImmichAppbar: true),
SharingRoute(), const CollectionsRoute(),
LibraryRoute(), // SharingRoute(),
// LibraryRoute(),
const SearchRoute(),
], ],
duration: const Duration(milliseconds: 600), duration: const Duration(milliseconds: 600),
transitionBuilder: (context, child, animation) => FadeTransition( transitionBuilder: (context, child, animation) => FadeTransition(

View File

@ -184,7 +184,7 @@ class LibraryPage extends HookConsumerWidget {
final sorted = albumSortOption.sortFn(remote, albumSortIsReverse); final sorted = albumSortOption.sortFn(remote, albumSortIsReverse);
final local = albums.where((a) => a.isLocal).toList(); final local = albums.where((a) => a.isLocal).toList();
Widget? shareTrashButton() { Widget shareTrashButton() {
return trashEnabled return trashEnabled
? InkWell( ? InkWell(
onTap: () => context.pushRoute(const TrashRoute()), onTap: () => context.pushRoute(const TrashRoute()),
@ -195,12 +195,12 @@ class LibraryPage extends HookConsumerWidget {
semanticLabel: 'profile_drawer_trash'.tr(), semanticLabel: 'profile_drawer_trash'.tr(),
), ),
) )
: null; : const SizedBox.shrink();
} }
return Scaffold( return Scaffold(
appBar: ImmichAppBar( appBar: ImmichAppBar(
action: shareTrashButton(), actions: [shareTrashButton()],
), ),
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [

View File

@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget {
Text( Text(
name.value, name.value,
style: context.textTheme.titleLarge, style: context.textTheme.titleLarge,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
@ -125,9 +126,11 @@ class PersonResultPage extends HookConsumerWidget {
headers: ApiService.getRequestHeaders(), headers: ApiService.getRequestHeaders(),
), ),
), ),
Padding( Expanded(
padding: const EdgeInsets.only(left: 16.0), child: Padding(
child: buildTitleBlock(), padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: buildTitleBlock(),
),
), ),
], ],
), ),

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dar
import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/providers/partner.provider.dart'; import 'package:immich_mobile/providers/partner.provider.dart';
import 'package:immich_mobile/widgets/common/share_partner_button.dart';
import 'package:immich_mobile/widgets/partner/partner_list.dart'; import 'package:immich_mobile/widgets/partner/partner_list.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
@ -215,25 +216,13 @@ class SharingPage extends HookConsumerWidget {
); );
} }
Widget sharePartnerButton() {
return InkWell(
onTap: () => context.pushRoute(const PartnerRoute()),
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Icon(
Icons.swap_horizontal_circle_rounded,
size: 25,
semanticLabel: 'partner_page_title'.tr(),
),
);
}
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
}, },
child: Scaffold( child: Scaffold(
appBar: ImmichAppBar( appBar: const ImmichAppBar(
action: sharePartnerButton(), actions: [SharePartnerButton()],
), ),
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [

View File

@ -12,7 +12,7 @@ import 'package:immich_mobile/utils/renderlist_generator.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
class AlbumNotifier extends StateNotifier<List<Album>> { class AlbumNotifier extends StateNotifier<List<Album>> {
AlbumNotifier(this._albumService, Isar db) : super([]) { AlbumNotifier(this._albumService, this.db) : super([]) {
final query = db.albums final query = db.albums
.filter() .filter()
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)); .owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
@ -25,6 +25,7 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
} }
final AlbumService _albumService; final AlbumService _albumService;
final Isar db;
late final StreamSubscription<List<Album>> _streamSub; late final StreamSubscription<List<Album>> _streamSub;
Future<void> getAllAlbums() => Future.wait([ Future<void> getAllAlbums() => Future.wait([
@ -64,6 +65,16 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
_streamSub.cancel(); _streamSub.cancel();
super.dispose(); super.dispose();
} }
void searchAlbums(String value) async {
final query = db.albums
.filter()
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId))
.nameContains(value, caseSensitive: false);
final albums = await query.findAll();
state = albums;
}
} }
final albumProvider = final albumProvider =

View File

@ -0,0 +1,178 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/pages/collections/albums/albums_collection.page.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:isar/isar.dart';
class AlbumNotifierV2 extends StateNotifier<List<Album>> {
AlbumNotifierV2(this._albumService, this.db) : super([]) {
final query = db.albums.where();
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub = query.watch().listen((data) => state = data);
}
final AlbumService _albumService;
final Isar db;
late final StreamSubscription<List<Album>> _streamSub;
Future<void> refreshAlbums() async {
// Future.wait([
// _albumService.refreshDeviceAlbums(),
// _albumService.refreshAllRemoteAlbums(),
// ]);
await _albumService.refreshDeviceAlbums();
await _albumService.refreshRemoteAlbums(isShared: false);
await _albumService.refreshRemoteAlbums(isShared: true);
}
Future<void> getDeviceAlbums() {
return _albumService.refreshDeviceAlbums();
}
Future<bool> deleteAlbum(Album album) {
return _albumService.deleteAlbum(album);
}
Future<Album?> createAlbum(
String albumTitle,
Set<Asset> assets,
) {
return _albumService.createAlbum(albumTitle, assets, []);
}
Future<Album?> getAlbumByName(String albumName, {bool remoteOnly = false}) {
return _albumService.getAlbumByName(albumName, remoteOnly);
}
/// Create an album on the server with the same name as the selected album for backup
/// First this will check if the album already exists on the server with name
/// If it does not exist, it will create the album on the server
Future<void> createSyncAlbum(
String albumName,
) async {
final album = await getAlbumByName(albumName, remoteOnly: true);
if (album != null) {
return;
}
await createAlbum(albumName, {});
}
void searchAlbums(String value) async {
final query = db.albums
.filter()
.remoteIdIsNotNull()
.nameContains(value, caseSensitive: false);
final albums = await query.findAll();
state = albums;
}
void filterAlbums(QuickFilterMode mode) async {
switch (mode) {
case QuickFilterMode.all:
state = await db.albums.filter().remoteIdIsNotNull().findAll();
return;
case QuickFilterMode.sharedWithMe:
state = await db.albums
.filter()
.remoteIdIsNotNull()
.owner(
(q) =>
q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
)
.findAll();
return;
case QuickFilterMode.myAlbums:
state = await db.albums
.filter()
.remoteIdIsNotNull()
.owner(
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
)
.findAll();
return;
}
}
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
final albumProviderV2 =
StateNotifierProvider.autoDispose<AlbumNotifierV2, List<Album>>((ref) {
return AlbumNotifierV2(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),
);
});
class RemoteAlbumsNotifier extends StateNotifier<List<Album>> {
RemoteAlbumsNotifier(this.db) : super([]) {
final query = db.albums.filter().remoteIdIsNotNull();
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub = query.watch().listen((data) => state = data);
}
final Isar db;
late final StreamSubscription<List<Album>> _streamSub;
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
LocalAlbumsNotifier(this.db) : super([]) {
final query = db.albums.filter().not().remoteIdIsNotNull();
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub = query.watch().listen((data) => state = data);
}
final Isar db;
late final StreamSubscription<List<Album>> _streamSub;
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
final localAlbumsProvider =
StateNotifierProvider.autoDispose<LocalAlbumsNotifier, List<Album>>((ref) {
return LocalAlbumsNotifier(ref.watch(dbProvider));
});
final remoteAlbumsProvider =
StateNotifierProvider.autoDispose<RemoteAlbumsNotifier, List<Album>>((ref) {
return RemoteAlbumsNotifier(ref.watch(dbProvider));
});

View File

@ -63,6 +63,8 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
_ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); _ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
case TabEnum.library: case TabEnum.library:
_ref.read(albumProvider.notifier).getAllAlbums(); _ref.read(albumProvider.notifier).getAllAlbums();
case TabEnum.collections:
// nothing to do
} }
} }

View File

@ -1,11 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
enum TabEnum { enum TabEnum { home, search, sharing, library, collections }
home,
search,
sharing,
library,
}
/// Provides the currently active tab /// Provides the currently active tab
final tabProvider = StateProvider<TabEnum>( final tabProvider = StateProvider<TabEnum>(

View File

@ -13,6 +13,11 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/backup_options.page.dart';
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
import 'package:immich_mobile/pages/collections/albums/albums_collection.page.dart';
import 'package:immich_mobile/pages/collections/albums/local_albums_collection.page.dart';
import 'package:immich_mobile/pages/collections/people/people_collection.page.dart';
import 'package:immich_mobile/pages/collections/places/places_collection.part.dart';
import 'package:immich_mobile/pages/collections/collections.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/common/album_additional_shared_user_selection.page.dart';
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart'; import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
@ -113,6 +118,14 @@ class AppRouter extends RootStackRouter {
page: LibraryRoute.page, page: LibraryRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
), ),
AutoRoute(
page: CollectionsRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: AlbumsCollectionRoute.page,
guards: [_authGuard, _duplicateGuard],
),
], ],
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
@ -135,7 +148,11 @@ class AppRouter extends RootStackRouter {
), ),
AutoRoute(page: EditImageRoute.page), AutoRoute(page: EditImageRoute.page),
AutoRoute(page: CropImageRoute.page), AutoRoute(page: CropImageRoute.page),
AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]), CustomRoute(
page: FavoritesRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute( AutoRoute(
page: AllMotionPhotosRoute.page, page: AllMotionPhotosRoute.page,
@ -181,8 +198,16 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]), CustomRoute(
AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]), page: ArchiveRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: PartnerRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
AutoRoute( AutoRoute(
page: PartnerDetailRoute.page, page: PartnerDetailRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
@ -198,10 +223,15 @@ class AppRouter extends RootStackRouter {
page: AlbumOptionsRoute.page, page: AlbumOptionsRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
), ),
AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), CustomRoute(
AutoRoute( page: TrashRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: SharedLinkRoute.page, page: SharedLinkRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
), ),
AutoRoute( AutoRoute(
page: SharedLinkEditRoute.page, page: SharedLinkEditRoute.page,
@ -230,6 +260,26 @@ class AppRouter extends RootStackRouter {
page: HeaderSettingsRoute.page, page: HeaderSettingsRoute.page,
guards: [_duplicateGuard], guards: [_duplicateGuard],
), ),
CustomRoute(
page: PeopleCollectionRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: AlbumsCollectionRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: LocalAlbumsCollectionRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: PlacesCollectionRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
]; ];
} }

View File

@ -319,6 +319,53 @@ class AlbumViewerRouteArgs {
} }
} }
/// generated route for
/// [AlbumsCollectionPage]
class AlbumsCollectionRoute extends PageRouteInfo<AlbumsCollectionRouteArgs> {
AlbumsCollectionRoute({
Key? key,
bool showImmichAppbar = false,
List<PageRouteInfo>? children,
}) : super(
AlbumsCollectionRoute.name,
args: AlbumsCollectionRouteArgs(
key: key,
showImmichAppbar: showImmichAppbar,
),
initialChildren: children,
);
static const String name = 'AlbumsCollectionRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<AlbumsCollectionRouteArgs>(
orElse: () => const AlbumsCollectionRouteArgs());
return AlbumsCollectionPage(
key: args.key,
showImmichAppbar: args.showImmichAppbar,
);
},
);
}
class AlbumsCollectionRouteArgs {
const AlbumsCollectionRouteArgs({
this.key,
this.showImmichAppbar = false,
});
final Key? key;
final bool showImmichAppbar;
@override
String toString() {
return 'AlbumsCollectionRouteArgs{key: $key, showImmichAppbar: $showImmichAppbar}';
}
}
/// generated route for /// generated route for
/// [AllMotionPhotosPage] /// [AllMotionPhotosPage]
class AllMotionPhotosRoute extends PageRouteInfo<void> { class AllMotionPhotosRoute extends PageRouteInfo<void> {
@ -555,6 +602,25 @@ class ChangePasswordRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [CollectionsPage]
class CollectionsRoute extends PageRouteInfo<void> {
const CollectionsRoute({List<PageRouteInfo>? children})
: super(
CollectionsRoute.name,
initialChildren: children,
);
static const String name = 'CollectionsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const CollectionsPage();
},
);
}
/// generated route for /// generated route for
/// [CreateAlbumPage] /// [CreateAlbumPage]
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> { class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
@ -857,6 +923,25 @@ class LibraryRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [LocalAlbumsCollectionPage]
class LocalAlbumsCollectionRoute extends PageRouteInfo<void> {
const LocalAlbumsCollectionRoute({List<PageRouteInfo>? children})
: super(
LocalAlbumsCollectionRoute.name,
initialChildren: children,
);
static const String name = 'LocalAlbumsCollectionRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const LocalAlbumsCollectionPage();
},
);
}
/// generated route for /// generated route for
/// [LoginPage] /// [LoginPage]
class LoginRoute extends PageRouteInfo<void> { class LoginRoute extends PageRouteInfo<void> {
@ -1059,6 +1144,25 @@ class PartnerRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [PeopleCollectionPage]
class PeopleCollectionRoute extends PageRouteInfo<void> {
const PeopleCollectionRoute({List<PageRouteInfo>? children})
: super(
PeopleCollectionRoute.name,
initialChildren: children,
);
static const String name = 'PeopleCollectionRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const PeopleCollectionPage();
},
);
}
/// generated route for /// generated route for
/// [PermissionOnboardingPage] /// [PermissionOnboardingPage]
class PermissionOnboardingRoute extends PageRouteInfo<void> { class PermissionOnboardingRoute extends PageRouteInfo<void> {
@ -1149,6 +1253,25 @@ class PhotosRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [PlacesCollectionPage]
class PlacesCollectionRoute extends PageRouteInfo<void> {
const PlacesCollectionRoute({List<PageRouteInfo>? children})
: super(
PlacesCollectionRoute.name,
initialChildren: children,
);
static const String name = 'PlacesCollectionRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const PlacesCollectionPage();
},
);
}
/// generated route for /// generated route for
/// [RecentlyAddedPage] /// [RecentlyAddedPage]
class RecentlyAddedRoute extends PageRouteInfo<void> { class RecentlyAddedRoute extends PageRouteInfo<void> {

View File

@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.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/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/albumv2.provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart';
@ -50,6 +51,10 @@ class TabNavigationObserver extends AutoRouterObserver {
ref.read(albumProvider.notifier).getAllAlbums(); ref.read(albumProvider.notifier).getAllAlbums();
} }
if (route.name == 'CollectionsRoute') {
ref.read(albumProviderV2.notifier).refreshAlbums();
}
if (route.name == 'HomeRoute') { if (route.name == 'HomeRoute') {
ref.invalidate(memoryFutureProvider); ref.invalidate(memoryFutureProvider);
Future(() => ref.read(assetProvider.notifier).getAllAsset()); Future(() => ref.read(assetProvider.notifier).getAllAsset());

View File

@ -175,6 +175,49 @@ class AlbumService {
return changes; return changes;
} }
/// V2
Future<bool> refreshAllRemoteAlbums() async {
if (!_remoteCompleter.isCompleted) {
// guard against concurrent calls
return _remoteCompleter.future;
}
_remoteCompleter = Completer();
final Stopwatch sw = Stopwatch()..start();
bool changes = false;
try {
final albumList = await Future.wait([
// _apiService.albumsApi.getAllAlbums(shared: true),
// _apiService.albumsApi.getAllAlbums(shared: false),
]);
// for (int i = 0; i < albumList.length; i++) {
// final albums = albumList[i];
// final isShared = i == 1;
// if (albums != null) {
// final hasChange = await _syncService.syncRemoteAlbumsToDb(
// albums,
// isShared: isShared,
// loadDetails: (dto) async => dto.assetCount == dto.assets.length
// ? dto
// : (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
// );
// if (hasChange) {
// changes = true;
// }
// }
// }
} catch (e) {
debugPrint("Error refreshing all albums: $e");
return false;
} finally {
_remoteCompleter.complete(changes);
}
debugPrint("refreshAllRemoteAlbums took ${sw.elapsedMilliseconds}ms");
return changes;
}
Future<Album?> createAlbum( Future<Album?> createAlbum(
String albumName, String albumName,
Iterable<Asset> assets, [ Iterable<Asset> assets, [

View File

@ -12,12 +12,14 @@ class AlbumThumbnailCard extends StatelessWidget {
/// Whether or not to show the owner of the album (or "Owned") /// Whether or not to show the owner of the album (or "Owned")
/// in the subtitle of the album /// in the subtitle of the album
final bool showOwner; final bool showOwner;
final bool showTitle;
const AlbumThumbnailCard({ const AlbumThumbnailCard({
super.key, super.key,
required this.album, required this.album,
this.onTap, this.onTap,
this.showOwner = false, this.showOwner = false,
this.showTitle = true,
}); });
final Album album; final Album album;
@ -76,7 +78,7 @@ class AlbumThumbnailCard extends StatelessWidget {
: 'album_thumbnail_card_items' : 'album_thumbnail_card_items'
.tr(args: ['${album.assetCount}']), .tr(args: ['${album.assetCount}']),
), ),
if (owner != null) const TextSpan(text: ' · '), if (owner != null) const TextSpan(text: ' '),
if (owner != null) TextSpan(text: owner), if (owner != null) TextSpan(text: owner),
], ],
), ),
@ -102,21 +104,23 @@ class AlbumThumbnailCard extends StatelessWidget {
: buildAlbumThumbnail(), : buildAlbumThumbnail(),
), ),
), ),
Padding( if (showTitle) ...[
padding: const EdgeInsets.only(top: 8.0), Padding(
child: SizedBox( padding: const EdgeInsets.only(top: 8.0),
width: cardSize, child: SizedBox(
child: Text( width: cardSize,
album.name, child: Text(
overflow: TextOverflow.ellipsis, album.name,
style: context.textTheme.bodyMedium?.copyWith( overflow: TextOverflow.ellipsis,
color: context.colorScheme.onSurface, style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500, color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
), ),
), ),
), ),
), buildAlbumTextRow(),
buildAlbumTextRow(), ],
], ],
), ),
), ),

View File

@ -18,9 +18,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
@override @override
Size get preferredSize => const Size.fromHeight(kToolbarHeight); Size get preferredSize => const Size.fromHeight(kToolbarHeight);
final Widget? action; final List<Widget>? actions;
final bool showUploadButton;
const ImmichAppBar({super.key, this.action}); const ImmichAppBar({super.key, this.actions, this.showUploadButton = true});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -184,12 +185,18 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
}, },
), ),
actions: [ actions: [
if (action != null) if (actions != null)
Padding(padding: const EdgeInsets.only(right: 20), child: action!), ...actions!.map(
Padding( (action) => Padding(
padding: const EdgeInsets.only(right: 20), padding: const EdgeInsets.only(right: 16),
child: buildBackupIndicator(), child: action,
), ),
),
if (showUploadButton)
Padding(
padding: const EdgeInsets.only(right: 20),
child: buildBackupIndicator(),
),
Padding( Padding(
padding: const EdgeInsets.only(right: 20), padding: const EdgeInsets.only(right: 20),
child: buildProfileIndicator(), child: buildProfileIndicator(),

View File

@ -0,0 +1,21 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/routing/router.dart';
class SharePartnerButton extends StatelessWidget {
const SharePartnerButton({super.key});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => context.pushRoute(const PartnerRoute()),
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Icon(
Icons.swap_horizontal_circle_rounded,
size: 25,
semanticLabel: 'partner_page_title'.tr(),
),
);
}
}

View File

@ -13,6 +13,7 @@ class SearchMapThumbnail extends StatelessWidget {
}); });
final double size; final double size;
final bool showTitle = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {