1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-27 10:58:13 +02:00

feat(mobile): new mobile UI (#12582)

This commit is contained in:
Alex 2024-10-10 15:44:14 +07:00 committed by GitHub
parent b59abdff3d
commit e9813315e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1861 additions and 1230 deletions

View File

@ -1,4 +1,24 @@
{
"all": "All",
"shared_with_me": "Shared with me",
"my_albums": "My albums",
"create_new": "CREATE NEW",
"create_album": "Create album",
"videos": "Videos",
"recently_added": "Recently added",
"partners": "Partners",
"partner_page_title": "Partners",
"library": "Library",
"on_this_device": "On this device",
"add_a_name": "Add a name",
"places": "Places",
"albums": "Albums",
"people": "People",
"shared_links": "Shared links",
"trash": "Trash",
"archived": "Archived",
"favorites": "Favorites",
"search_albums": "Search albums",
"action_common_back": "Back",
"action_common_cancel": "Cancel",
"action_common_clear": "Clear",
@ -353,7 +373,6 @@
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"partner_list_user_photos": "{user}'s photos",
"partner_list_view_all": "View all",
"partner_page_add_partner": "Add partner",
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
"partner_page_no_more_users": "No more users to add",
@ -362,7 +381,6 @@
"partner_page_shared_to_title": "Shared to",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_page_stop_sharing_title": "Stop sharing your photos?",
"partner_page_title": "Partner",
"permission_onboarding_back": "Back",
"permission_onboarding_continue_anyway": "Continue anyway",
"permission_onboarding_get_started": "Get started",

View File

@ -3,7 +3,7 @@ PODS:
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
@ -77,7 +77,6 @@ PODS:
- photo_manager (2.0.0):
- Flutter
- FlutterMacOS
- ReachabilitySwift (5.0.0)
- SAMKeychain (1.5.3)
- SDWebImage (5.19.4):
- SDWebImage/Core (= 5.19.4)
@ -102,7 +101,7 @@ PODS:
DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
@ -133,7 +132,6 @@ SPEC REPOS:
- DKImagePickerController
- DKPhotoGallery
- MapLibre
- ReachabilitySwift
- SAMKeychain
- SDWebImage
- SwiftyGif
@ -143,7 +141,7 @@ EXTERNAL SOURCES:
background_downloader:
:path: ".symlinks/plugins/background_downloader/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
:path: ".symlinks/plugins/connectivity_plus/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
@ -195,8 +193,8 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
@ -217,7 +215,6 @@ SPEC CHECKSUMS:
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad

View File

@ -24,7 +24,10 @@ final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
ImmichColorPreset.indigo: ImmichTheme(
light: ColorScheme.fromSeed(
seedColor: immichBrandColorLight,
).copyWith(primary: immichBrandColorLight),
).copyWith(
primary: immichBrandColorLight,
onSurface: const Color.fromARGB(255, 34, 31, 32),
),
dark: ColorScheme.fromSeed(
seedColor: immichBrandColorDark,
brightness: Brightness.dark,

Binary file not shown.

View File

@ -2,6 +2,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
abstract interface class IAlbumRepository implements IDatabaseRepository {
Future<Album> create(Album album);
@ -38,6 +39,8 @@ abstract interface class IAlbumRepository implements IDatabaseRepository {
Future<void> removeAssets(Album album, List<Asset> assets);
Future<Album> recalculateMetadata(Album album);
Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode);
}
enum AlbumSort { remoteId, localId }

View File

@ -0,0 +1,5 @@
enum QuickFilterMode {
all,
sharedWithMe,
myAlbums,
}

View 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),
),
),
],
),
);
},
);
}
}

View File

@ -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);
}

View File

@ -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",

View File

@ -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;
}
},

View File

@ -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(
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.watch(albumTitleProvider),
assets,
sharedUsersList.value,
);
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(

View File

@ -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,7 +178,8 @@ class AlbumViewerPage extends HookConsumerWidget {
}
Widget buildSharedUserIconsRow(Album album) {
return GestureDetector(
return album.sharedUsers.isNotEmpty
? GestureDetector(
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
child: SizedBox(
height: 50,
@ -204,7 +199,8 @@ class AlbumViewerPage extends HookConsumerWidget {
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(

View File

@ -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,22 +212,6 @@ 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

View 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(),
],
),
],
),
);
}
}

View File

@ -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(

View File

@ -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,

View File

@ -1,175 +1,401 @@
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,
child: Row(
return Scaffold(
appBar: ImmichAppBar(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ListView(
shrinkWrap: true,
children: [
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Icon(
Icons.check,
color:
selected ? context.primaryColor : Colors.transparent,
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(),
),
],
),
),
Text(
option.label.tr(),
const SizedBox(height: 8),
Row(
children: [
ActionButton(
onPressed: () => context.pushRoute(const SharedLinkRoute()),
icon: Icons.link_outlined,
label: 'shared_links'.tr(),
),
const SizedBox(width: 8),
trashEnabled
? ActionButton(
onPressed: () => context.pushRoute(const TrashRoute()),
icon: Icons.delete_outline_rounded,
label: 'trash'.tr(),
)
: const SizedBox.shrink(),
],
),
const SizedBox(height: 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),
],
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(
color: selected ? context.primaryColor : null,
fontSize: 14.0,
fontWeight: FontWeight.w500,
),
).tr(
namedArgs: {
'user': partner.name,
},
),
],
onTap: () => context.pushRoute(
(PartnerDetailRoute(partner: partner)),
),
);
}).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,
),
),
],
),
);
}
}
Widget buildCreateAlbumButton() {
return LayoutBuilder(
builder: (context, constraints) {
var cardSize = constraints.maxWidth;
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(CreateAlbumRoute(isSharedAlbum: false)),
child: Padding(
padding:
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: cardSize,
height: cardSize,
height: size,
width: size,
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(20)),
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(30),
context.colorScheme.primary.withAlpha(25),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
child: Center(
child: Icon(
Icons.add_rounded,
size: 28,
color: context.primaryColor,
),
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);
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.only(
top: 8.0,
bottom: 16,
),
padding: const EdgeInsets.all(8.0),
child: Text(
'library_page_new_album',
style: context.textTheme.labelLarge?.copyWith(
'places'.tr(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
).tr(),
),
],
),
),
);
},
);
}
}
Widget buildLibraryNavButton(
String label,
IconData icon,
Function() onClick,
) {
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: onClick,
onPressed: onPressed,
label: Padding(
padding: const EdgeInsets.only(left: 8.0),
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(vertical: 16, horizontal: 16),
backgroundColor: context.colorScheme.surfaceContainer,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
backgroundColor: context.colorScheme.surfaceContainerLow,
alignment: Alignment.centerLeft,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(25)),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
),
),
icon: Icon(
@ -179,151 +405,4 @@ class LibraryPage extends HookConsumerWidget {
),
);
}
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,
),
),
);
},
),
),
),
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_device_albums',
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
).tr(),
],
),
),
),
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,
),
),
),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,55 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/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)),
),
);
},
),
);
}
}

View File

@ -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: [

View File

@ -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,

View File

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

View File

@ -0,0 +1,125 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@RoutePage()
class PlacesCollectionPage extends HookConsumerWidget {
const PlacesCollectionPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final places = ref.watch(getAllPlacesProvider);
return Scaffold(
appBar: AppBar(
title: Text('places'.tr()),
),
body: ListView(
shrinkWrap: true,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
height: 200,
width: context.width,
child: MapThumbnail(
onTap: (_, __) => context.pushRoute(const MapRoute()),
zoom: 8,
centre: const LatLng(
21.44950,
-157.91959,
),
showAttribution: false,
themeMode:
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
),
),
),
places.when(
data: (places) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: places.length,
itemBuilder: (context, index) {
final place = places[index];
return PlaceTile(id: place.id, name: place.label);
},
);
},
error: (error, stask) => const Text('Error getting places'),
loading: () => 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),
),
),
);
}
}

View File

@ -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;
},

View File

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

View File

@ -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(

View File

@ -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,7 +494,9 @@ class SearchInputPage extends HookConsumerWidget {
appBar: AppBar(
automaticallyImplyLeading: true,
actions: [
IconButton(
Padding(
padding: const EdgeInsets.only(right: 14.0),
child: IconButton(
icon: isContextualSearch.value
? const Icon(Icons.abc_rounded)
: const Icon(Icons.image_search_rounded),
@ -498,14 +505,35 @@ class SearchInputPage extends HookConsumerWidget {
textSearchController.clear();
},
),
],
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () => context.router.maybePop(),
),
title: TextField(
],
title: 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,
),
),
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(),
@ -513,14 +541,35 @@ class SearchInputPage extends HookConsumerWidget {
color: context.themeData.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.w500,
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
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),
),
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),
),
onSubmitted: handleTextSubmitted,
focusNode: focusNode,
onTapOutside: (_) => focusNode.unfocus(),
),
),
),
body: Column(

View File

@ -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();
}
},
),
],
),
),
);
}
}

View File

@ -1,21 +1,21 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/utils/renderlist_generator.dart';
import 'package:isar/isar.dart';
final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
class AlbumNotifier extends StateNotifier<List<Album>> {
AlbumNotifier(this._albumService, Isar db) : super([]) {
final query = db.albums
.filter()
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
AlbumNotifier(this._albumService, this.db, this.ref) : super([]) {
final query = db.albums.filter().remoteIdIsNotNull();
query.findAll().then((value) {
if (mounted) {
state = value;
@ -25,14 +25,22 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
}
final AlbumService _albumService;
final Isar db;
final Ref ref;
late final StreamSubscription<List<Album>> _streamSub;
Future<void> getAllAlbums() => Future.wait([
_albumService.refreshDeviceAlbums(),
_albumService.refreshRemoteAlbums(isShared: false),
]);
Future<void> refreshRemoteAlbums() async {
final isRefresing =
ref.read(isRefreshingRemoteAlbumProvider.notifier).state;
Future<void> getDeviceAlbums() => _albumService.refreshDeviceAlbums();
if (isRefresing) return;
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = true;
await _albumService.refreshRemoteAlbums();
ref.read(isRefreshingRemoteAlbumProvider.notifier).state = false;
}
Future<void> refreshDeviceAlbums() => _albumService.refreshDeviceAlbums();
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
@ -59,6 +67,50 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
await createAlbum(albumName, {});
}
Future<bool> leaveAlbum(Album album) async {
var res = await _albumService.leaveAlbum(album);
if (res) {
await deleteAlbum(album);
return true;
} else {
return false;
}
}
void searchAlbums(String searchTerm, QuickFilterMode filterMode) async {
state = await _albumService.search(searchTerm, filterMode);
}
Future<void> addUsers(Album album, List<String> userIds) async {
await _albumService.addUsers(album, userIds);
}
Future<bool> removeUser(Album album, User user) async {
final isRemoved = await _albumService.removeUser(album, user);
if (isRemoved && album.sharedUsers.isEmpty) {
state = state.where((element) => element.id != album.id).toList();
}
return isRemoved;
}
Future<void> addAssets(Album album, Iterable<Asset> assets) async {
await _albumService.addAssets(album, assets);
}
Future<bool> removeAsset(Album album, Iterable<Asset> assets) async {
return await _albumService.removeAsset(album, assets);
}
Future<bool> setActivitystatus(
Album album,
bool enabled,
) {
return _albumService.setActivityStatus(album, enabled);
}
@override
void dispose() {
_streamSub.cancel();
@ -71,6 +123,7 @@ final albumProvider =
return AlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),
ref,
);
});
@ -94,3 +147,31 @@ final albumRenderlistProvider =
}
return const Stream.empty();
});
class LocalAlbumsNotifier extends StateNotifier<List<Album>> {
LocalAlbumsNotifier(this.db) : super([]) {
final query = db.albums.where().remoteIdIsNull();
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub = query.watch().listen((data) => state = data);
}
final Isar db;
late final StreamSubscription<List<Album>> _streamSub;
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
final localAlbumsProvider =
StateNotifierProvider.autoDispose<LocalAlbumsNotifier, List<Album>>((ref) {
return LocalAlbumsNotifier(ref.watch(dbProvider));
});

View File

@ -1,6 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@ -40,7 +39,6 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
if (isSuccess) {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return true;
}

View File

@ -1,90 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:isar/isar.dart';
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
query.findAll().then((value) {
if (mounted) {
state = value;
}
});
_streamSub = query.watch().listen((data) => state = data);
}
final AlbumService _albumService;
late final StreamSubscription<List<Album>> _streamSub;
Future<Album?> createSharedAlbum(
String albumName,
Iterable<Asset> assets,
Iterable<User> sharedUsers,
) async {
try {
return await _albumService.createAlbum(
albumName,
assets,
sharedUsers,
);
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
}
return null;
}
Future<void> getAllSharedAlbums() =>
_albumService.refreshRemoteAlbums(isShared: true);
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
Future<bool> leaveAlbum(Album album) async {
var res = await _albumService.leaveAlbum(album);
if (res) {
await deleteAlbum(album);
return true;
} else {
return false;
}
}
Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
return _albumService.removeAssetFromAlbum(album, assets);
}
Future<bool> removeUserFromAlbum(Album album, User user) async {
final result = await _albumService.removeUserFromAlbum(album, user);
if (result && album.sharedUsers.isEmpty) {
state = state.where((element) => element.id != album.id).toList();
}
return result;
}
Future<bool> setActivityEnabled(Album album, bool activityEnabled) {
return _albumService.setActivityEnabled(album, activityEnabled);
}
@override
void dispose() {
_streamSub.cancel();
super.dispose();
}
}
final sharedAlbumProvider =
StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<Album>>((ref) {
return SharedAlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),
);
});

View File

