mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 02:13:51 -04:00
Merge 39017922a9555b938e6d6d12396ea17e7b20908f into 63437529e1224f5e7879ce567b1a4502fb97573b
This commit is contained in:
commit
09a2dcca7a
@ -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",
|
||||||
|
405
mobile/lib/pages/collections/albums/albums_collection.page.dart
Normal file
405
mobile/lib/pages/collections/albums/albums_collection.page.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
310
mobile/lib/pages/collections/collections.page.dart
Normal file
310
mobile/lib/pages/collections/collections.page.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
104
mobile/lib/pages/collections/people/people_collection.page.dart
Normal file
104
mobile/lib/pages/collections/people/people_collection.page.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
125
mobile/lib/pages/collections/places/places_collection.part.dart
Normal file
125
mobile/lib/pages/collections/places/places_collection.part.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
47
mobile/lib/pages/common/large_leading_tile.dart
Normal file
47
mobile/lib/pages/common/large_leading_tile.dart
Normal 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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
||||||
|
@ -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: [
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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: [
|
||||||
|
@ -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 =
|
||||||
|
178
mobile/lib/providers/album/albumv2.provider.dart
Normal file
178
mobile/lib/providers/album/albumv2.provider.dart
Normal 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));
|
||||||
|
});
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>(
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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> {
|
||||||
|
@ -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());
|
||||||
|
@ -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, [
|
||||||
|
@ -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(),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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(),
|
||||||
|
21
mobile/lib/widgets/common/share_partner_button.dart
Normal file
21
mobile/lib/widgets/common/share_partner_button.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user