mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01:00
3a2e30e30e
* fix(mobile): make widgets rebuild on locale changes This will make the make the pages to instantly refresh the correct translated string, without the need to pop and push the settings page. * fix(mobile): set the default intl locale This is needed because across the app, you don't pass the context.locale to DateFormat, so by default it uses the system's locale. This will fix the issue without the need to refactor a lot of code. * feat(mobile): create localeProvider This provider can be used to refresh providers that provide UI elements and get cached. * fix(mobile): refresh asset providers on locale change This is necessary to update the locale on the already evaluated DateFormat. --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
430 lines
14 KiB
Dart
430 lines
14 KiB
Dart
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/entities/user.entity.dart';
|
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
|
import 'package:immich_mobile/providers/partner.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/user_avatar.dart';
|
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
|
|
@RoutePage()
|
|
class LibraryPage extends ConsumerWidget {
|
|
const LibraryPage({super.key});
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
context.locale;
|
|
final trashEnabled =
|
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
|
|
|
return Scaffold(
|
|
appBar: ImmichAppBar(),
|
|
body: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: ListView(
|
|
shrinkWrap: true,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 16.0),
|
|
child: 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(),
|
|
),
|
|
SizedBox(width: trashEnabled ? 8 : 0),
|
|
trashEnabled
|
|
? ActionButton(
|
|
onPressed: () => context.pushRoute(const TrashRoute()),
|
|
icon: Icons.delete_outline_rounded,
|
|
label: 'trash'.tr(),
|
|
)
|
|
: const SizedBox.shrink(),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
PeopleCollectionCard(),
|
|
PlacesCollectionCard(),
|
|
LocalAlbumsCollectionCard(),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
QuickAccessButtons(),
|
|
const SizedBox(
|
|
height: 32,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class QuickAccessButtons extends ConsumerWidget {
|
|
const QuickAccessButtons({super.key});
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final partners = ref.watch(partnerSharedWithProvider);
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: context.colorScheme.onSurface.withAlpha(10),
|
|
width: 1,
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
context.colorScheme.primary.withAlpha(10),
|
|
context.colorScheme.primary.withAlpha(15),
|
|
context.colorScheme.primary.withAlpha(20),
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
child: ListView(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
children: [
|
|
ListTile(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(20),
|
|
topRight: Radius.circular(20),
|
|
bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0),
|
|
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
|
|
),
|
|
),
|
|
leading: const Icon(
|
|
Icons.group_outlined,
|
|
size: 26,
|
|
),
|
|
title: Text(
|
|
'partners'.tr(),
|
|
style: context.textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
onTap: () => context.pushRoute(const PartnerRoute()),
|
|
),
|
|
PartnerList(partners: partners),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PartnerList extends ConsumerWidget {
|
|
const PartnerList({super.key, required this.partners});
|
|
|
|
final List<User> partners;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return ListView.builder(
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: partners.length,
|
|
shrinkWrap: true,
|
|
itemBuilder: (context, index) {
|
|
final partner = partners[index];
|
|
final isLastItem = index == partners.length - 1;
|
|
return ListTile(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(isLastItem ? 20 : 0),
|
|
bottomRight: Radius.circular(isLastItem ? 20 : 0),
|
|
),
|
|
),
|
|
contentPadding: const EdgeInsets.only(
|
|
left: 12.0,
|
|
right: 18.0,
|
|
),
|
|
leading: userAvatar(context, partner, radius: 16),
|
|
title: Text(
|
|
"partner_list_user_photos",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
).tr(
|
|
namedArgs: {
|
|
'user': partner.name,
|
|
},
|
|
),
|
|
onTap: () => context.pushRoute(
|
|
(PartnerDetailRoute(partner: partner)),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class PeopleCollectionCard extends ConsumerWidget {
|
|
const PeopleCollectionCard({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final people = ref.watch(getAllPeopleProvider);
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final isTablet = constraints.maxWidth > 600;
|
|
final widthFactor = isTablet ? 0.25 : 0.5;
|
|
final size = MediaQuery.of(context).size.width * widthFactor - 20.0;
|
|
|
|
return GestureDetector(
|
|
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: size,
|
|
width: size,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
context.colorScheme.primary.withAlpha(30),
|
|
context.colorScheme.primary.withAlpha(25),
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
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 LocalAlbumsCollectionCard extends HookConsumerWidget {
|
|
const LocalAlbumsCollectionCard({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final albums = ref.watch(localAlbumsProvider);
|
|
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final isTablet = constraints.maxWidth > 600;
|
|
final widthFactor = isTablet ? 0.25 : 0.5;
|
|
final size = MediaQuery.of(context).size.width * widthFactor - 20.0;
|
|
|
|
return GestureDetector(
|
|
onTap: () => context.pushRoute(
|
|
const LocalAlbumsRoute(),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: size,
|
|
width: size,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
context.colorScheme.primary.withAlpha(30),
|
|
context.colorScheme.primary.withAlpha(25),
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
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(
|
|
'on_this_device'.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) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final isTablet = constraints.maxWidth > 600;
|
|
final widthFactor = isTablet ? 0.25 : 0.5;
|
|
final size = MediaQuery.of(context).size.width * widthFactor - 20.0;
|
|
|
|
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: 15,
|
|
),
|
|
),
|
|
),
|
|
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|