@ -1,6 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
@ -58,11 +57,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
_ref.read(assetProvider.notifier).getAllAsset();
case TabEnum.search:
// nothing to do
case TabEnum.sharing:
_ref.read(assetProvider.notifier).getAllAsset();
_ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
case TabEnum.albums:
_ref.read(albumProvider.notifier).refreshRemoteAlbums();
case TabEnum.library:
_ref.read(albumProvider.notifier).getAllAlbums();
// nothing to do
}
}

View File

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
import 'package:immich_mobile/entities/user.entity.dart';
@ -115,7 +114,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Store.delete(StoreKey.accessToken),
]);
_ref.invalidate(albumProvider);
_ref.invalidate(sharedAlbumProvider);
state = state.copyWith(
deviceId: "",

View File

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

View File

@ -1,8 +1,10 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart';
@ -118,4 +120,33 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
@override
Future<void> deleteAllLocal() =>
txn(() => db.albums.where().localIdIsNotNull().deleteAll());
@override
Future<List<Album>> search(
String searchTerm,
QuickFilterMode filterMode,
) async {
var query = db.albums
.filter()
.nameContains(searchTerm, caseSensitive: false)
.remoteIdIsNotNull();
switch (filterMode) {
case QuickFilterMode.sharedWithMe:
query = query.owner(
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
);
break;
case QuickFilterMode.myAlbums:
query = query.owner(
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
);
break;
case QuickFilterMode.all:
default:
break;
}
return await query.findAll();
}
}

View File

@ -36,7 +36,7 @@ class PartnerApiRepository extends ApiRepository
}
@override
Future<void> delete(String id) => checkNull(_api.removePartner(id));
Future<void> delete(String id) => _api.removePartner(id);
@override
Future<User> update(String id, {required bool inTimeline}) async {

View File

@ -13,6 +13,11 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
import 'package:immich_mobile/pages/albums/albums.page.dart';
import 'package:immich_mobile/pages/library/local_albums.page.dart';
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
import 'package:immich_mobile/pages/library/places/places_collection.part.dart';
import 'package:immich_mobile/pages/library/library.page.dart';
import 'package:immich_mobile/pages/common/activities.page.dart';
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
@ -32,7 +37,6 @@ import 'package:immich_mobile/pages/editing/crop.page.dart';
import 'package:immich_mobile/pages/editing/filter.page.dart';
import 'package:immich_mobile/pages/library/archive.page.dart';
import 'package:immich_mobile/pages/library/favorite.page.dart';
import 'package:immich_mobile/pages/library/library.page.dart';
import 'package:immich_mobile/pages/library/trash.page.dart';
import 'package:immich_mobile/pages/login/change_password.page.dart';
import 'package:immich_mobile/pages/login/login.page.dart';
@ -49,11 +53,10 @@ import 'package:immich_mobile/pages/search/person_result.page.dart';
import 'package:immich_mobile/pages/search/recently_added.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/search/search_input.page.dart';
import 'package:immich_mobile/pages/sharing/partner/partner.page.dart';
import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart';
import 'package:immich_mobile/pages/sharing/shared_link/shared_link.page.dart';
import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart';
import 'package:immich_mobile/pages/sharing/sharing.page.dart';
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/auth_guard.dart';
@ -103,17 +106,18 @@ class AppRouter extends RootStackRouter {
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: SearchRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: SharingRoute.page,
page: SearchInputRoute.page,
guards: [_authGuard, _duplicateGuard],
maintainState: false,
),
AutoRoute(
page: LibraryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: AlbumsRoute.page,
guards: [_authGuard, _duplicateGuard],
),
],
transitionsBuilder: TransitionsBuilders.fadeIn,
),
@ -137,7 +141,11 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: EditImageRoute.page),
AutoRoute(page: CropImageRoute.page),
AutoRoute(page: FilterImageRoute.page),
AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]),
CustomRoute(
page: FavoritesRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(
page: AllMotionPhotosRoute.page,
@ -183,8 +191,16 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
CustomRoute(
page: ArchiveRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: PartnerRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
AutoRoute(
page: PartnerDetailRoute.page,
guards: [_authGuard, _duplicateGuard],
@ -200,10 +216,15 @@ class AppRouter extends RootStackRouter {
page: AlbumOptionsRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(
CustomRoute(
page: TrashRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: SharedLinkRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
AutoRoute(
page: SharedLinkEditRoute.page,
@ -232,6 +253,26 @@ class AppRouter extends RootStackRouter {
page: HeaderSettingsRoute.page,
guards: [_duplicateGuard],
),
CustomRoute(
page: PeopleCollectionRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: AlbumsRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: LocalAlbumsRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
CustomRoute(
page: PlacesCollectionRoute.page,
guards: [_authGuard, _duplicateGuard],
transitionsBuilder: TransitionsBuilders.slideLeft,
),
];
}

View File

@ -319,6 +319,25 @@ class AlbumViewerRouteArgs {
}
}
/// generated route for
/// [AlbumsPage]
class AlbumsRoute extends PageRouteInfo<void> {
const AlbumsRoute({List<PageRouteInfo>? children})
: super(
AlbumsRoute.name,
initialChildren: children,
);
static const String name = 'AlbumsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const AlbumsPage();
},
);
}
/// generated route for
/// [AllMotionPhotosPage]
class AllMotionPhotosRoute extends PageRouteInfo<void> {
@ -560,15 +579,13 @@ class ChangePasswordRoute extends PageRouteInfo<void> {
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
CreateAlbumRoute({
Key? key,
required bool isSharedAlbum,
List<Asset>? initialAssets,
List<Asset>? assets,
List<PageRouteInfo>? children,
}) : super(
CreateAlbumRoute.name,
args: CreateAlbumRouteArgs(
key: key,
isSharedAlbum: isSharedAlbum,
initialAssets: initialAssets,
assets: assets,
),
initialChildren: children,
);
@ -578,11 +595,11 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<CreateAlbumRouteArgs>();
final args = data.argsAs<CreateAlbumRouteArgs>(
orElse: () => const CreateAlbumRouteArgs());
return CreateAlbumPage(
key: args.key,
isSharedAlbum: args.isSharedAlbum,
initialAssets: args.initialAssets,
assets: args.assets,
);
},
);
@ -591,19 +608,16 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
class CreateAlbumRouteArgs {
const CreateAlbumRouteArgs({
this.key,
required this.isSharedAlbum,
this.initialAssets,
this.assets,
});
final Key? key;
final bool isSharedAlbum;
final List<Asset>? initialAssets;
final List<Asset>? assets;
@override
String toString() {
return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum, initialAssets: $initialAssets}';
return 'CreateAlbumRouteArgs{key: $key, assets: $assets}';
}
}
@ -909,6 +923,25 @@ class LibraryRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [LocalAlbumsPage]
class LocalAlbumsRoute extends PageRouteInfo<void> {
const LocalAlbumsRoute({List<PageRouteInfo>? children})
: super(
LocalAlbumsRoute.name,
initialChildren: children,
);
static const String name = 'LocalAlbumsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const LocalAlbumsPage();
},
);
}
/// generated route for
/// [LoginPage]
class LoginRoute extends PageRouteInfo<void> {
@ -1111,6 +1144,25 @@ class PartnerRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [PeopleCollectionPage]
class PeopleCollectionRoute extends PageRouteInfo<void> {
const PeopleCollectionRoute({List<PageRouteInfo>? children})
: super(
PeopleCollectionRoute.name,
initialChildren: children,
);
static const String name = 'PeopleCollectionRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const PeopleCollectionPage();
},
);
}
/// generated route for
/// [PermissionOnboardingPage]
class PermissionOnboardingRoute extends PageRouteInfo<void> {
@ -1201,6 +1253,25 @@ class PhotosRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [PlacesCollectionPage]
class PlacesCollectionRoute extends PageRouteInfo<void> {
const PlacesCollectionRoute({List<PageRouteInfo>? children})
: super(
PlacesCollectionRoute.name,
initialChildren: children,
);
static const String name = 'PlacesCollectionRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const PlacesCollectionPage();
},
);
}
/// generated route for
/// [RecentlyAddedPage]
class RecentlyAddedRoute extends PageRouteInfo<void> {
@ -1429,25 +1500,6 @@ class SharedLinkRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [SharingPage]
class SharingRoute extends PageRouteInfo<void> {
const SharingRoute({List<PageRouteInfo>? children})
: super(
SharingRoute.name,
initialChildren: children,
);
static const String name = 'SharingRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const SharingPage();
},
);
}
/// generated route for
/// [SplashScreenPage]
class SplashScreenRoute extends PageRouteInfo<void> {

View File

@ -1,12 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
@ -21,14 +19,6 @@ class TabNavigationObserver extends AutoRouterObserver {
required this.ref,
});
@override
void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) {
// Perform tasks on first navigation to SearchRoute
if (route.name == 'SearchRoute') {
// ref.refresh(getCuratedLocationProvider);
}
}
@override
Future<void> didChangeTabRoute(
TabPageRoute route,
@ -41,15 +31,6 @@ class TabNavigationObserver extends AutoRouterObserver {
ref.invalidate(getAllPeopleProvider);
}
if (route.name == 'SharingRoute') {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
Future(() => ref.read(assetProvider.notifier).getAllAsset());
}
if (route.name == 'LibraryRoute') {
ref.read(albumProvider.notifier).getAllAlbums();
}
if (route.name == 'HomeRoute') {
ref.invalidate(memoryFutureProvider);
Future(() => ref.read(assetProvider.notifier).getAllAsset());

View File

@ -16,6 +16,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
@ -152,7 +153,7 @@ class AlbumService {
/// Checks remote albums (owned if `isShared` is false) for changes,
/// updates the local database and returns `true` if there were any changes
Future<bool> refreshRemoteAlbums({required bool isShared}) async {
Future<bool> refreshRemoteAlbums() async {
if (!_remoteCompleter.isCompleted) {
// guard against concurrent calls
return _remoteCompleter.future;
@ -162,12 +163,21 @@ class AlbumService {
bool changes = false;
try {
await _userService.refreshUsers();
final List<Album> serverAlbums =
await _albumApiRepository.getAll(shared: isShared ? true : null);
changes = await _syncService.syncRemoteAlbumsToDb(
serverAlbums,
isShared: isShared,
final List<Album> sharedAlbum =
await _albumApiRepository.getAll(shared: true);
final List<Album> ownedAlbum =
await _albumApiRepository.getAll(shared: null);
final albums = HashSet<Album>(
equals: (a, b) => a.remoteId == b.remoteId,
hashCode: (a) => a.remoteId.hashCode,
);
albums.addAll(sharedAlbum);
albums.addAll(ownedAlbum);
changes = await _syncService.syncRemoteAlbumsToDb(albums.toList());
} finally {
_remoteCompleter.complete(changes);
}
@ -213,9 +223,9 @@ class AlbumService {
);
}
Future<AlbumAddAssetsResponse?> addAdditionalAssetToAlbum(
Iterable<Asset> assets,
Future<AlbumAddAssetsResponse?> addAssets(
Album album,
Iterable<Asset> assets,
) async {
try {
final result = await _albumApiRepository.addAssets(
@ -234,7 +244,7 @@ class AlbumService {
successfullyAdded: addedAssets.length,
);
} catch (e) {
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
debugPrint("Error addAssets ${e.toString()}");
}
return null;
}
@ -253,30 +263,14 @@ class AlbumService {
await _albumRepository.update(album);
});
Future<bool> addAdditionalUserToAlbum(
List<String> sharedUserIds,
Album album,
) async {
try {
final updatedAlbum =
await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds);
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
await _albumRepository.update(updatedAlbum);
return true;
} catch (e) {
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
}
return false;
}
Future<bool> setActivityEnabled(Album album, bool enabled) async {
Future<bool> setActivityStatus(Album album, bool enabled) async {
try {
final updatedAlbum = await _albumApiRepository.update(
album.remoteId!,
activityEnabled: enabled,
);
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
await _albumRepository.update(updatedAlbum);
album.activityEnabled = updatedAlbum.activityEnabled;
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error setActivityEnabled ${e.toString()}");
@ -327,7 +321,7 @@ class AlbumService {
}
}
Future<bool> removeAssetFromAlbum(
Future<bool> removeAsset(
Album album,
Iterable<Asset> assets,
) async {
@ -346,7 +340,7 @@ class AlbumService {
return false;
}
Future<bool> removeUserFromAlbum(
Future<bool> removeUser(
Album album,
User user,
) async {
@ -363,22 +357,44 @@ class AlbumService {
await _albumRepository.update(a!);
return true;
} catch (e) {
debugPrint("Error removeUserFromAlbum ${e.toString()}");
} catch (error) {
debugPrint("Error removeUser ${error.toString()}");
return false;
}
}
Future<bool> addUsers(
Album album,
List<String> userIds,
) async {
try {
final updatedAlbum =
await _albumApiRepository.addUsers(album.remoteId!, userIds);
album.sharedUsers.addAll(updatedAlbum.remoteUsers);
album.shared = true;
await _albumRepository.addUsers(album, album.sharedUsers.toList());
await _albumRepository.update(album);
return true;
} catch (error) {
debugPrint("Error addUsers ${error.toString()}");
}
return false;
}
Future<bool> changeTitleAlbum(
Album album,
String newAlbumTitle,
) async {
try {
album = await _albumApiRepository.update(
final updatedAlbum = await _albumApiRepository.update(
album.remoteId!,
name: newAlbumTitle,
);
await _entityService.fillAlbumWithDatabaseEntities(album);
album.name = updatedAlbum.name;
await _albumRepository.update(album);
return true;
} catch (e) {
@ -405,4 +421,15 @@ class AlbumService {
}
}
}
Future<List<Album>> getAll() async {
return _albumRepository.getAll(remote: true);
}
Future<List<Album>> search(
String searchTerm,
QuickFilterMode filterMode,
) async {
return _albumRepository.search(searchTerm, filterMode);
}
}

View File

@ -32,6 +32,7 @@ class EntityService {
.getByIds(album.remoteUsers.map((user) => user.id).toList());
album.sharedUsers.clear();
album.sharedUsers.addAll(users);
album.shared = true;
}
if (album.remoteAssets.isNotEmpty) {
// replace all assets with assets from database

View File

@ -95,10 +95,9 @@ class SyncService {
/// Syncs remote albums to the database
/// returns `true` if there were any changes
Future<bool> syncRemoteAlbumsToDb(
List<Album> remote, {
required bool isShared,
}) =>
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
List<Album> remote,
) =>
_lock.run(() => _syncRemoteAlbumsToDb(remote));
/// Syncs all device albums and their assets to the database
/// Returns `true` if there were any changes
@ -310,17 +309,14 @@ class SyncService {
/// returns `true` if there were any changes
Future<bool> _syncRemoteAlbumsToDb(
List<Album> remoteAlbums,
bool isShared,
) async {
remoteAlbums.sortBy((e) => e.remoteId!);
final User me = await _userRepository.me();
final List<Album> dbAlbums = await _albumRepository.getAll(
remote: true,
shared: isShared ? true : null,
ownerId: isShared ? null : me.isarId,
sortBy: AlbumSort.remoteId,
);
final List<Asset> toDelete = [];
final List<Asset> existing = [];
@ -335,7 +331,7 @@ class SyncService {
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
);
if (isShared && toDelete.isNotEmpty) {
if (toDelete.isNotEmpty) {
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
if (idsToRemove.isNotEmpty) {
await _assetRepository.deleteById(idsToRemove);

View File

@ -190,17 +190,14 @@ ThemeData getThemeData({required ColorScheme colorScheme}) {
displayLarge: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : primaryColor,
),
displayMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
displaySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: primaryColor,
),
titleSmall: const TextStyle(
fontSize: 16.0,
@ -241,7 +238,7 @@ ThemeData getThemeData({required ColorScheme colorScheme}) {
isDark ? colorScheme.surfaceContainer : colorScheme.surface,
labelTextStyle: const WidgetStatePropertyAll(
TextStyle(
fontSize: 13,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),

View File

@ -5,7 +5,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/services/album.service.dart';
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
import 'package:immich_mobile/routing/router.dart';
@ -27,13 +26,11 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
final albumService = ref.watch(albumServiceProvider);
final sharedAlbums = ref.watch(sharedAlbumProvider);
useEffect(
() {
// Fetch album updates, e.g., cover image
ref.read(albumProvider.notifier).getAllAlbums();
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.read(albumProvider.notifier).refreshRemoteAlbums();
return null;
},
@ -41,9 +38,9 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
);
void addToAlbum(Album album) async {
final result = await albumService.addAdditionalAssetToAlbum(
assets,
final result = await albumService.addAssets(
album,
assets,
);
if (result != null) {
@ -107,8 +104,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
onPressed: () {
context.pushRoute(
CreateAlbumRoute(
isSharedAlbum: false,
initialAssets: assets,
assets: assets,
),
);
},
@ -123,7 +119,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: AddToAlbumSliverList(
albums: albums,
sharedAlbums: sharedAlbums,
sharedAlbums: albums.where((a) => a.shared).toList(),
onAddToAlbum: addToAlbum,
),
),

View File

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

View File

@ -7,7 +7,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_viewer.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/routing/router.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@ -46,10 +45,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
final bool success;
if (album.shared) {
success =
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
context
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
} else {
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
context
@ -113,11 +110,10 @@ class AlbumViewerAppbar extends HookConsumerWidget
isProcessing.value = true;
bool isSuccess =
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
await ref.watch(albumProvider.notifier).leaveAlbum(album);
if (isSuccess) {
context
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
} else {
context.pop();
ImmichToast.show(

View File

@ -4,7 +4,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/widgets/album/add_to_album_sliverlist.dart';
import 'package:immich_mobile/models/asset_selection_state.dart';
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
@ -72,7 +71,8 @@ class ControlBottomAppBar extends HookConsumerWidget {
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
final sharedAlbums = ref.watch(sharedAlbumProvider);
final sharedAlbums =
ref.watch(albumProvider).where((a) => a.shared).toList();
const bottomPadding = 0.20;
final scrollController = useDraggableScrollController();

View File

@ -9,7 +9,6 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/collection_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/services/album.service.dart';
import 'package:immich_mobile/services/stack.service.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
@ -272,10 +271,9 @@ class MultiselectGrid extends HookConsumerWidget {
if (assets.isEmpty) {
return;
}
final result =
await ref.read(albumServiceProvider).addAdditionalAssetToAlbum(
assets,
final result = await ref.read(albumServiceProvider).addAssets(
album,
assets,
);
if (result != null) {
@ -323,8 +321,7 @@ class MultiselectGrid extends HookConsumerWidget {
.createAlbumWithGeneratedName(assets);
if (result != null) {
ref.watch(albumProvider.notifier).getAllAlbums();
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
selectionEnabledHook.value = false;
context.pushRoute(AlbumViewerRoute(albumId: result.id));

View File

@ -6,8 +6,8 @@ import 'package:flutter/material.dart';
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/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/providers/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
@ -230,9 +230,7 @@ class BottomGalleryBar extends ConsumerWidget {
handleRemoveFromAlbum() async {
final album = ref.read(currentAlbumProvider);
final bool isSuccess = album != null &&
await ref
.read(sharedAlbumProvider.notifier)
.removeAssetFromAlbum(album, [asset]);
await ref.read(albumProvider.notifier).removeAsset(album, [asset]);
if (isSuccess) {
// Workaround for asset remaining in the gallery

View File

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

View File

@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
populateTestLoginInfo1() {
usernameController.text = 'testuser@email.com';
passwordController.text = 'password';
serverEndpointController.text = 'http://192.168.1.16:2283/api';
serverEndpointController.text = 'http://192.168.1.118:2283/api';
}
login() async {

View File

@ -1,48 +0,0 @@
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/routing/router.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/widgets/common/user_avatar.dart';
class PartnerList extends HookConsumerWidget {
const PartnerList({super.key, required this.partner});
final List<User> partner;
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverList(
delegate:
SliverChildBuilderDelegate(listEntry, childCount: partner.length),
);
}
Widget listEntry(BuildContext context, int index) {
final User p = partner[index];
return ListTile(
contentPadding: const EdgeInsets.only(
left: 12.0,
right: 18.0,
),
leading: userAvatar(context, p, radius: 24),
title: Text(
"partner_list_user_photos",
style: context.textTheme.labelLarge,
).tr(
namedArgs: {
'user': p.name,
},
),
trailing: Text(
"partner_list_view_all",
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
),
).tr(),
onTap: () => context.pushRoute((PartnerDetailRoute(partner: p))),
);
}
}

View File

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

View File

@ -79,25 +79,35 @@ void main() {
verifyNoMoreInteractions(syncService);
});
});
group('refreshRemoteAlbums', () {
test('isShared: false', () async {
test('is working', () async {
when(() => userService.refreshUsers()).thenAnswer((_) async => true);
when(() => albumApiRepository.getAll(shared: true))
.thenAnswer((_) async => [AlbumStub.sharedWithUser]);
when(() => albumApiRepository.getAll(shared: null))
.thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
when(
() => syncService.syncRemoteAlbumsToDb(
[AlbumStub.oneAsset, AlbumStub.twoAsset],
isShared: false,
),
() => syncService.syncRemoteAlbumsToDb([
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
]),
).thenAnswer((_) async => true);
final result = await sut.refreshRemoteAlbums(isShared: false);
final result = await sut.refreshRemoteAlbums();
expect(result, true);
verify(() => userService.refreshUsers()).called(1);
verify(() => albumApiRepository.getAll(shared: true)).called(1);
verify(() => albumApiRepository.getAll(shared: null)).called(1);
verify(
() => syncService.syncRemoteAlbumsToDb(
[AlbumStub.oneAsset, AlbumStub.twoAsset],
isShared: false,
[
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
],
),
).called(1);
verifyNoMoreInteractions(userService);
@ -166,9 +176,9 @@ void main() {
() => albumRepository.update(AlbumStub.oneAsset),
).thenAnswer((_) async => AlbumStub.oneAsset);
final result = await sut.addAdditionalAssetToAlbum(
[AssetStub.image1, AssetStub.image2],
final result = await sut.addAssets(
AlbumStub.oneAsset,
[AssetStub.image1, AssetStub.image2],
);
expect(result != null, true);
@ -185,18 +195,23 @@ void main() {
).thenAnswer(
(_) async => AlbumStub.sharedWithUser,
);
when(
() => entityService
.fillAlbumWithDatabaseEntities(AlbumStub.sharedWithUser),
).thenAnswer((_) async => AlbumStub.sharedWithUser);
when(
() => albumRepository.update(AlbumStub.sharedWithUser),
).thenAnswer((_) async => AlbumStub.sharedWithUser);
final result = await sut.addAdditionalUserToAlbum(
[UserStub.user2.id],
when(
() => albumRepository.addUsers(
AlbumStub.emptyAlbum,
AlbumStub.emptyAlbum.sharedUsers.toList(),
),
).thenAnswer((_) async => AlbumStub.emptyAlbum);
when(
() => albumRepository.update(AlbumStub.emptyAlbum),
).thenAnswer((_) async => AlbumStub.emptyAlbum);
final result = await sut.addUsers(
AlbumStub.emptyAlbum,
[UserStub.user2.id],
);
expect(result, true);
});
});