mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(mobile): new mobile UI (#12582)
This commit is contained in:
parent
b59abdff3d
commit
e9813315e7
56 changed files with 1960 additions and 1274 deletions
469
mobile/lib/pages/albums/albums.page.dart
Normal file
469
mobile/lib/pages/albums/albums.page.dart
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
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/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.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';
|
||||
|
||||
@RoutePage()
|
||||
class AlbumsPage extends HookConsumerWidget {
|
||||
const AlbumsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums =
|
||||
ref.watch(albumProvider).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;
|
||||
final searchFocusNode = useFocusNode();
|
||||
|
||||
toggleViewMode() {
|
||||
isGrid.value = !isGrid.value;
|
||||
}
|
||||
|
||||
onSearch(String searchTerm, QuickFilterMode mode) {
|
||||
debounceTimer.value?.cancel();
|
||||
debounceTimer.value = Timer(const Duration(milliseconds: 300), () {
|
||||
ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode);
|
||||
});
|
||||
}
|
||||
|
||||
changeFilter(QuickFilterMode mode) {
|
||||
filterMode.value = mode;
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
searchController.addListener(() {
|
||||
onSearch(searchController.text, filterMode.value);
|
||||
});
|
||||
|
||||
return () {
|
||||
searchController.removeListener(() {
|
||||
onSearch(searchController.text, filterMode.value);
|
||||
});
|
||||
debounceTimer.value?.cancel();
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
clearSearch() {
|
||||
filterMode.value = QuickFilterMode.all;
|
||||
searchController.clear();
|
||||
onSearch('', QuickFilterMode.all);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: ImmichAppBar(
|
||||
showUploadButton: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add_rounded,
|
||||
size: 28,
|
||||
),
|
||||
onPressed: () => context.pushRoute(
|
||||
CreateAlbumRoute(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
displacement: 70,
|
||||
onRefresh: () async {
|
||||
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||
},
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: context.colorScheme.onSurface.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withOpacity(0.075),
|
||||
context.colorScheme.primary.withOpacity(0.09),
|
||||
context.colorScheme.primary.withOpacity(0.075),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
transform: GradientRotation(0.5 * pi),
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.all(16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceDim,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceDim,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.primary.withAlpha(100),
|
||||
),
|
||||
),
|
||||
hintText: 'search_albums'.tr(),
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.search_rounded),
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear_rounded),
|
||||
onPressed: clearSearch,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
controller: searchController,
|
||||
onChanged: (_) =>
|
||||
onSearch(searchController.text, filterMode.value),
|
||||
focusNode: searchFocusNode,
|
||||
onTapOutside: (_) => searchFocusNode.unfocus(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
QuickFilterButton(
|
||||
label: 'all'.tr(),
|
||||
isSelected: filterMode.value == QuickFilterMode.all,
|
||||
onTap: () {
|
||||
changeFilter(QuickFilterMode.all);
|
||||
onSearch(searchController.text, QuickFilterMode.all);
|
||||
},
|
||||
),
|
||||
QuickFilterButton(
|
||||
label: 'shared_with_me'.tr(),
|
||||
isSelected: filterMode.value == QuickFilterMode.sharedWithMe,
|
||||
onTap: () {
|
||||
changeFilter(QuickFilterMode.sharedWithMe);
|
||||
onSearch(
|
||||
searchController.text,
|
||||
QuickFilterMode.sharedWithMe,
|
||||
);
|
||||
},
|
||||
),
|
||||
QuickFilterButton(
|
||||
label: 'my_albums'.tr(),
|
||||
isSelected: filterMode.value == QuickFilterMode.myAlbums,
|
||||
onTap: () {
|
||||
changeFilter(QuickFilterMode.myAlbums);
|
||||
onSearch(
|
||||
searchController.text,
|
||||
QuickFilterMode.myAlbums,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SortButton(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isGrid.value
|
||||
? Icons.view_list_outlined
|
||||
: 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 NeverScrollableScrollPhysics(),
|
||||
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(
|
||||
onPressed: onTap,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: 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(
|
||||
elevation: WidgetStatePropertyAll(1),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
),
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.all(4),
|
||||
),
|
||||
),
|
||||
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.fromLTRB(16, 16, 32, 16),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
albumSortOption == mode
|
||||
? context.colorScheme.primary
|
||||
: Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
mode.label.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: albumSortOption == mode
|
||||
? context.colorScheme.onPrimary
|
||||
: context.colorScheme.onSurface.withAlpha(185),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
builder: (context, controller, child) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||
|
||||
handleSyncAlbumToggle(bool isEnable) async {
|
||||
if (isEnable) {
|
||||
await ref.read(albumProvider.notifier).getAllAlbums();
|
||||
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||
for (final album in selectedBackupAlbums) {
|
||||
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||
.read(backupProvider.notifier)
|
||||
.backupAlbumSelectionDone();
|
||||
// waited until backup albums are stored in DB
|
||||
ref.read(albumProvider.notifier).getDeviceAlbums();
|
||||
ref.read(albumProvider.notifier).refreshDeviceAlbums();
|
||||
},
|
||||
child: const Text(
|
||||
"backup_controller_page_select",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:fluttertoast/fluttertoast.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/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
|
@ -45,11 +45,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||
|
||||
try {
|
||||
final isSuccess =
|
||||
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
|
||||
await ref.read(albumProvider.notifier).leaveAlbum(album);
|
||||
|
||||
if (isSuccess) {
|
||||
context.navigateTo(
|
||||
const TabControllerRoute(children: [SharingRoute()]),
|
||||
TabControllerRoute(children: [AlbumsRoute()]),
|
||||
);
|
||||
} else {
|
||||
showErrorMessage();
|
||||
|
|
@ -65,9 +65,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||
isProcessing.value = true;
|
||||
|
||||
try {
|
||||
await ref
|
||||
.read(sharedAlbumProvider.notifier)
|
||||
.removeUserFromAlbum(album, user);
|
||||
await ref.read(albumProvider.notifier).removeUser(album, user);
|
||||
album.sharedUsers.remove(user);
|
||||
sharedUsers.value = album.sharedUsers.toList();
|
||||
} catch (error) {
|
||||
|
|
@ -200,8 +198,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||
onChanged: (bool value) async {
|
||||
activityEnabled.value = value;
|
||||
if (await ref
|
||||
.read(sharedAlbumProvider.notifier)
|
||||
.setActivityEnabled(album, value)) {
|
||||
.read(albumProvider.notifier)
|
||||
.setActivitystatus(album, value)) {
|
||||
album.activityEnabled = value;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.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/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
|
|
@ -25,20 +25,15 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
|||
final suggestedShareUsers = ref.watch(otherUsersProvider);
|
||||
|
||||
createSharedAlbum() async {
|
||||
var newAlbum =
|
||||
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
|
||||
ref.watch(albumTitleProvider),
|
||||
assets,
|
||||
sharedUsersList.value,
|
||||
);
|
||||
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||
ref.watch(albumTitleProvider),
|
||||
assets,
|
||||
);
|
||||
|
||||
if (newAlbum != null) {
|
||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
context.maybePop(true);
|
||||
context
|
||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
||||
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||
}
|
||||
|
||||
ScaffoldMessenger(
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
|
|
@ -50,9 +48,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
||||
final a = album.valueOrNull;
|
||||
final bool isSuccess = a != null &&
|
||||
await ref
|
||||
.read(sharedAlbumProvider.notifier)
|
||||
.removeAssetFromAlbum(a, assets);
|
||||
await ref.read(albumProvider.notifier).removeAsset(a, assets);
|
||||
|
||||
if (!isSuccess) {
|
||||
ImmichToast.show(
|
||||
|
|
@ -81,9 +77,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
// Check if there is new assets add
|
||||
isProcessing.value = true;
|
||||
|
||||
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
||||
returnPayload.selectedAssets,
|
||||
await ref.watch(albumProvider.notifier).addAssets(
|
||||
albumInfo,
|
||||
returnPayload.selectedAssets,
|
||||
);
|
||||
|
||||
isProcessing.value = false;
|
||||
|
|
@ -98,9 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
if (sharedUserIds != null) {
|
||||
isProcessing.value = true;
|
||||
|
||||
await ref
|
||||
.watch(albumServiceProvider)
|
||||
.addAdditionalUserToAlbum(sharedUserIds, album);
|
||||
await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds);
|
||||
|
||||
isProcessing.value = false;
|
||||
}
|
||||
|
|
@ -184,27 +178,29 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
Widget buildSharedUserIconsRow(Album album) {
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: ((context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: UserCircleAvatar(
|
||||
user: album.sharedUsers.toList()[index],
|
||||
radius: 18,
|
||||
size: 36,
|
||||
return album.sharedUsers.isNotEmpty
|
||||
? GestureDetector(
|
||||
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: ((context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: UserCircleAvatar(
|
||||
user: album.sharedUsers.toList()[index],
|
||||
radius: 18,
|
||||
size: 36,
|
||||
),
|
||||
);
|
||||
}),
|
||||
itemCount: album.sharedUsers.length,
|
||||
),
|
||||
);
|
||||
}),
|
||||
itemCount: album.sharedUsers.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget buildHeader(Album album) {
|
||||
|
|
@ -214,7 +210,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
children: [
|
||||
buildTitle(album),
|
||||
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
||||
if (album.shared) buildSharedUserIconsRow(album),
|
||||
buildSharedUserIconsRow(album),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -231,17 +227,17 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
body: Stack(
|
||||
children: [
|
||||
album.widgetWhen(
|
||||
onData: (data) => MultiselectGrid(
|
||||
onData: (albumInfo) => MultiselectGrid(
|
||||
renderListProvider: albumRenderlistProvider(albumId),
|
||||
topWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildHeader(data),
|
||||
if (data.isRemote) buildControlButton(data),
|
||||
buildHeader(albumInfo),
|
||||
if (albumInfo.isRemote) buildControlButton(albumInfo),
|
||||
],
|
||||
),
|
||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||
editEnabled: data.ownerId == userId,
|
||||
editEnabled: albumInfo.ownerId == userId,
|
||||
),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
|||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class CreateAlbumPage extends HookConsumerWidget {
|
||||
final bool isSharedAlbum;
|
||||
final List<Asset>? initialAssets;
|
||||
final List<Asset>? assets;
|
||||
|
||||
const CreateAlbumPage({
|
||||
super.key,
|
||||
required this.isSharedAlbum,
|
||||
this.initialAssets,
|
||||
this.assets,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -34,18 +32,9 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||
final isAlbumTitleTextFieldFocus = useState(false);
|
||||
final isAlbumTitleEmpty = useState(true);
|
||||
final selectedAssets = useState<Set<Asset>>(
|
||||
initialAssets != null ? Set.from(initialAssets!) : const {},
|
||||
assets != null ? Set.from(assets!) : const {},
|
||||
);
|
||||
|
||||
showSelectUserPage() async {
|
||||
final bool? ok = await context.pushRoute<bool?>(
|
||||
AlbumSharedUserSelectionRoute(assets: selectedAssets.value),
|
||||
);
|
||||
if (ok == true) {
|
||||
selectedAssets.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
void onBackgroundTapped() {
|
||||
albumTitleTextFieldFocusNode.unfocus();
|
||||
isAlbumTitleTextFieldFocus.value = false;
|
||||
|
|
@ -199,7 +188,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||
);
|
||||
|
||||
if (newAlbum != null) {
|
||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||
selectedAssets.value = {};
|
||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
|
||||
|
|
@ -223,36 +212,20 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||
'share_create_album',
|
||||
).tr(),
|
||||
actions: [
|
||||
if (isSharedAlbum)
|
||||
TextButton(
|
||||
onPressed: albumTitleController.text.isNotEmpty
|
||||
? showSelectUserPage
|
||||
: null,
|
||||
child: Text(
|
||||
'create_shared_album_page_share'.tr(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: albumTitleController.text.isEmpty
|
||||
? context.themeData.disabledColor
|
||||
: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isSharedAlbum)
|
||||
TextButton(
|
||||
onPressed: albumTitleController.text.isNotEmpty
|
||||
? createNonSharedAlbum
|
||||
: null,
|
||||
child: Text(
|
||||
'create_shared_album_page_create'.tr(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: albumTitleController.text.isNotEmpty
|
||||
? context.primaryColor
|
||||
: context.themeData.disabledColor,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: albumTitleController.text.isNotEmpty
|
||||
? createNonSharedAlbum
|
||||
: null,
|
||||
child: Text(
|
||||
'create_shared_album_page_create'.tr(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: albumTitleController.text.isNotEmpty
|
||||
? context.primaryColor
|
||||
: context.themeData.disabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GestureDetector(
|
||||
|
|
|
|||
50
mobile/lib/pages/common/large_leading_tile.dart
Normal file
50
mobile/lib/pages/common/large_leading_tile.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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,
|
||||
),
|
||||
this.borderRadius = 20.0,
|
||||
});
|
||||
|
||||
final Widget leading;
|
||||
final VoidCallback onTap;
|
||||
final Widget title;
|
||||
final Widget? subtitle;
|
||||
final EdgeInsetsGeometry leadingPadding;
|
||||
final double borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
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(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ 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/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
|
@ -16,10 +17,11 @@ class TabControllerPage extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final refreshing = ref.watch(assetProvider);
|
||||
final isRefreshingAssets = ref.watch(assetProvider);
|
||||
final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider);
|
||||
|
||||
Widget buildIcon(Widget icon) {
|
||||
if (!refreshing) return icon;
|
||||
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
||||
if (!isProcessing) return icon;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
|
|
@ -84,15 +86,15 @@ class TabControllerPage extends HookConsumerWidget {
|
|||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
selectedIcon: const Icon(Icons.share),
|
||||
label: const Text('tab_controller_nav_sharing').tr(),
|
||||
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.photo_album_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_album),
|
||||
label: const Text('tab_controller_nav_library').tr(),
|
||||
icon: const Icon(Icons.space_dashboard_outlined),
|
||||
selectedIcon: const Icon(Icons.space_dashboard_rounded),
|
||||
label: const Text('library').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -118,7 +120,8 @@ class TabControllerPage extends HookConsumerWidget {
|
|||
Icons.photo_library_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
isProcessing: isRefreshingAssets,
|
||||
icon: Icon(
|
||||
Icons.photo_library,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
|
|
@ -135,38 +138,42 @@ class TabControllerPage extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
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(),
|
||||
label: 'albums'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
isProcessing: isRefreshingRemoteAlbums,
|
||||
icon: Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'library'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.space_dashboard_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
isProcessing: isRefreshingAssets,
|
||||
icon: Icon(
|
||||
Icons.space_dashboard_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||
return AutoTabsRouter(
|
||||
routes: const [
|
||||
PhotosRoute(),
|
||||
SearchRoute(),
|
||||
SharingRoute(),
|
||||
LibraryRoute(),
|
||||
routes: [
|
||||
const PhotosRoute(),
|
||||
SearchInputRoute(),
|
||||
const AlbumsRoute(),
|
||||
const LibraryRoute(),
|
||||
],
|
||||
duration: const Duration(milliseconds: 600),
|
||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class EditImagePage extends ConsumerWidget {
|
|||
imageData,
|
||||
title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
|
||||
);
|
||||
await ref.read(albumProvider.notifier).getDeviceAlbums();
|
||||
await ref.read(albumProvider.notifier).refreshDeviceAlbums();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
ImmichToast.show(
|
||||
durationInSecond: 3,
|
||||
|
|
|
|||
|
|
@ -1,324 +1,248 @@
|
|||
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/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/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||
import 'package:immich_mobile/routing/router.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 HookConsumerWidget {
|
||||
class LibraryPage extends ConsumerWidget {
|
||||
const LibraryPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
final albums = ref.watch(albumProvider);
|
||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
Widget buildSortButton() {
|
||||
return PopupMenuButton(
|
||||
position: PopupMenuPosition.over,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return AlbumSortMode.values
|
||||
.map<PopupMenuEntry<AlbumSortMode>>((option) {
|
||||
final selected = albumSortOption == option;
|
||||
return PopupMenuItem(
|
||||
value: option,
|
||||
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: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color:
|
||||
selected ? context.primaryColor : Colors.transparent,
|
||||
),
|
||||
ActionButton(
|
||||
onPressed: () => context.pushRoute(const FavoritesRoute()),
|
||||
icon: Icons.favorite_outline_rounded,
|
||||
label: 'favorites'.tr(),
|
||||
),
|
||||
Text(
|
||||
option.label.tr(),
|
||||
style: TextStyle(
|
||||
color: selected ? context.primaryColor : null,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ActionButton(
|
||||
onPressed: () => context.pushRoute(const ArchiveRoute()),
|
||||
icon: Icons.archive_outlined,
|
||||
label: 'archived'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
onSelected: (AlbumSortMode value) {
|
||||
final selected = albumSortOption == value;
|
||||
// Switch direction
|
||||
if (selected) {
|
||||
ref
|
||||
.read(albumSortOrderProvider.notifier)
|
||||
.changeSortDirection(!albumSortIsReverse);
|
||||
} else {
|
||||
ref.read(albumSortByOptionsProvider.notifier).changeSortMode(value);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: Icon(
|
||||
albumSortIsReverse
|
||||
? Icons.arrow_downward_rounded
|
||||
: Icons.arrow_upward_rounded,
|
||||
size: 14,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
albumSortOption.label.tr(),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
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: 12),
|
||||
const Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
PeopleCollectionCard(),
|
||||
PlacesCollectionCard(),
|
||||
LocalAlbumsCollectionCard(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
QuickAccessButtons(),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCreateAlbumButton() {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
var cardSize = constraints.maxWidth;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.add_rounded,
|
||||
size: 28,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
bottom: 16,
|
||||
),
|
||||
child: Text(
|
||||
'library_page_new_album',
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLibraryNavButton(
|
||||
String label,
|
||||
IconData icon,
|
||||
Function() onClick,
|
||||
) {
|
||||
return Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: onClick,
|
||||
label: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
backgroundColor: context.colorScheme.surfaceContainer,
|
||||
alignment: Alignment.centerLeft,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
icon,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final remote = albums.where((a) => a.isRemote).toList();
|
||||
final sorted = albumSortOption.sortFn(remote, albumSortIsReverse);
|
||||
final local = albums.where((a) => a.isLocal).toList();
|
||||
|
||||
Widget? shareTrashButton() {
|
||||
return trashEnabled
|
||||
? InkWell(
|
||||
onTap: () => context.pushRoute(const TrashRoute()),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Icon(
|
||||
Icons.delete_rounded,
|
||||
size: 25,
|
||||
semanticLabel: 'profile_drawer_trash'.tr(),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: ImmichAppBar(
|
||||
action: shareTrashButton(),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
top: 24.0,
|
||||
bottom: 12.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
buildLibraryNavButton(
|
||||
"library_page_favorites".tr(), Icons.favorite_border, () {
|
||||
context.navigateTo(const FavoritesRoute());
|
||||
}),
|
||||
const SizedBox(width: 12.0),
|
||||
buildLibraryNavButton(
|
||||
"library_page_archive".tr(), Icons.archive_outlined, () {
|
||||
context.navigateTo(const ArchiveRoute());
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12.0,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
bottom: 20.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'library_page_albums',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
buildSortButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: sorted.length + 1,
|
||||
(context, index) {
|
||||
if (index == 0) {
|
||||
return buildCreateAlbumButton();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return AlbumThumbnailCard(
|
||||
album: sorted[index - 1],
|
||||
onTap: () => context.pushRoute(
|
||||
AlbumViewerRoute(
|
||||
albumId: sorted[index - 1].id,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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),
|
||||
],
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12.0,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
bottom: 20.0,
|
||||
leading: const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 26,
|
||||
),
|
||||
title: Text(
|
||||
'partners'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'library_page_device_albums',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
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);
|
||||
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),
|
||||
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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: local.length,
|
||||
(context, index) => AlbumThumbnailCard(
|
||||
album: local[index],
|
||||
onTap: () => context.pushRoute(
|
||||
AlbumViewerRoute(
|
||||
albumId: local[index].id,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'people'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -327,3 +251,158 @@ class LibraryPage extends HookConsumerWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LocalAlbumsCollectionCard extends HookConsumerWidget {
|
||||
const LocalAlbumsCollectionCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums = ref.watch(localAlbumsProvider);
|
||||
|
||||
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||
|
||||
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) {
|
||||
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: 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
55
mobile/lib/pages/library/local_albums.page.dart
Normal file
55
mobile/lib/pages/library/local_albums.page.dart
Normal 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/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LocalAlbumsPage extends HookConsumerWidget {
|
||||
const LocalAlbumsPage({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: LargeLeadingTile(
|
||||
leadingPadding: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
child: ImmichThumbnail(
|
||||
asset: albums[index].thumbnail.value,
|
||||
width: 80,
|
||||
height: 80,
|
||||
),
|
||||
),
|
||||
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)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -86,12 +86,10 @@ class PartnerPage extends HookConsumerWidget {
|
|||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"partner_page_shared_to_title",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface.withAlpha(200),
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
|
|
@ -104,10 +102,7 @@ class PartnerPage extends HookConsumerWidget {
|
|||
leading: userAvatar(context, users[index]),
|
||||
title: Text(
|
||||
users[index].email,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.person_remove),
|
||||
|
|
@ -148,7 +143,7 @@ class PartnerPage extends HookConsumerWidget {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("partner_page_title").tr(),
|
||||
title: const Text("partners").tr(),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
|
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.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/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
|
|
@ -22,7 +23,11 @@ class PartnerDetailPage extends HookConsumerWidget {
|
|||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(assetProvider.notifier).getAllAsset();
|
||||
Future.microtask(
|
||||
() async => {
|
||||
await ref.read(assetProvider.notifier).getAllAsset(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
|
|
@ -64,19 +69,47 @@ class PartnerDetailPage extends HookConsumerWidget {
|
|||
title: Text(partner.name),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: toggleInTimeline,
|
||||
icon: Icon(
|
||||
inTimeline.value
|
||||
? Icons.collections
|
||||
: Icons.collections_outlined,
|
||||
),
|
||||
tooltip: "Show/hide photos on your main timeline",
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MultiselectGrid(
|
||||
topWidget: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0),
|
||||
child: 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),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
"Show in timeline",
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
"Show photos and videos from this user in your timeline",
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
trailing: Switch(
|
||||
value: inTimeline.value,
|
||||
onChanged: (_) => toggleInTimeline(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
renderListProvider: assetsProvider(partner.isarId),
|
||||
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
|
||||
deleteEnabled: false,
|
||||
104
mobile/lib/pages/library/people/people_collection.page.dart
Normal file
104
mobile/lib/pages/library/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/library/places/places_collection.part.dart
Normal file
125
mobile/lib/pages/library/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: () => Center(child: 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ 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/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
|
|
@ -33,8 +32,7 @@ class PhotosPage extends HookConsumerWidget {
|
|||
() {
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums());
|
||||
ref.read(serverInfoProvider.notifier).getServerInfo();
|
||||
return;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget {
|
|||
Text(
|
||||
name.value,
|
||||
style: context.textTheme.titleLarge,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -125,9 +126,11 @@ class PersonResultPage extends HookConsumerWidget {
|
|||
headers: ApiService.getRequestHeaders(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: buildTitleBlock(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||
child: buildTitleBlock(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,25 +1,11 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/widgets/search/curated_people_row.dart';
|
||||
import 'package:immich_mobile/widgets/search/curated_places_row.dart';
|
||||
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_row_section.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/scaffold_error_body.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
|
|
@ -28,12 +14,6 @@ class SearchPage extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final places = ref.watch(getPreviewPlacesProvider);
|
||||
final curatedPeople = ref.watch(getAllPeopleProvider);
|
||||
final isMapEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
|
||||
final double imageSize = math.min(context.width / 3, 150);
|
||||
|
||||
TextStyle categoryTitleStyle = const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.0,
|
||||
|
|
@ -41,87 +21,6 @@ class SearchPage extends HookConsumerWidget {
|
|||
|
||||
Color categoryIconColor = context.colorScheme.onSurface;
|
||||
|
||||
showNameEditModel(
|
||||
String personId,
|
||||
String personName,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return PersonNameEditForm(personId: personId, personName: personName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildPeople() {
|
||||
return curatedPeople.widgetWhen(
|
||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||
onData: (people) {
|
||||
return SearchRowSection(
|
||||
onViewAllPressed: () => context.pushRoute(const AllPeopleRoute()),
|
||||
title: "search_page_people".tr(),
|
||||
isEmpty: people.isEmpty,
|
||||
child: CuratedPeopleRow(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
content: people
|
||||
.map((e) => SearchCuratedContent(label: e.name, id: e.id))
|
||||
.take(12)
|
||||
.toList(),
|
||||
onTap: (content, index) {
|
||||
context.pushRoute(
|
||||
PersonResultRoute(
|
||||
personId: content.id,
|
||||
personName: content.label,
|
||||
),
|
||||
);
|
||||
},
|
||||
onNameTap: (person, index) => {
|
||||
showNameEditModel(person.id, person.label),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildPlaces() {
|
||||
return places.widgetWhen(
|
||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||
onData: (data) {
|
||||
return SearchRowSection(
|
||||
onViewAllPressed: () => context.pushRoute(const AllPlacesRoute()),
|
||||
title: "search_page_places".tr(),
|
||||
isEmpty: !isMapEnabled && data.isEmpty,
|
||||
child: CuratedPlacesRow(
|
||||
isMapEnabled: isMapEnabled,
|
||||
content: data,
|
||||
imageSize: imageSize,
|
||||
onTap: (content, index) {
|
||||
context.pushRoute(
|
||||
SearchInputRoute(
|
||||
prefilter: SearchFilter(
|
||||
people: {},
|
||||
location: SearchLocationFilter(
|
||||
city: content.label,
|
||||
),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
display: SearchDisplayFilters(
|
||||
isNotInAlbum: false,
|
||||
isArchive: false,
|
||||
isFavorite: false,
|
||||
),
|
||||
mediaType: AssetType.other,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildSearchButton() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
|
|
@ -165,20 +64,17 @@ class SearchPage extends HookConsumerWidget {
|
|||
body: ListView(
|
||||
children: [
|
||||
buildSearchButton(),
|
||||
const SizedBox(height: 8.0),
|
||||
buildPeople(),
|
||||
const SizedBox(height: 8.0),
|
||||
buildPlaces(),
|
||||
const SizedBox(height: 24.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'search_page_your_activity',
|
||||
'search_page_categories',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.favorite_border_rounded,
|
||||
|
|
@ -200,16 +96,7 @@ class SearchPage extends HookConsumerWidget {
|
|||
).tr(),
|
||||
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'search_page_categories',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
const CategoryDivider(),
|
||||
ListTile(
|
||||
title: Text('search_page_videos', style: categoryTitleStyle).tr(),
|
||||
leading: Icon(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class SearchInputPage extends HookConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isContextualSearch = useState(true);
|
||||
final textSearchController = useTextEditingController();
|
||||
final focusNode = useFocusNode();
|
||||
final filter = useState<SearchFilter>(
|
||||
SearchFilter(
|
||||
people: prefilter?.people ?? {},
|
||||
|
|
@ -440,6 +441,10 @@ class SearchInputPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
handleTextSubmitted(String value) {
|
||||
if (value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContextualSearch.value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
context: value,
|
||||
|
|
@ -489,38 +494,82 @@ class SearchInputPage extends HookConsumerWidget {
|
|||
appBar: AppBar(
|
||||
automaticallyImplyLeading: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: isContextualSearch.value
|
||||
? const Icon(Icons.abc_rounded)
|
||||
: const Icon(Icons.image_search_rounded),
|
||||
onPressed: () {
|
||||
isContextualSearch.value = !isContextualSearch.value;
|
||||
textSearchController.clear();
|
||||
},
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 14.0),
|
||||
child: IconButton(
|
||||
icon: isContextualSearch.value
|
||||
? const Icon(Icons.abc_rounded)
|
||||
: const Icon(Icons.image_search_rounded),
|
||||
onPressed: () {
|
||||
isContextualSearch.value = !isContextualSearch.value;
|
||||
textSearchController.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
onPressed: () => context.router.maybePop(),
|
||||
),
|
||||
title: TextField(
|
||||
controller: textSearchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: isContextualSearch.value
|
||||
? 'contextual_search'.tr()
|
||||
: 'filename_search'.tr(),
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
title: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: context.colorScheme.onSurface.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
enabledBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withOpacity(0.075),
|
||||
context.colorScheme.primary.withOpacity(0.09),
|
||||
context.colorScheme.primary.withOpacity(0.075),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
onSubmitted: handleTextSubmitted,
|
||||
child: TextField(
|
||||
controller: textSearchController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.all(8),
|
||||
prefixIcon: prefilter != null
|
||||
? null
|
||||
: Icon(
|
||||
Icons.search_rounded,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
hintText: isContextualSearch.value
|
||||
? 'contextual_search'.tr()
|
||||
: 'filename_search'.tr(),
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceDim,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceDim,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.primary.withAlpha(100),
|
||||
),
|
||||
),
|
||||
),
|
||||
onSubmitted: handleTextSubmitted,
|
||||
focusNode: focusNode,
|
||||
onTapOutside: (_) => focusNode.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
|
|
|
|||
|
|
@ -1,283 +0,0 @@
|
|||
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/providers/album/album_sort_by_options.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/providers/partner.provider.dart';
|
||||
import 'package:immich_mobile/widgets/partner/partner_list.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
|
||||
@RoutePage()
|
||||
class SharingPage extends HookConsumerWidget {
|
||||
const SharingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||
final albums = ref.watch(sharedAlbumProvider);
|
||||
final sharedAlbums = albumSortOption.sortFn(albums, albumSortIsReverse);
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
final partner = ref.watch(partnerSharedWithProvider);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
buildAlbumGrid() {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 250,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: .7,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return AlbumThumbnailCard(
|
||||
album: sharedAlbums[index],
|
||||
showOwner: true,
|
||||
onTap: () => context.pushRoute(
|
||||
AlbumViewerRoute(albumId: sharedAlbums[index].id),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: sharedAlbums.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildAlbumList() {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final album = sharedAlbums[index];
|
||||
final isOwner = album.ownerId == userId;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ImmichThumbnail(
|
||||
asset: album.thumbnail.value,
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
album.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: isOwner
|
||||
? Text(
|
||||
'album_thumbnail_owned'.tr(),
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
)
|
||||
: album.ownerName != null
|
||||
? Text(
|
||||
'album_thumbnail_shared_by'
|
||||
.tr(args: [album.ownerName!]),
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () => context
|
||||
.pushRoute(AlbumViewerRoute(albumId: sharedAlbums[index].id)),
|
||||
);
|
||||
},
|
||||
childCount: sharedAlbums.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildTopBottons() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
top: 24.0,
|
||||
bottom: 12.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () =>
|
||||
context.pushRoute(CreateAlbumRoute(isSharedAlbum: true)),
|
||||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
"sharing_silver_appbar_create_shared_album",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => context.pushRoute(const SharedLinkRoute()),
|
||||
icon: const Icon(
|
||||
Icons.link,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
"sharing_silver_appbar_shared_links",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
),
|
||||
maxLines: 1,
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildEmptyListIndication() {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
side: BorderSide(
|
||||
color: context.isDarkTheme
|
||||
? const Color(0xFF383838)
|
||||
: Colors.black12,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5.0, bottom: 5),
|
||||
child: Icon(
|
||||
Icons.insert_photo_rounded,
|
||||
size: 50,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'sharing_page_empty_list',
|
||||
style: context.textTheme.displaySmall,
|
||||
).tr(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'sharing_page_description',
|
||||
style: context.textTheme.bodyMedium,
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
onRefresh: () async {
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: ImmichAppBar(
|
||||
action: sharePartnerButton(),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: buildTopBottons()),
|
||||
if (partner.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
"partner_page_title",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
if (partner.isNotEmpty) PartnerList(partner: partner),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
"sharing_page_album",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (sharedAlbums.isEmpty) {
|
||||
return buildEmptyListIndication();
|
||||
}
|
||||
|
||||
if (constraints.crossAxisExtent < 600) {
|
||||
return buildAlbumList();
|
||||
} else {
|
||||
return buildAlbumGrid();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue