mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
feat(mobile): new mobile UI (#12582)
This commit is contained in:
parent
b59abdff3d
commit
e9813315e7
56 changed files with 1861 additions and 1230 deletions
|
@ -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_back": "Back",
|
||||||
"action_common_cancel": "Cancel",
|
"action_common_cancel": "Cancel",
|
||||||
"action_common_clear": "Clear",
|
"action_common_clear": "Clear",
|
||||||
|
@ -353,7 +373,6 @@
|
||||||
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
||||||
"notification_permission_list_tile_title": "Notification Permission",
|
"notification_permission_list_tile_title": "Notification Permission",
|
||||||
"partner_list_user_photos": "{user}'s photos",
|
"partner_list_user_photos": "{user}'s photos",
|
||||||
"partner_list_view_all": "View all",
|
|
||||||
"partner_page_add_partner": "Add partner",
|
"partner_page_add_partner": "Add partner",
|
||||||
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
|
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
|
||||||
"partner_page_no_more_users": "No more users to add",
|
"partner_page_no_more_users": "No more users to add",
|
||||||
|
@ -362,7 +381,6 @@
|
||||||
"partner_page_shared_to_title": "Shared to",
|
"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_content": "{} will no longer be able to access your photos.",
|
||||||
"partner_page_stop_sharing_title": "Stop sharing your photos?",
|
"partner_page_stop_sharing_title": "Stop sharing your photos?",
|
||||||
"partner_page_title": "Partner",
|
|
||||||
"permission_onboarding_back": "Back",
|
"permission_onboarding_back": "Back",
|
||||||
"permission_onboarding_continue_anyway": "Continue anyway",
|
"permission_onboarding_continue_anyway": "Continue anyway",
|
||||||
"permission_onboarding_get_started": "Get started",
|
"permission_onboarding_get_started": "Get started",
|
||||||
|
|
|
@ -3,7 +3,7 @@ PODS:
|
||||||
- Flutter
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift
|
- FlutterMacOS
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- DKImagePickerController/Core (4.3.9):
|
- DKImagePickerController/Core (4.3.9):
|
||||||
|
@ -77,7 +77,6 @@ PODS:
|
||||||
- photo_manager (2.0.0):
|
- photo_manager (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- ReachabilitySwift (5.0.0)
|
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- SDWebImage (5.19.4):
|
- SDWebImage (5.19.4):
|
||||||
- SDWebImage/Core (= 5.19.4)
|
- SDWebImage/Core (= 5.19.4)
|
||||||
|
@ -102,7 +101,7 @@ PODS:
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
- 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`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
@ -133,7 +132,6 @@ SPEC REPOS:
|
||||||
- DKImagePickerController
|
- DKImagePickerController
|
||||||
- DKPhotoGallery
|
- DKPhotoGallery
|
||||||
- MapLibre
|
- MapLibre
|
||||||
- ReachabilitySwift
|
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
|
@ -143,7 +141,7 @@ EXTERNAL SOURCES:
|
||||||
background_downloader:
|
background_downloader:
|
||||||
:path: ".symlinks/plugins/background_downloader/ios"
|
:path: ".symlinks/plugins/background_downloader/ios"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
|
@ -195,8 +193,8 @@ EXTERNAL SOURCES:
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
|
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
|
||||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||||
|
@ -217,7 +215,6 @@ SPEC CHECKSUMS:
|
||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
||||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||||
|
|
|
@ -24,7 +24,10 @@ final Map<ImmichColorPreset, ImmichTheme> _themePresetsMap = {
|
||||||
ImmichColorPreset.indigo: ImmichTheme(
|
ImmichColorPreset.indigo: ImmichTheme(
|
||||||
light: ColorScheme.fromSeed(
|
light: ColorScheme.fromSeed(
|
||||||
seedColor: immichBrandColorLight,
|
seedColor: immichBrandColorLight,
|
||||||
).copyWith(primary: immichBrandColorLight),
|
).copyWith(
|
||||||
|
primary: immichBrandColorLight,
|
||||||
|
onSurface: const Color.fromARGB(255, 34, 31, 32),
|
||||||
|
),
|
||||||
dark: ColorScheme.fromSeed(
|
dark: ColorScheme.fromSeed(
|
||||||
seedColor: immichBrandColorDark,
|
seedColor: immichBrandColorDark,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
|
BIN
mobile/lib/entities/asset.entity.g.dart
generated
BIN
mobile/lib/entities/asset.entity.g.dart
generated
Binary file not shown.
|
@ -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/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/database.interface.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 {
|
abstract interface class IAlbumRepository implements IDatabaseRepository {
|
||||||
Future<Album> create(Album album);
|
Future<Album> create(Album album);
|
||||||
|
@ -38,6 +39,8 @@ abstract interface class IAlbumRepository implements IDatabaseRepository {
|
||||||
Future<void> removeAssets(Album album, List<Asset> assets);
|
Future<void> removeAssets(Album album, List<Asset> assets);
|
||||||
|
|
||||||
Future<Album> recalculateMetadata(Album album);
|
Future<Album> recalculateMetadata(Album album);
|
||||||
|
|
||||||
|
Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AlbumSort { remoteId, localId }
|
enum AlbumSort { remoteId, localId }
|
||||||
|
|
5
mobile/lib/models/albums/album_search.model.dart
Normal file
5
mobile/lib/models/albums/album_search.model.dart
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
enum QuickFilterMode {
|
||||||
|
all,
|
||||||
|
sharedWithMe,
|
||||||
|
myAlbums,
|
||||||
|
}
|
469
mobile/lib/pages/albums/albums.page.dart
Normal file
469
mobile/lib/pages/albums/albums.page.dart
Normal file
|
@ -0,0 +1,469 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class AlbumsPage extends HookConsumerWidget {
|
||||||
|
const AlbumsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albums =
|
||||||
|
ref.watch(albumProvider).where((album) => album.isRemote).toList();
|
||||||
|
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||||
|
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||||
|
final sorted = albumSortOption.sortFn(albums, albumSortIsReverse);
|
||||||
|
final isGrid = useState(false);
|
||||||
|
final searchController = useTextEditingController();
|
||||||
|
final debounceTimer = useRef<Timer?>(null);
|
||||||
|
final filterMode = useState(QuickFilterMode.all);
|
||||||
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
|
final searchFocusNode = useFocusNode();
|
||||||
|
|
||||||
|
toggleViewMode() {
|
||||||
|
isGrid.value = !isGrid.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(String searchTerm, QuickFilterMode mode) {
|
||||||
|
debounceTimer.value?.cancel();
|
||||||
|
debounceTimer.value = Timer(const Duration(milliseconds: 300), () {
|
||||||
|
ref.read(albumProvider.notifier).searchAlbums(searchTerm, mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFilter(QuickFilterMode mode) {
|
||||||
|
filterMode.value = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
searchController.addListener(() {
|
||||||
|
onSearch(searchController.text, filterMode.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () {
|
||||||
|
searchController.removeListener(() {
|
||||||
|
onSearch(searchController.text, filterMode.value);
|
||||||
|
});
|
||||||
|
debounceTimer.value?.cancel();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
clearSearch() {
|
||||||
|
filterMode.value = QuickFilterMode.all;
|
||||||
|
searchController.clear();
|
||||||
|
onSearch('', QuickFilterMode.all);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: ImmichAppBar(
|
||||||
|
showUploadButton: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
onPressed: () => context.pushRoute(
|
||||||
|
CreateAlbumRoute(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: RefreshIndicator(
|
||||||
|
displacement: 70,
|
||||||
|
onRefresh: () async {
|
||||||
|
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(0),
|
||||||
|
width: 0,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.primary.withOpacity(0.075),
|
||||||
|
context.colorScheme.primary.withOpacity(0.09),
|
||||||
|
context.colorScheme.primary.withOpacity(0.075),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
transform: GradientRotation(0.5 * pi),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
autofocus: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.all(16),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceDim,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.surfaceDim,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: context.colorScheme.primary.withAlpha(100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hintText: 'search_albums'.tr(),
|
||||||
|
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
prefixIcon: const Icon(Icons.search_rounded),
|
||||||
|
suffixIcon: searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear_rounded),
|
||||||
|
onPressed: clearSearch,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: (_) =>
|
||||||
|
onSearch(searchController.text, filterMode.value),
|
||||||
|
focusNode: searchFocusNode,
|
||||||
|
onTapOutside: (_) => searchFocusNode.unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
QuickFilterButton(
|
||||||
|
label: 'all'.tr(),
|
||||||
|
isSelected: filterMode.value == QuickFilterMode.all,
|
||||||
|
onTap: () {
|
||||||
|
changeFilter(QuickFilterMode.all);
|
||||||
|
onSearch(searchController.text, QuickFilterMode.all);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
QuickFilterButton(
|
||||||
|
label: 'shared_with_me'.tr(),
|
||||||
|
isSelected: filterMode.value == QuickFilterMode.sharedWithMe,
|
||||||
|
onTap: () {
|
||||||
|
changeFilter(QuickFilterMode.sharedWithMe);
|
||||||
|
onSearch(
|
||||||
|
searchController.text,
|
||||||
|
QuickFilterMode.sharedWithMe,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
QuickFilterButton(
|
||||||
|
label: 'my_albums'.tr(),
|
||||||
|
isSelected: filterMode.value == QuickFilterMode.myAlbums,
|
||||||
|
onTap: () {
|
||||||
|
changeFilter(QuickFilterMode.myAlbums);
|
||||||
|
onSearch(
|
||||||
|
searchController.text,
|
||||||
|
QuickFilterMode.myAlbums,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const SortButton(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isGrid.value
|
||||||
|
? Icons.view_list_outlined
|
||||||
|
: Icons.grid_view_outlined,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onPressed: toggleViewMode,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
child: isGrid.value
|
||||||
|
? GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 250,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
childAspectRatio: .7,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return AlbumThumbnailCard(
|
||||||
|
album: sorted[index],
|
||||||
|
onTap: () => context.pushRoute(
|
||||||
|
AlbumViewerRoute(albumId: sorted[index].id),
|
||||||
|
),
|
||||||
|
showOwner: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: sorted.length,
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: sorted.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: LargeLeadingTile(
|
||||||
|
title: Text(
|
||||||
|
sorted[index].name,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: sorted[index].ownerId == userId
|
||||||
|
? Text(
|
||||||
|
'${sorted[index].assetCount} items',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style:
|
||||||
|
context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context
|
||||||
|
.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: sorted[index].ownerName != null
|
||||||
|
? Text(
|
||||||
|
'${sorted[index].assetCount} items • ${'album_thumbnail_shared_by'.tr(
|
||||||
|
args: [
|
||||||
|
sorted[index].ownerName!,
|
||||||
|
],
|
||||||
|
)}',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: context
|
||||||
|
.colorScheme.onSurfaceSecondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: () => context.pushRoute(
|
||||||
|
AlbumViewerRoute(albumId: sorted[index].id),
|
||||||
|
),
|
||||||
|
leadingPadding: const EdgeInsets.only(
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(15),
|
||||||
|
),
|
||||||
|
child: ImmichThumbnail(
|
||||||
|
asset: sorted[index].thumbnail.value,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// minVerticalPadding: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuickFilterButton extends StatelessWidget {
|
||||||
|
const QuickFilterButton({
|
||||||
|
super.key,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: onTap,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
isSelected ? context.colorScheme.primary : Colors.transparent,
|
||||||
|
),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(25),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortButton extends ConsumerWidget {
|
||||||
|
const SortButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
||||||
|
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
||||||
|
|
||||||
|
return MenuAnchor(
|
||||||
|
style: MenuStyle(
|
||||||
|
elevation: WidgetStatePropertyAll(1),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.all(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
consumeOutsideTap: true,
|
||||||
|
menuChildren: AlbumSortMode.values
|
||||||
|
.map(
|
||||||
|
(mode) => MenuItemButton(
|
||||||
|
leadingIcon: albumSortOption == mode
|
||||||
|
? albumSortIsReverse
|
||||||
|
? Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
color: albumSortOption == mode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.keyboard_arrow_up_rounded,
|
||||||
|
color: albumSortOption == mode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
: const Icon(Icons.abc, color: Colors.transparent),
|
||||||
|
onPressed: () {
|
||||||
|
final selected = albumSortOption == mode;
|
||||||
|
// Switch direction
|
||||||
|
if (selected) {
|
||||||
|
ref
|
||||||
|
.read(albumSortOrderProvider.notifier)
|
||||||
|
.changeSortDirection(!albumSortIsReverse);
|
||||||
|
} else {
|
||||||
|
ref
|
||||||
|
.read(albumSortByOptionsProvider.notifier)
|
||||||
|
.changeSortMode(mode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(
|
||||||
|
const EdgeInsets.fromLTRB(16, 16, 32, 16),
|
||||||
|
),
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
albumSortOption == mode
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
shape: WidgetStateProperty.all(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
mode.label.tr(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: albumSortOption == mode
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurface.withAlpha(185),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
builder: (context, controller, child) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (controller.isOpen) {
|
||||||
|
controller.close();
|
||||||
|
} else {
|
||||||
|
controller.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: 90 * pi / 180,
|
||||||
|
child: Icon(
|
||||||
|
Icons.compare_arrows_rounded,
|
||||||
|
size: 18,
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(225),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
albumSortOption.label.tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(225),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -151,7 +151,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
|
|
||||||
handleSyncAlbumToggle(bool isEnable) async {
|
handleSyncAlbumToggle(bool isEnable) async {
|
||||||
if (isEnable) {
|
if (isEnable) {
|
||||||
await ref.read(albumProvider.notifier).getAllAlbums();
|
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
for (final album in selectedBackupAlbums) {
|
for (final album in selectedBackupAlbums) {
|
||||||
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,7 +212,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
.read(backupProvider.notifier)
|
.read(backupProvider.notifier)
|
||||||
.backupAlbumSelectionDone();
|
.backupAlbumSelectionDone();
|
||||||
// waited until backup albums are stored in DB
|
// waited until backup albums are stored in DB
|
||||||
ref.read(albumProvider.notifier).getDeviceAlbums();
|
ref.read(albumProvider.notifier).refreshDeviceAlbums();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"backup_controller_page_select",
|
"backup_controller_page_select",
|
||||||
|
|
|
@ -6,7 +6,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_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/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
@ -45,11 +45,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final isSuccess =
|
final isSuccess =
|
||||||
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
|
await ref.read(albumProvider.notifier).leaveAlbum(album);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
context.navigateTo(
|
context.navigateTo(
|
||||||
const TabControllerRoute(children: [SharingRoute()]),
|
TabControllerRoute(children: [AlbumsRoute()]),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showErrorMessage();
|
showErrorMessage();
|
||||||
|
@ -65,9 +65,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ref
|
await ref.read(albumProvider.notifier).removeUser(album, user);
|
||||||
.read(sharedAlbumProvider.notifier)
|
|
||||||
.removeUserFromAlbum(album, user);
|
|
||||||
album.sharedUsers.remove(user);
|
album.sharedUsers.remove(user);
|
||||||
sharedUsers.value = album.sharedUsers.toList();
|
sharedUsers.value = album.sharedUsers.toList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -200,8 +198,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
activityEnabled.value = value;
|
activityEnabled.value = value;
|
||||||
if (await ref
|
if (await ref
|
||||||
.read(sharedAlbumProvider.notifier)
|
.read(albumProvider.notifier)
|
||||||
.setActivityEnabled(album, value)) {
|
.setActivitystatus(album, value)) {
|
||||||
album.activityEnabled = value;
|
album.activityEnabled = value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_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/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/providers/album/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
@ -25,20 +25,15 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||||
final suggestedShareUsers = ref.watch(otherUsersProvider);
|
final suggestedShareUsers = ref.watch(otherUsersProvider);
|
||||||
|
|
||||||
createSharedAlbum() async {
|
createSharedAlbum() async {
|
||||||
var newAlbum =
|
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||||
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
|
|
||||||
ref.watch(albumTitleProvider),
|
ref.watch(albumTitleProvider),
|
||||||
assets,
|
assets,
|
||||||
sharedUsersList.value,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newAlbum != null) {
|
if (newAlbum != null) {
|
||||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
context.maybePop(true);
|
context.maybePop(true);
|
||||||
context
|
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScaffoldMessenger(
|
ScaffoldMessenger(
|
||||||
|
|
|
@ -11,9 +11,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/current_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/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_action_filled_button.dart';
|
||||||
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
|
@ -50,9 +48,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
||||||
final a = album.valueOrNull;
|
final a = album.valueOrNull;
|
||||||
final bool isSuccess = a != null &&
|
final bool isSuccess = a != null &&
|
||||||
await ref
|
await ref.read(albumProvider.notifier).removeAsset(a, assets);
|
||||||
.read(sharedAlbumProvider.notifier)
|
|
||||||
.removeAssetFromAlbum(a, assets);
|
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
|
@ -81,9 +77,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
// Check if there is new assets add
|
// Check if there is new assets add
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
await ref.watch(albumProvider.notifier).addAssets(
|
||||||
returnPayload.selectedAssets,
|
|
||||||
albumInfo,
|
albumInfo,
|
||||||
|
returnPayload.selectedAssets,
|
||||||
);
|
);
|
||||||
|
|
||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
|
@ -98,9 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
if (sharedUserIds != null) {
|
if (sharedUserIds != null) {
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
await ref
|
await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds);
|
||||||
.watch(albumServiceProvider)
|
|
||||||
.addAdditionalUserToAlbum(sharedUserIds, album);
|
|
||||||
|
|
||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
|
@ -184,7 +178,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSharedUserIconsRow(Album album) {
|
Widget buildSharedUserIconsRow(Album album) {
|
||||||
return GestureDetector(
|
return album.sharedUsers.isNotEmpty
|
||||||
|
? GestureDetector(
|
||||||
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
|
@ -204,7 +199,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
itemCount: album.sharedUsers.length,
|
itemCount: album.sharedUsers.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildHeader(Album album) {
|
Widget buildHeader(Album album) {
|
||||||
|
@ -214,7 +210,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
buildTitle(album),
|
buildTitle(album),
|
||||||
if (album.assets.isNotEmpty == true) buildAlbumDateRange(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(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
album.widgetWhen(
|
album.widgetWhen(
|
||||||
onData: (data) => MultiselectGrid(
|
onData: (albumInfo) => MultiselectGrid(
|
||||||
renderListProvider: albumRenderlistProvider(albumId),
|
renderListProvider: albumRenderlistProvider(albumId),
|
||||||
topWidget: Column(
|
topWidget: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
buildHeader(data),
|
buildHeader(albumInfo),
|
||||||
if (data.isRemote) buildControlButton(data),
|
if (albumInfo.isRemote) buildControlButton(albumInfo),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||||
editEnabled: data.ownerId == userId,
|
editEnabled: albumInfo.ownerId == userId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
|
|
|
@ -17,13 +17,11 @@ import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class CreateAlbumPage extends HookConsumerWidget {
|
class CreateAlbumPage extends HookConsumerWidget {
|
||||||
final bool isSharedAlbum;
|
final List<Asset>? assets;
|
||||||
final List<Asset>? initialAssets;
|
|
||||||
|
|
||||||
const CreateAlbumPage({
|
const CreateAlbumPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.isSharedAlbum,
|
this.assets,
|
||||||
this.initialAssets,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -34,18 +32,9 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||||
final isAlbumTitleTextFieldFocus = useState(false);
|
final isAlbumTitleTextFieldFocus = useState(false);
|
||||||
final isAlbumTitleEmpty = useState(true);
|
final isAlbumTitleEmpty = useState(true);
|
||||||
final selectedAssets = useState<Set<Asset>>(
|
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() {
|
void onBackgroundTapped() {
|
||||||
albumTitleTextFieldFocusNode.unfocus();
|
albumTitleTextFieldFocusNode.unfocus();
|
||||||
isAlbumTitleTextFieldFocus.value = false;
|
isAlbumTitleTextFieldFocus.value = false;
|
||||||
|
@ -199,7 +188,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newAlbum != null) {
|
if (newAlbum != null) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
selectedAssets.value = {};
|
selectedAssets.value = {};
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
|
|
||||||
|
@ -223,22 +212,6 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||||
'share_create_album',
|
'share_create_album',
|
||||||
).tr(),
|
).tr(),
|
||||||
actions: [
|
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(
|
TextButton(
|
||||||
onPressed: albumTitleController.text.isNotEmpty
|
onPressed: albumTitleController.text.isNotEmpty
|
||||||
? createNonSharedAlbum
|
? createNonSharedAlbum
|
||||||
|
|
50
mobile/lib/pages/common/large_leading_tile.dart
Normal file
50
mobile/lib/pages/common/large_leading_tile.dart
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LargeLeadingTile extends StatelessWidget {
|
||||||
|
const LargeLeadingTile({
|
||||||
|
super.key,
|
||||||
|
required this.leading,
|
||||||
|
required this.onTap,
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leadingPadding = const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
this.borderRadius = 20.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget leading;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final Widget title;
|
||||||
|
final Widget? subtitle;
|
||||||
|
final EdgeInsetsGeometry leadingPadding;
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: leadingPadding,
|
||||||
|
child: leading,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.6,
|
||||||
|
child: title,
|
||||||
|
),
|
||||||
|
subtitle ?? const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_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/asset_viewer/scroll_notifier.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/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
@ -16,10 +17,11 @@ class TabControllerPage extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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) {
|
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
||||||
if (!refreshing) return icon;
|
if (!isProcessing) return icon;
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
|
@ -84,15 +86,15 @@ class TabControllerPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
icon: const Icon(Icons.share_rounded),
|
icon: const Icon(Icons.photo_album_outlined),
|
||||||
selectedIcon: const Icon(Icons.share),
|
selectedIcon: const Icon(Icons.photo_album),
|
||||||
label: const Text('tab_controller_nav_sharing').tr(),
|
label: const Text('albums').tr(),
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
icon: const Icon(Icons.photo_album_outlined),
|
icon: const Icon(Icons.space_dashboard_outlined),
|
||||||
selectedIcon: const Icon(Icons.photo_album),
|
selectedIcon: const Icon(Icons.space_dashboard_rounded),
|
||||||
label: const Text('tab_controller_nav_library').tr(),
|
label: const Text('library').tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -118,7 +120,8 @@ class TabControllerPage extends HookConsumerWidget {
|
||||||
Icons.photo_library_outlined,
|
Icons.photo_library_outlined,
|
||||||
),
|
),
|
||||||
selectedIcon: buildIcon(
|
selectedIcon: buildIcon(
|
||||||
Icon(
|
isProcessing: isRefreshingAssets,
|
||||||
|
icon: Icon(
|
||||||
Icons.photo_library,
|
Icons.photo_library,
|
||||||
color: context.primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
|
@ -135,38 +138,42 @@ class TabControllerPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'tab_controller_nav_sharing'.tr(),
|
label: 'albums'.tr(),
|
||||||
icon: const Icon(
|
|
||||||
Icons.group_outlined,
|
|
||||||
),
|
|
||||||
selectedIcon: Icon(
|
|
||||||
Icons.group,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
|
||||||
label: 'tab_controller_nav_library'.tr(),
|
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.photo_album_outlined,
|
Icons.photo_album_outlined,
|
||||||
),
|
),
|
||||||
selectedIcon: buildIcon(
|
selectedIcon: buildIcon(
|
||||||
Icon(
|
isProcessing: isRefreshingRemoteAlbums,
|
||||||
|
icon: Icon(
|
||||||
Icons.photo_album_rounded,
|
Icons.photo_album_rounded,
|
||||||
color: context.primaryColor,
|
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);
|
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: const [
|
routes: [
|
||||||
PhotosRoute(),
|
const PhotosRoute(),
|
||||||
SearchRoute(),
|
SearchInputRoute(),
|
||||||
SharingRoute(),
|
const AlbumsRoute(),
|
||||||
LibraryRoute(),
|
const LibraryRoute(),
|
||||||
],
|
],
|
||||||
duration: const Duration(milliseconds: 600),
|
duration: const Duration(milliseconds: 600),
|
||||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||||
|
|
|
@ -69,7 +69,7 @@ class EditImagePage extends ConsumerWidget {
|
||||||
imageData,
|
imageData,
|
||||||
title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
|
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);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
durationInSecond: 3,
|
durationInSecond: 3,
|
||||||
|
|
|
@ -1,175 +1,401 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/providers/server_info.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/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()
|
@RoutePage()
|
||||||
class LibraryPage extends HookConsumerWidget {
|
class LibraryPage extends ConsumerWidget {
|
||||||
const LibraryPage({super.key});
|
const LibraryPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final trashEnabled =
|
final trashEnabled =
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
final albums = ref.watch(albumProvider);
|
|
||||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
|
||||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
|
||||||
|
|
||||||
useEffect(
|
return Scaffold(
|
||||||
() {
|
appBar: ImmichAppBar(),
|
||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
body: Padding(
|
||||||
return null;
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
},
|
child: ListView(
|
||||||
[],
|
shrinkWrap: true,
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 12.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: Icon(
|
child: Row(
|
||||||
Icons.check,
|
children: [
|
||||||
color:
|
ActionButton(
|
||||||
selected ? context.primaryColor : Colors.transparent,
|
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(
|
const SizedBox(height: 8),
|
||||||
option.label.tr(),
|
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(
|
style: TextStyle(
|
||||||
color: selected ? context.primaryColor : null,
|
fontWeight: FontWeight.w500,
|
||||||
fontSize: 14.0,
|
|
||||||
),
|
),
|
||||||
|
).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() {
|
class PeopleCollectionCard extends ConsumerWidget {
|
||||||
return LayoutBuilder(
|
const PeopleCollectionCard({super.key});
|
||||||
builder: (context, constraints) {
|
|
||||||
var cardSize = constraints.maxWidth;
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final people = ref.watch(getAllPeopleProvider);
|
||||||
|
final size = MediaQuery.of(context).size.width * 0.5 - 20;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () =>
|
onTap: () => context.pushRoute(const PeopleCollectionRoute()),
|
||||||
context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)),
|
|
||||||
child: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.only(bottom: 32), // Adjust padding to suit
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: cardSize,
|
height: size,
|
||||||
height: cardSize,
|
width: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surfaceContainer,
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderRadius: const BorderRadius.all(Radius.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(
|
child: people.widgetWhen(
|
||||||
Icons.add_rounded,
|
onData: (people) {
|
||||||
size: 28,
|
return GridView.count(
|
||||||
color: context.primaryColor,
|
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(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.all(8.0),
|
||||||
top: 8.0,
|
|
||||||
bottom: 16,
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'library_page_new_album',
|
'places'.tr(),
|
||||||
style: context.textTheme.labelLarge?.copyWith(
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
color: context.colorScheme.onSurface,
|
color: context.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
).tr(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildLibraryNavButton(
|
class ActionButton extends StatelessWidget {
|
||||||
String label,
|
final VoidCallback onPressed;
|
||||||
IconData icon,
|
final IconData icon;
|
||||||
Function() onClick,
|
final String label;
|
||||||
) {
|
|
||||||
|
const ActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: onClick,
|
onPressed: onPressed,
|
||||||
label: Padding(
|
label: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: context.colorScheme.onSurface,
|
color: context.colorScheme.onSurface,
|
||||||
|
fontSize: 15,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
backgroundColor: context.colorScheme.surfaceContainer,
|
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
borderRadius: const BorderRadius.all(Radius.circular(25)),
|
||||||
|
side: BorderSide(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
icon: Icon(
|
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
55
mobile/lib/pages/library/local_albums.page.dart
Normal file
55
mobile/lib/pages/library/local_albums.page.dart
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class LocalAlbumsPage extends HookConsumerWidget {
|
||||||
|
const LocalAlbumsPage({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albums = ref.watch(localAlbumsProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('on_this_device'.tr()),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(18.0),
|
||||||
|
itemCount: albums.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: LargeLeadingTile(
|
||||||
|
leadingPadding: const EdgeInsets.only(
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
child: ImmichThumbnail(
|
||||||
|
asset: albums[index].thumbnail.value,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
albums[index].name,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text('${albums[index].assetCount} items'),
|
||||||
|
onTap: () => context
|
||||||
|
.pushRoute(AlbumViewerRoute(albumId: albums[index].id)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,12 +86,10 @@ class PartnerPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||||
child: const Text(
|
child: Text(
|
||||||
"partner_page_shared_to_title",
|
"partner_page_shared_to_title",
|
||||||
style: TextStyle(
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
fontSize: 14,
|
color: context.colorScheme.onSurface.withAlpha(200),
|
||||||
color: Colors.grey,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
@ -104,10 +102,7 @@ class PartnerPage extends HookConsumerWidget {
|
||||||
leading: userAvatar(context, users[index]),
|
leading: userAvatar(context, users[index]),
|
||||||
title: Text(
|
title: Text(
|
||||||
users[index].email,
|
users[index].email,
|
||||||
style: const TextStyle(
|
style: context.textTheme.bodyLarge,
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.person_remove),
|
icon: const Icon(Icons.person_remove),
|
||||||
|
@ -148,7 +143,7 @@ class PartnerPage extends HookConsumerWidget {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("partner_page_title").tr(),
|
title: const Text("partners").tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
import 'package:immich_mobile/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
@ -22,7 +23,11 @@ class PartnerDetailPage extends HookConsumerWidget {
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
Future.microtask(
|
||||||
|
() async => {
|
||||||
|
await ref.read(assetProvider.notifier).getAllAsset(),
|
||||||
|
},
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
@ -64,19 +69,47 @@ class PartnerDetailPage extends HookConsumerWidget {
|
||||||
title: Text(partner.name),
|
title: Text(partner.name),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
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(
|
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),
|
renderListProvider: assetsProvider(partner.isarId),
|
||||||
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
|
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
|
||||||
deleteEnabled: false,
|
deleteEnabled: false,
|
104
mobile/lib/pages/library/people/people_collection.page.dart
Normal file
104
mobile/lib/pages/library/people/people_collection.page.dart
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class PeopleCollectionPage extends HookConsumerWidget {
|
||||||
|
const PeopleCollectionPage({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final people = ref.watch(getAllPeopleProvider);
|
||||||
|
final headers = ApiService.getRequestHeaders();
|
||||||
|
|
||||||
|
showNameEditModel(
|
||||||
|
String personId,
|
||||||
|
String personName,
|
||||||
|
) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return PersonNameEditForm(personId: personId, personName: personName);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('people'.tr()),
|
||||||
|
),
|
||||||
|
body: people.when(
|
||||||
|
data: (people) {
|
||||||
|
return GridView.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
childAspectRatio: 0.85,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
|
itemCount: people.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final person = people[index];
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.pushRoute(
|
||||||
|
PersonResultRoute(
|
||||||
|
personId: person.id,
|
||||||
|
personName: person.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Material(
|
||||||
|
shape: const CircleBorder(side: BorderSide.none),
|
||||||
|
elevation: 3,
|
||||||
|
child: CircleAvatar(
|
||||||
|
maxRadius: 96 / 2,
|
||||||
|
backgroundImage: NetworkImage(
|
||||||
|
getFaceThumbnailUrl(person.id),
|
||||||
|
headers: headers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => showNameEditModel(person.id, person.name),
|
||||||
|
child: person.name.isEmpty
|
||||||
|
? Text(
|
||||||
|
'add_a_name'.tr(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
person.name,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, stack) => const Text("error"),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
125
mobile/lib/pages/library/places/places_collection.part.dart
Normal file
125
mobile/lib/pages/library/places/places_collection.part.dart
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class PlacesCollectionPage extends HookConsumerWidget {
|
||||||
|
const PlacesCollectionPage({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final places = ref.watch(getAllPlacesProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('places'.tr()),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 200,
|
||||||
|
width: context.width,
|
||||||
|
child: MapThumbnail(
|
||||||
|
onTap: (_, __) => context.pushRoute(const MapRoute()),
|
||||||
|
zoom: 8,
|
||||||
|
centre: const LatLng(
|
||||||
|
21.44950,
|
||||||
|
-157.91959,
|
||||||
|
),
|
||||||
|
showAttribution: false,
|
||||||
|
themeMode:
|
||||||
|
context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
places.when(
|
||||||
|
data: (places) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: places.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final place = places[index];
|
||||||
|
|
||||||
|
return PlaceTile(id: place.id, name: place.label);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, stask) => const Text('Error getting places'),
|
||||||
|
loading: () => Center(child: const CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaceTile extends StatelessWidget {
|
||||||
|
const PlaceTile({super.key, required this.id, required this.name});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final thumbnailUrl =
|
||||||
|
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail';
|
||||||
|
|
||||||
|
void navigateToPlace() {
|
||||||
|
context.pushRoute(
|
||||||
|
SearchInputRoute(
|
||||||
|
prefilter: SearchFilter(
|
||||||
|
people: {},
|
||||||
|
location: SearchLocationFilter(
|
||||||
|
city: name,
|
||||||
|
),
|
||||||
|
camera: SearchCameraFilter(),
|
||||||
|
date: SearchDateFilter(),
|
||||||
|
display: SearchDisplayFilters(
|
||||||
|
isNotInAlbum: false,
|
||||||
|
isArchive: false,
|
||||||
|
isFavorite: false,
|
||||||
|
),
|
||||||
|
mediaType: AssetType.other,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LargeLeadingTile(
|
||||||
|
onTap: () => navigateToPlace(),
|
||||||
|
title: Text(
|
||||||
|
name,
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
imageUrl: thumbnailUrl,
|
||||||
|
httpHeaders: ApiService.getRequestHeaders(),
|
||||||
|
errorWidget: (context, url, error) =>
|
||||||
|
const Icon(Icons.image_not_supported_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_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.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
|
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
|
@ -33,8 +32,7 @@ class PhotosPage extends HookConsumerWidget {
|
||||||
() {
|
() {
|
||||||
ref.read(websocketProvider.notifier).connect();
|
ref.read(websocketProvider.notifier).connect();
|
||||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums());
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
ref.read(serverInfoProvider.notifier).getServerInfo();
|
ref.read(serverInfoProvider.notifier).getServerInfo();
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|
|
@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget {
|
||||||
Text(
|
Text(
|
||||||
name.value,
|
name.value,
|
||||||
style: context.textTheme.titleLarge,
|
style: context.textTheme.titleLarge,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -125,10 +126,12 @@ class PersonResultPage extends HookConsumerWidget {
|
||||||
headers: ApiService.getRequestHeaders(),
|
headers: ApiService.getRequestHeaders(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Expanded(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||||
child: buildTitleBlock(),
|
child: buildTitleBlock(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,25 +1,11 @@
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_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/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/immich_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/scaffold_error_body.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
|
@ -28,12 +14,6 @@ class SearchPage extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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(
|
TextStyle categoryTitleStyle = const TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
fontSize: 15.0,
|
fontSize: 15.0,
|
||||||
|
@ -41,87 +21,6 @@ class SearchPage extends HookConsumerWidget {
|
||||||
|
|
||||||
Color categoryIconColor = context.colorScheme.onSurface;
|
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() {
|
buildSearchButton() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -165,20 +64,17 @@ class SearchPage extends HookConsumerWidget {
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
buildSearchButton(),
|
buildSearchButton(),
|
||||||
const SizedBox(height: 8.0),
|
|
||||||
buildPeople(),
|
|
||||||
const SizedBox(height: 8.0),
|
|
||||||
buildPlaces(),
|
|
||||||
const SizedBox(height: 24.0),
|
const SizedBox(height: 24.0),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'search_page_your_activity',
|
'search_page_categories',
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12.0),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Icons.favorite_border_rounded,
|
Icons.favorite_border_rounded,
|
||||||
|
@ -200,16 +96,7 @@ class SearchPage extends HookConsumerWidget {
|
||||||
).tr(),
|
).tr(),
|
||||||
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24.0),
|
const CategoryDivider(),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Text(
|
|
||||||
'search_page_categories',
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('search_page_videos', style: categoryTitleStyle).tr(),
|
title: Text('search_page_videos', style: categoryTitleStyle).tr(),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
|
|
|
@ -31,6 +31,7 @@ class SearchInputPage extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isContextualSearch = useState(true);
|
final isContextualSearch = useState(true);
|
||||||
final textSearchController = useTextEditingController();
|
final textSearchController = useTextEditingController();
|
||||||
|
final focusNode = useFocusNode();
|
||||||
final filter = useState<SearchFilter>(
|
final filter = useState<SearchFilter>(
|
||||||
SearchFilter(
|
SearchFilter(
|
||||||
people: prefilter?.people ?? {},
|
people: prefilter?.people ?? {},
|
||||||
|
@ -440,6 +441,10 @@ class SearchInputPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTextSubmitted(String value) {
|
handleTextSubmitted(String value) {
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isContextualSearch.value) {
|
if (isContextualSearch.value) {
|
||||||
filter.value = filter.value.copyWith(
|
filter.value = filter.value.copyWith(
|
||||||
context: value,
|
context: value,
|
||||||
|
@ -489,7 +494,9 @@ class SearchInputPage extends HookConsumerWidget {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 14.0),
|
||||||
|
child: IconButton(
|
||||||
icon: isContextualSearch.value
|
icon: isContextualSearch.value
|
||||||
? const Icon(Icons.abc_rounded)
|
? const Icon(Icons.abc_rounded)
|
||||||
: const Icon(Icons.image_search_rounded),
|
: const Icon(Icons.image_search_rounded),
|
||||||
|
@ -498,14 +505,35 @@ class SearchInputPage extends HookConsumerWidget {
|
||||||
textSearchController.clear();
|
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,
|
controller: textSearchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.all(8),
|
||||||
|
prefixIcon: prefilter != null
|
||||||
|
? null
|
||||||
|
: Icon(
|
||||||
|
Icons.search_rounded,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
hintText: isContextualSearch.value
|
hintText: isContextualSearch.value
|
||||||
? 'contextual_search'.tr()
|
? 'contextual_search'.tr()
|
||||||
: 'filename_search'.tr(),
|
: 'filename_search'.tr(),
|
||||||
|
@ -513,14 +541,35 @@ class SearchInputPage extends HookConsumerWidget {
|
||||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
enabledBorder: const UnderlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
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,
|
onSubmitted: handleTextSubmitted,
|
||||||
|
focusNode: focusNode,
|
||||||
|
onTapOutside: (_) => focusNode.unfocus(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
|
|
|
@ -1,283 +0,0 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
|
||||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/partner/partner_list.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class SharingPage extends HookConsumerWidget {
|
|
||||||
const SharingPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final albumSortOption = ref.watch(albumSortByOptionsProvider);
|
|
||||||
final albumSortIsReverse = ref.watch(albumSortOrderProvider);
|
|
||||||
final albums = ref.watch(sharedAlbumProvider);
|
|
||||||
final sharedAlbums = albumSortOption.sortFn(albums, albumSortIsReverse);
|
|
||||||
final userId = ref.watch(currentUserProvider)?.id;
|
|
||||||
final partner = ref.watch(partnerSharedWithProvider);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
buildAlbumGrid() {
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(18.0),
|
|
||||||
sliver: SliverGrid(
|
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 250,
|
|
||||||
mainAxisSpacing: 12,
|
|
||||||
crossAxisSpacing: 12,
|
|
||||||
childAspectRatio: .7,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) {
|
|
||||||
return AlbumThumbnailCard(
|
|
||||||
album: sharedAlbums[index],
|
|
||||||
showOwner: true,
|
|
||||||
onTap: () => context.pushRoute(
|
|
||||||
AlbumViewerRoute(albumId: sharedAlbums[index].id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: sharedAlbums.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildAlbumList() {
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) {
|
|
||||||
final album = sharedAlbums[index];
|
|
||||||
final isOwner = album.ownerId == userId;
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: ImmichThumbnail(
|
|
||||||
asset: album.thumbnail.value,
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
album.name,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.colorScheme.onSurface,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: isOwner
|
|
||||||
? Text(
|
|
||||||
'album_thumbnail_owned'.tr(),
|
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: album.ownerName != null
|
|
||||||
? Text(
|
|
||||||
'album_thumbnail_shared_by'
|
|
||||||
.tr(args: [album.ownerName!]),
|
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
onTap: () => context
|
|
||||||
.pushRoute(AlbumViewerRoute(albumId: sharedAlbums[index].id)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: sharedAlbums.length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTopBottons() {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 12.0,
|
|
||||||
right: 12.0,
|
|
||||||
top: 24.0,
|
|
||||||
bottom: 12.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () =>
|
|
||||||
context.pushRoute(CreateAlbumRoute(isSharedAlbum: true)),
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.photo_album_outlined,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
"sharing_silver_appbar_create_shared_album",
|
|
||||||
maxLines: 1,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12.0),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => context.pushRoute(const SharedLinkRoute()),
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.link,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
"sharing_silver_appbar_shared_links",
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildEmptyListIndication() {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Card(
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
|
||||||
side: BorderSide(
|
|
||||||
color: context.isDarkTheme
|
|
||||||
? const Color(0xFF383838)
|
|
||||||
: Colors.black12,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(18.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 5.0, bottom: 5),
|
|
||||||
child: Icon(
|
|
||||||
Icons.insert_photo_rounded,
|
|
||||||
size: 50,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
'sharing_page_empty_list',
|
|
||||||
style: context.textTheme.displaySmall,
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
'sharing_page_description',
|
|
||||||
style: context.textTheme.bodyMedium,
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget sharePartnerButton() {
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => context.pushRoute(const PartnerRoute()),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
|
||||||
child: Icon(
|
|
||||||
Icons.swap_horizontal_circle_rounded,
|
|
||||||
size: 25,
|
|
||||||
semanticLabel: 'partner_page_title'.tr(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
},
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: ImmichAppBar(
|
|
||||||
action: sharePartnerButton(),
|
|
||||||
),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(child: buildTopBottons()),
|
|
||||||
if (partner.isNotEmpty)
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Text(
|
|
||||||
"partner_page_title",
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (partner.isNotEmpty) PartnerList(partner: partner),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Text(
|
|
||||||
"sharing_page_album",
|
|
||||||
style: context.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverLayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
if (sharedAlbums.isEmpty) {
|
|
||||||
return buildEmptyListIndication();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constraints.crossAxisExtent < 600) {
|
|
||||||
return buildAlbumList();
|
|
||||||
} else {
|
|
||||||
return buildAlbumGrid();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +1,21 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
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/services/album.service.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.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/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/album.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/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
import 'package:immich_mobile/utils/renderlist_generator.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
|
||||||
|
|
||||||
class AlbumNotifier extends StateNotifier<List<Album>> {
|
class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
AlbumNotifier(this._albumService, Isar db) : super([]) {
|
AlbumNotifier(this._albumService, this.db, this.ref) : super([]) {
|
||||||
final query = db.albums
|
final query = db.albums.filter().remoteIdIsNotNull();
|
||||||
.filter()
|
|
||||||
.owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
|
|
||||||
query.findAll().then((value) {
|
query.findAll().then((value) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
state = value;
|
state = value;
|
||||||
|
@ -25,14 +25,22 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
|
final Isar db;
|
||||||
|
final Ref ref;
|
||||||
late final StreamSubscription<List<Album>> _streamSub;
|
late final StreamSubscription<List<Album>> _streamSub;
|
||||||
|
|
||||||
Future<void> getAllAlbums() => Future.wait([
|
Future<void> refreshRemoteAlbums() async {
|
||||||
_albumService.refreshDeviceAlbums(),
|
final isRefresing =
|
||||||
_albumService.refreshRemoteAlbums(isShared: false),
|
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);
|
Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
|
||||||
|
|
||||||
|
@ -59,6 +67,50 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
await createAlbum(albumName, {});
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_streamSub.cancel();
|
_streamSub.cancel();
|
||||||
|
@ -71,6 +123,7 @@ final albumProvider =
|
||||||
return AlbumNotifier(
|
return AlbumNotifier(
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -94,3 +147,31 @@ final albumRenderlistProvider =
|
||||||
}
|
}
|
||||||
return const Stream.empty();
|
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));
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.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/services/album.service.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
|
||||||
|
@ -40,7 +39,6 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
|
||||||
);
|
|
||||||
});
|
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
|
@ -58,11 +57,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
_ref.read(assetProvider.notifier).getAllAsset();
|
_ref.read(assetProvider.notifier).getAllAsset();
|
||||||
case TabEnum.search:
|
case TabEnum.search:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
case TabEnum.sharing:
|
case TabEnum.albums:
|
||||||
_ref.read(assetProvider.notifier).getAllAsset();
|
_ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
_ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
case TabEnum.library:
|
case TabEnum.library:
|
||||||
_ref.read(albumProvider.notifier).getAllAlbums();
|
// nothing to do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_udid/flutter_udid.dart';
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
|
import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
@ -115,7 +114,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
Store.delete(StoreKey.accessToken),
|
Store.delete(StoreKey.accessToken),
|
||||||
]);
|
]);
|
||||||
_ref.invalidate(albumProvider);
|
_ref.invalidate(albumProvider);
|
||||||
_ref.invalidate(sharedAlbumProvider);
|
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
|
|
Binary file not shown.
|
@ -1,11 +1,6 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
enum TabEnum {
|
enum TabEnum { home, search, albums, library }
|
||||||
home,
|
|
||||||
search,
|
|
||||||
sharing,
|
|
||||||
library,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provides the currently active tab
|
/// Provides the currently active tab
|
||||||
final tabProvider = StateProvider<TabEnum>(
|
final tabProvider = StateProvider<TabEnum>(
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.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/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/album.interface.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/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
@ -118,4 +120,33 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||||
@override
|
@override
|
||||||
Future<void> deleteAllLocal() =>
|
Future<void> deleteAllLocal() =>
|
||||||
txn(() => db.albums.where().localIdIsNotNull().deleteAll());
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class PartnerApiRepository extends ApiRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> delete(String id) => checkNull(_api.removePartner(id));
|
Future<void> delete(String id) => _api.removePartner(id);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<User> update(String id, {required bool inTimeline}) async {
|
Future<User> update(String id, {required bool inTimeline}) async {
|
||||||
|
|
|
@ -13,6 +13,11 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/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/activities.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
||||||
|
@ -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/editing/filter.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/archive.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/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/library/trash.page.dart';
|
||||||
import 'package:immich_mobile/pages/login/change_password.page.dart';
|
import 'package:immich_mobile/pages/login/change_password.page.dart';
|
||||||
import 'package:immich_mobile/pages/login/login.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/recently_added.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search.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/search/search_input.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/partner/partner.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link.page.dart';
|
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart';
|
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
|
||||||
import 'package:immich_mobile/pages/sharing/sharing.page.dart';
|
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
|
@ -103,17 +106,18 @@ class AppRouter extends RootStackRouter {
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: SearchRoute.page,
|
page: SearchInputRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
|
||||||
),
|
|
||||||
AutoRoute(
|
|
||||||
page: SharingRoute.page,
|
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
maintainState: false,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: LibraryRoute.page,
|
page: LibraryRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: AlbumsRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||||
),
|
),
|
||||||
|
@ -137,7 +141,11 @@ class AppRouter extends RootStackRouter {
|
||||||
AutoRoute(page: EditImageRoute.page),
|
AutoRoute(page: EditImageRoute.page),
|
||||||
AutoRoute(page: CropImageRoute.page),
|
AutoRoute(page: CropImageRoute.page),
|
||||||
AutoRoute(page: FilterImageRoute.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: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: AllMotionPhotosRoute.page,
|
page: AllMotionPhotosRoute.page,
|
||||||
|
@ -183,8 +191,16 @@ class AppRouter extends RootStackRouter {
|
||||||
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
|
CustomRoute(
|
||||||
AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
|
page: ArchiveRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: PartnerRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: PartnerDetailRoute.page,
|
page: PartnerDetailRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
@ -200,10 +216,15 @@ class AppRouter extends RootStackRouter {
|
||||||
page: AlbumOptionsRoute.page,
|
page: AlbumOptionsRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]),
|
CustomRoute(
|
||||||
AutoRoute(
|
page: TrashRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
page: SharedLinkRoute.page,
|
page: SharedLinkRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: SharedLinkEditRoute.page,
|
page: SharedLinkEditRoute.page,
|
||||||
|
@ -232,6 +253,26 @@ class AppRouter extends RootStackRouter {
|
||||||
page: HeaderSettingsRoute.page,
|
page: HeaderSettingsRoute.page,
|
||||||
guards: [_duplicateGuard],
|
guards: [_duplicateGuard],
|
||||||
),
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: PeopleCollectionRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: 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,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
/// generated route for
|
||||||
/// [AllMotionPhotosPage]
|
/// [AllMotionPhotosPage]
|
||||||
class AllMotionPhotosRoute extends PageRouteInfo<void> {
|
class AllMotionPhotosRoute extends PageRouteInfo<void> {
|
||||||
|
@ -560,15 +579,13 @@ class ChangePasswordRoute extends PageRouteInfo<void> {
|
||||||
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
||||||
CreateAlbumRoute({
|
CreateAlbumRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required bool isSharedAlbum,
|
List<Asset>? assets,
|
||||||
List<Asset>? initialAssets,
|
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
CreateAlbumRoute.name,
|
CreateAlbumRoute.name,
|
||||||
args: CreateAlbumRouteArgs(
|
args: CreateAlbumRouteArgs(
|
||||||
key: key,
|
key: key,
|
||||||
isSharedAlbum: isSharedAlbum,
|
assets: assets,
|
||||||
initialAssets: initialAssets,
|
|
||||||
),
|
),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
|
@ -578,11 +595,11 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
||||||
static PageInfo page = PageInfo(
|
static PageInfo page = PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
final args = data.argsAs<CreateAlbumRouteArgs>();
|
final args = data.argsAs<CreateAlbumRouteArgs>(
|
||||||
|
orElse: () => const CreateAlbumRouteArgs());
|
||||||
return CreateAlbumPage(
|
return CreateAlbumPage(
|
||||||
key: args.key,
|
key: args.key,
|
||||||
isSharedAlbum: args.isSharedAlbum,
|
assets: args.assets,
|
||||||
initialAssets: args.initialAssets,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -591,19 +608,16 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
|
||||||
class CreateAlbumRouteArgs {
|
class CreateAlbumRouteArgs {
|
||||||
const CreateAlbumRouteArgs({
|
const CreateAlbumRouteArgs({
|
||||||
this.key,
|
this.key,
|
||||||
required this.isSharedAlbum,
|
this.assets,
|
||||||
this.initialAssets,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final bool isSharedAlbum;
|
final List<Asset>? assets;
|
||||||
|
|
||||||
final List<Asset>? initialAssets;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
/// generated route for
|
||||||
/// [LoginPage]
|
/// [LoginPage]
|
||||||
class LoginRoute extends PageRouteInfo<void> {
|
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
|
/// generated route for
|
||||||
/// [PermissionOnboardingPage]
|
/// [PermissionOnboardingPage]
|
||||||
class PermissionOnboardingRoute extends PageRouteInfo<void> {
|
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
|
/// generated route for
|
||||||
/// [RecentlyAddedPage]
|
/// [RecentlyAddedPage]
|
||||||
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
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
|
/// generated route for
|
||||||
/// [SplashScreenPage]
|
/// [SplashScreenPage]
|
||||||
class SplashScreenRoute extends PageRouteInfo<void> {
|
class SplashScreenRoute extends PageRouteInfo<void> {
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/providers/search/search_page_state.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/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
@ -21,14 +19,6 @@ class TabNavigationObserver extends AutoRouterObserver {
|
||||||
required this.ref,
|
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
|
@override
|
||||||
Future<void> didChangeTabRoute(
|
Future<void> didChangeTabRoute(
|
||||||
TabPageRoute route,
|
TabPageRoute route,
|
||||||
|
@ -41,15 +31,6 @@ class TabNavigationObserver extends AutoRouterObserver {
|
||||||
ref.invalidate(getAllPeopleProvider);
|
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') {
|
if (route.name == 'HomeRoute') {
|
||||||
ref.invalidate(memoryFutureProvider);
|
ref.invalidate(memoryFutureProvider);
|
||||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||||
|
|
|
@ -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/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.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.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.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,
|
/// Checks remote albums (owned if `isShared` is false) for changes,
|
||||||
/// updates the local database and returns `true` if there were any 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) {
|
if (!_remoteCompleter.isCompleted) {
|
||||||
// guard against concurrent calls
|
// guard against concurrent calls
|
||||||
return _remoteCompleter.future;
|
return _remoteCompleter.future;
|
||||||
|
@ -162,12 +163,21 @@ class AlbumService {
|
||||||
bool changes = false;
|
bool changes = false;
|
||||||
try {
|
try {
|
||||||
await _userService.refreshUsers();
|
await _userService.refreshUsers();
|
||||||
final List<Album> serverAlbums =
|
final List<Album> sharedAlbum =
|
||||||
await _albumApiRepository.getAll(shared: isShared ? true : null);
|
await _albumApiRepository.getAll(shared: true);
|
||||||
changes = await _syncService.syncRemoteAlbumsToDb(
|
|
||||||
serverAlbums,
|
final List<Album> ownedAlbum =
|
||||||
isShared: isShared,
|
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 {
|
} finally {
|
||||||
_remoteCompleter.complete(changes);
|
_remoteCompleter.complete(changes);
|
||||||
}
|
}
|
||||||
|
@ -213,9 +223,9 @@ class AlbumService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AlbumAddAssetsResponse?> addAdditionalAssetToAlbum(
|
Future<AlbumAddAssetsResponse?> addAssets(
|
||||||
Iterable<Asset> assets,
|
|
||||||
Album album,
|
Album album,
|
||||||
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final result = await _albumApiRepository.addAssets(
|
final result = await _albumApiRepository.addAssets(
|
||||||
|
@ -234,7 +244,7 @@ class AlbumService {
|
||||||
successfullyAdded: addedAssets.length,
|
successfullyAdded: addedAssets.length,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
debugPrint("Error addAssets ${e.toString()}");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -253,30 +263,14 @@ class AlbumService {
|
||||||
await _albumRepository.update(album);
|
await _albumRepository.update(album);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<bool> addAdditionalUserToAlbum(
|
Future<bool> setActivityStatus(Album album, bool enabled) async {
|
||||||
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 {
|
|
||||||
try {
|
try {
|
||||||
final updatedAlbum = await _albumApiRepository.update(
|
final updatedAlbum = await _albumApiRepository.update(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
activityEnabled: enabled,
|
activityEnabled: enabled,
|
||||||
);
|
);
|
||||||
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
album.activityEnabled = updatedAlbum.activityEnabled;
|
||||||
await _albumRepository.update(updatedAlbum);
|
await _albumRepository.update(album);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error setActivityEnabled ${e.toString()}");
|
debugPrint("Error setActivityEnabled ${e.toString()}");
|
||||||
|
@ -327,7 +321,7 @@ class AlbumService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(
|
Future<bool> removeAsset(
|
||||||
Album album,
|
Album album,
|
||||||
Iterable<Asset> assets,
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
|
@ -346,7 +340,7 @@ class AlbumService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeUserFromAlbum(
|
Future<bool> removeUser(
|
||||||
Album album,
|
Album album,
|
||||||
User user,
|
User user,
|
||||||
) async {
|
) async {
|
||||||
|
@ -363,22 +357,44 @@ class AlbumService {
|
||||||
await _albumRepository.update(a!);
|
await _albumRepository.update(a!);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
debugPrint("Error removeUserFromAlbum ${e.toString()}");
|
debugPrint("Error removeUser ${error.toString()}");
|
||||||
return false;
|
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(
|
Future<bool> changeTitleAlbum(
|
||||||
Album album,
|
Album album,
|
||||||
String newAlbumTitle,
|
String newAlbumTitle,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
album = await _albumApiRepository.update(
|
final updatedAlbum = await _albumApiRepository.update(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
name: newAlbumTitle,
|
name: newAlbumTitle,
|
||||||
);
|
);
|
||||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
|
||||||
|
album.name = updatedAlbum.name;
|
||||||
await _albumRepository.update(album);
|
await _albumRepository.update(album);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ class EntityService {
|
||||||
.getByIds(album.remoteUsers.map((user) => user.id).toList());
|
.getByIds(album.remoteUsers.map((user) => user.id).toList());
|
||||||
album.sharedUsers.clear();
|
album.sharedUsers.clear();
|
||||||
album.sharedUsers.addAll(users);
|
album.sharedUsers.addAll(users);
|
||||||
|
album.shared = true;
|
||||||
}
|
}
|
||||||
if (album.remoteAssets.isNotEmpty) {
|
if (album.remoteAssets.isNotEmpty) {
|
||||||
// replace all assets with assets from database
|
// replace all assets with assets from database
|
||||||
|
|
|
@ -95,10 +95,9 @@ class SyncService {
|
||||||
/// Syncs remote albums to the database
|
/// Syncs remote albums to the database
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> syncRemoteAlbumsToDb(
|
Future<bool> syncRemoteAlbumsToDb(
|
||||||
List<Album> remote, {
|
List<Album> remote,
|
||||||
required bool isShared,
|
) =>
|
||||||
}) =>
|
_lock.run(() => _syncRemoteAlbumsToDb(remote));
|
||||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
|
|
||||||
|
|
||||||
/// Syncs all device albums and their assets to the database
|
/// Syncs all device albums and their assets to the database
|
||||||
/// Returns `true` if there were any changes
|
/// Returns `true` if there were any changes
|
||||||
|
@ -310,17 +309,14 @@ class SyncService {
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> _syncRemoteAlbumsToDb(
|
Future<bool> _syncRemoteAlbumsToDb(
|
||||||
List<Album> remoteAlbums,
|
List<Album> remoteAlbums,
|
||||||
bool isShared,
|
|
||||||
) async {
|
) async {
|
||||||
remoteAlbums.sortBy((e) => e.remoteId!);
|
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||||
|
|
||||||
final User me = await _userRepository.me();
|
|
||||||
final List<Album> dbAlbums = await _albumRepository.getAll(
|
final List<Album> dbAlbums = await _albumRepository.getAll(
|
||||||
remote: true,
|
remote: true,
|
||||||
shared: isShared ? true : null,
|
|
||||||
ownerId: isShared ? null : me.isarId,
|
|
||||||
sortBy: AlbumSort.remoteId,
|
sortBy: AlbumSort.remoteId,
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<Asset> toDelete = [];
|
final List<Asset> toDelete = [];
|
||||||
final List<Asset> existing = [];
|
final List<Asset> existing = [];
|
||||||
|
|
||||||
|
@ -335,7 +331,7 @@ class SyncService {
|
||||||
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
|
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isShared && toDelete.isNotEmpty) {
|
if (toDelete.isNotEmpty) {
|
||||||
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
|
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
|
||||||
if (idsToRemove.isNotEmpty) {
|
if (idsToRemove.isNotEmpty) {
|
||||||
await _assetRepository.deleteById(idsToRemove);
|
await _assetRepository.deleteById(idsToRemove);
|
||||||
|
|
|
@ -190,17 +190,14 @@ ThemeData getThemeData({required ColorScheme colorScheme}) {
|
||||||
displayLarge: TextStyle(
|
displayLarge: TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isDark ? Colors.white : primaryColor,
|
|
||||||
),
|
),
|
||||||
displayMedium: TextStyle(
|
displayMedium: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isDark ? Colors.white : Colors.black87,
|
|
||||||
),
|
),
|
||||||
displaySmall: TextStyle(
|
displaySmall: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: primaryColor,
|
|
||||||
),
|
),
|
||||||
titleSmall: const TextStyle(
|
titleSmall: const TextStyle(
|
||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
|
@ -241,7 +238,7 @@ ThemeData getThemeData({required ColorScheme colorScheme}) {
|
||||||
isDark ? colorScheme.surfaceContainer : colorScheme.surface,
|
isDark ? colorScheme.surfaceContainer : colorScheme.surface,
|
||||||
labelTextStyle: const WidgetStatePropertyAll(
|
labelTextStyle: const WidgetStatePropertyAll(
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_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.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/album.service.dart';
|
||||||
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
@ -27,13 +26,11 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||||
final albumService = ref.watch(albumServiceProvider);
|
final albumService = ref.watch(albumServiceProvider);
|
||||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
// Fetch album updates, e.g., cover image
|
// Fetch album updates, e.g., cover image
|
||||||
ref.read(albumProvider.notifier).getAllAlbums();
|
ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
@ -41,9 +38,9 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
void addToAlbum(Album album) async {
|
void addToAlbum(Album album) async {
|
||||||
final result = await albumService.addAdditionalAssetToAlbum(
|
final result = await albumService.addAssets(
|
||||||
assets,
|
|
||||||
album,
|
album,
|
||||||
|
assets,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
@ -107,8 +104,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushRoute(
|
context.pushRoute(
|
||||||
CreateAlbumRoute(
|
CreateAlbumRoute(
|
||||||
isSharedAlbum: false,
|
assets: assets,
|
||||||
initialAssets: assets,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -123,7 +119,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
sliver: AddToAlbumSliverList(
|
sliver: AddToAlbumSliverList(
|
||||||
albums: albums,
|
albums: albums,
|
||||||
sharedAlbums: sharedAlbums,
|
sharedAlbums: albums.where((a) => a.shared).toList(),
|
||||||
onAddToAlbum: addToAlbum,
|
onAddToAlbum: addToAlbum,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -12,12 +12,14 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||||
/// Whether or not to show the owner of the album (or "Owned")
|
/// Whether or not to show the owner of the album (or "Owned")
|
||||||
/// in the subtitle of the album
|
/// in the subtitle of the album
|
||||||
final bool showOwner;
|
final bool showOwner;
|
||||||
|
final bool showTitle;
|
||||||
|
|
||||||
const AlbumThumbnailCard({
|
const AlbumThumbnailCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.album,
|
required this.album,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.showOwner = false,
|
this.showOwner = false,
|
||||||
|
this.showTitle = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Album album;
|
final Album album;
|
||||||
|
@ -76,7 +78,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||||
: 'album_thumbnail_card_items'
|
: 'album_thumbnail_card_items'
|
||||||
.tr(args: ['${album.assetCount}']),
|
.tr(args: ['${album.assetCount}']),
|
||||||
),
|
),
|
||||||
if (owner != null) const TextSpan(text: ' · '),
|
if (owner != null) const TextSpan(text: ' • '),
|
||||||
if (owner != null) TextSpan(text: owner),
|
if (owner != null) TextSpan(text: owner),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -102,6 +104,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||||
: buildAlbumThumbnail(),
|
: buildAlbumThumbnail(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (showTitle) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
@ -109,7 +112,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
album.name,
|
album.name,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
color: context.colorScheme.onSurface,
|
color: context.colorScheme.onSurface,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
|
@ -118,6 +121,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
buildAlbumTextRow(),
|
buildAlbumTextRow(),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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/activity_statistics.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_viewer.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/utils/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
@ -46,10 +45,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
|
|
||||||
final bool success;
|
final bool success;
|
||||||
if (album.shared) {
|
if (album.shared) {
|
||||||
success =
|
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||||
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
|
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||||
context
|
|
||||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
|
||||||
} else {
|
} else {
|
||||||
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||||
context
|
context
|
||||||
|
@ -113,11 +110,10 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
|
await ref.watch(albumProvider.notifier).leaveAlbum(album);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
context
|
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
|
||||||
} else {
|
} else {
|
||||||
context.pop();
|
context.pop();
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_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.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/widgets/album/add_to_album_sliverlist.dart';
|
||||||
import 'package:immich_mobile/models/asset_selection_state.dart';
|
import 'package:immich_mobile/models/asset_selection_state.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
|
@ -72,7 +71,8 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
||||||
final trashEnabled =
|
final trashEnabled =
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
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;
|
const bottomPadding = 0.20;
|
||||||
final scrollController = useDraggableScrollController();
|
final scrollController = useDraggableScrollController();
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/stack.service.dart';
|
import 'package:immich_mobile/services/stack.service.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
|
@ -272,10 +271,9 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||||
if (assets.isEmpty) {
|
if (assets.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final result =
|
final result = await ref.read(albumServiceProvider).addAssets(
|
||||||
await ref.read(albumServiceProvider).addAdditionalAssetToAlbum(
|
|
||||||
assets,
|
|
||||||
album,
|
album,
|
||||||
|
assets,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
@ -323,8 +321,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||||
.createAlbumWithGeneratedName(assets);
|
.createAlbumWithGeneratedName(assets);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
|
|
||||||
context.pushRoute(AlbumViewerRoute(albumId: result.id));
|
context.pushRoute(AlbumViewerRoute(albumId: result.id));
|
||||||
|
|
|
@ -6,8 +6,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_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/current_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/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||||
|
@ -230,9 +230,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||||
handleRemoveFromAlbum() async {
|
handleRemoveFromAlbum() async {
|
||||||
final album = ref.read(currentAlbumProvider);
|
final album = ref.read(currentAlbumProvider);
|
||||||
final bool isSuccess = album != null &&
|
final bool isSuccess = album != null &&
|
||||||
await ref
|
await ref.read(albumProvider.notifier).removeAsset(album, [asset]);
|
||||||
.read(sharedAlbumProvider.notifier)
|
|
||||||
.removeAssetFromAlbum(album, [asset]);
|
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
// Workaround for asset remaining in the gallery
|
// Workaround for asset remaining in the gallery
|
||||||
|
|
|
@ -18,9 +18,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
final Widget? action;
|
final List<Widget>? actions;
|
||||||
|
final bool showUploadButton;
|
||||||
|
|
||||||
const ImmichAppBar({super.key, this.action});
|
const ImmichAppBar({super.key, this.actions, this.showUploadButton = true});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
@ -184,8 +185,14 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (action != null)
|
if (actions != null)
|
||||||
Padding(padding: const EdgeInsets.only(right: 20), child: action!),
|
...actions!.map(
|
||||||
|
(action) => Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16),
|
||||||
|
child: action,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showUploadButton)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 20),
|
padding: const EdgeInsets.only(right: 20),
|
||||||
child: buildBackupIndicator(),
|
child: buildBackupIndicator(),
|
||||||
|
|
|
@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
|
||||||
populateTestLoginInfo1() {
|
populateTestLoginInfo1() {
|
||||||
usernameController.text = 'testuser@email.com';
|
usernameController.text = 'testuser@email.com';
|
||||||
passwordController.text = 'password';
|
passwordController.text = 'password';
|
||||||
serverEndpointController.text = 'http://192.168.1.16:2283/api';
|
serverEndpointController.text = 'http://192.168.1.118:2283/api';
|
||||||
}
|
}
|
||||||
|
|
||||||
login() async {
|
login() async {
|
||||||
|
|
|
@ -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))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,6 +13,7 @@ class SearchMapThumbnail extends StatelessWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
final double size;
|
final double size;
|
||||||
|
final bool showTitle = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -79,25 +79,35 @@ void main() {
|
||||||
verifyNoMoreInteractions(syncService);
|
verifyNoMoreInteractions(syncService);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('refreshRemoteAlbums', () {
|
group('refreshRemoteAlbums', () {
|
||||||
test('isShared: false', () async {
|
test('is working', () async {
|
||||||
when(() => userService.refreshUsers()).thenAnswer((_) async => true);
|
when(() => userService.refreshUsers()).thenAnswer((_) async => true);
|
||||||
|
when(() => albumApiRepository.getAll(shared: true))
|
||||||
|
.thenAnswer((_) async => [AlbumStub.sharedWithUser]);
|
||||||
|
|
||||||
when(() => albumApiRepository.getAll(shared: null))
|
when(() => albumApiRepository.getAll(shared: null))
|
||||||
.thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
|
.thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => syncService.syncRemoteAlbumsToDb(
|
() => syncService.syncRemoteAlbumsToDb([
|
||||||
[AlbumStub.oneAsset, AlbumStub.twoAsset],
|
AlbumStub.twoAsset,
|
||||||
isShared: false,
|
AlbumStub.oneAsset,
|
||||||
),
|
AlbumStub.sharedWithUser,
|
||||||
|
]),
|
||||||
).thenAnswer((_) async => true);
|
).thenAnswer((_) async => true);
|
||||||
final result = await sut.refreshRemoteAlbums(isShared: false);
|
final result = await sut.refreshRemoteAlbums();
|
||||||
expect(result, true);
|
expect(result, true);
|
||||||
verify(() => userService.refreshUsers()).called(1);
|
verify(() => userService.refreshUsers()).called(1);
|
||||||
|
verify(() => albumApiRepository.getAll(shared: true)).called(1);
|
||||||
verify(() => albumApiRepository.getAll(shared: null)).called(1);
|
verify(() => albumApiRepository.getAll(shared: null)).called(1);
|
||||||
verify(
|
verify(
|
||||||
() => syncService.syncRemoteAlbumsToDb(
|
() => syncService.syncRemoteAlbumsToDb(
|
||||||
[AlbumStub.oneAsset, AlbumStub.twoAsset],
|
[
|
||||||
isShared: false,
|
AlbumStub.twoAsset,
|
||||||
|
AlbumStub.oneAsset,
|
||||||
|
AlbumStub.sharedWithUser,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
).called(1);
|
).called(1);
|
||||||
verifyNoMoreInteractions(userService);
|
verifyNoMoreInteractions(userService);
|
||||||
|
@ -166,9 +176,9 @@ void main() {
|
||||||
() => albumRepository.update(AlbumStub.oneAsset),
|
() => albumRepository.update(AlbumStub.oneAsset),
|
||||||
).thenAnswer((_) async => AlbumStub.oneAsset);
|
).thenAnswer((_) async => AlbumStub.oneAsset);
|
||||||
|
|
||||||
final result = await sut.addAdditionalAssetToAlbum(
|
final result = await sut.addAssets(
|
||||||
[AssetStub.image1, AssetStub.image2],
|
|
||||||
AlbumStub.oneAsset,
|
AlbumStub.oneAsset,
|
||||||
|
[AssetStub.image1, AssetStub.image2],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result != null, true);
|
expect(result != null, true);
|
||||||
|
@ -185,18 +195,23 @@ void main() {
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => AlbumStub.sharedWithUser,
|
(_) 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(
|
when(
|
||||||
[UserStub.user2.id],
|
() => albumRepository.addUsers(
|
||||||
AlbumStub.emptyAlbum,
|
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);
|
expect(result, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue