mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(mobile): new mobile UI (#12582)
This commit is contained in:
parent
b59abdff3d
commit
e9813315e7
56 changed files with 1960 additions and 1274 deletions
|
|
@ -6,7 +6,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
|
@ -45,11 +45,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||
|
||||
try {
|
||||
final isSuccess =
|
||||
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
|
||||
await ref.read(albumProvider.notifier).leaveAlbum(album);
|
||||
|
||||
if (isSuccess) {
|
||||
context.navigateTo(
|
||||
const TabControllerRoute(children: [SharingRoute()]),
|
||||
TabControllerRoute(children: [AlbumsRoute()]),
|
||||
);
|
||||
} else {
|
||||
showErrorMessage();
|
||||
|
|
@ -65,9 +65,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||
isProcessing.value = true;
|
||||
|
||||
try {
|
||||
await ref
|
||||
.read(sharedAlbumProvider.notifier)
|
||||
.removeUserFromAlbum(album, user);
|
||||
await ref.read(albumProvider.notifier).removeUser(album, user);
|
||||
album.sharedUsers.remove(user);
|
||||
sharedUsers.value = album.sharedUsers.toList();
|
||||
} catch (error) {
|
||||
|
|
@ -200,8 +198,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||
onChanged: (bool value) async {
|
||||
activityEnabled.value = value;
|
||||
if (await ref
|
||||
.read(sharedAlbumProvider.notifier)
|
||||
.setActivityEnabled(album, value)) {
|
||||
.read(albumProvider.notifier)
|
||||
.setActivitystatus(album, value)) {
|
||||
album.activityEnabled = value;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
|
|
@ -25,20 +25,15 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
|||
final suggestedShareUsers = ref.watch(otherUsersProvider);
|
||||
|
||||
createSharedAlbum() async {
|
||||
var newAlbum =
|
||||
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
|
||||
ref.watch(albumTitleProvider),
|
||||
assets,
|
||||
sharedUsersList.value,
|
||||
);
|
||||
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||
ref.watch(albumTitleProvider),
|
||||
assets,
|
||||
);
|
||||
|
||||
if (newAlbum != null) {
|
||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
context.maybePop(true);
|
||||
context
|
||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
||||
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||
}
|
||||
|
||||
ScaffoldMessenger(
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
|
|
@ -50,9 +48,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
|
||||
final a = album.valueOrNull;
|
||||
final bool isSuccess = a != null &&
|
||||
await ref
|
||||
.read(sharedAlbumProvider.notifier)
|
||||
.removeAssetFromAlbum(a, assets);
|
||||
await ref.read(albumProvider.notifier).removeAsset(a, assets);
|
||||
|
||||
if (!isSuccess) {
|
||||
ImmichToast.show(
|
||||
|
|
@ -81,9 +77,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
// Check if there is new assets add
|
||||
isProcessing.value = true;
|
||||
|
||||
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
|
||||
returnPayload.selectedAssets,
|
||||
await ref.watch(albumProvider.notifier).addAssets(
|
||||
albumInfo,
|
||||
returnPayload.selectedAssets,
|
||||
);
|
||||
|
||||
isProcessing.value = false;
|
||||
|
|
@ -98,9 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
if (sharedUserIds != null) {
|
||||
isProcessing.value = true;
|
||||
|
||||
await ref
|
||||
.watch(albumServiceProvider)
|
||||
.addAdditionalUserToAlbum(sharedUserIds, album);
|
||||
await ref.watch(albumProvider.notifier).addUsers(album, sharedUserIds);
|
||||
|
||||
isProcessing.value = false;
|
||||
}
|
||||
|
|
@ -184,27 +178,29 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
Widget buildSharedUserIconsRow(Album album) {
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: ((context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: UserCircleAvatar(
|
||||
user: album.sharedUsers.toList()[index],
|
||||
radius: 18,
|
||||
size: 36,
|
||||
return album.sharedUsers.isNotEmpty
|
||||
? GestureDetector(
|
||||
onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: ((context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: UserCircleAvatar(
|
||||
user: album.sharedUsers.toList()[index],
|
||||
radius: 18,
|
||||
size: 36,
|
||||
),
|
||||
);
|
||||
}),
|
||||
itemCount: album.sharedUsers.length,
|
||||
),
|
||||
);
|
||||
}),
|
||||
itemCount: album.sharedUsers.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget buildHeader(Album album) {
|
||||
|
|
@ -214,7 +210,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
children: [
|
||||
buildTitle(album),
|
||||
if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
|
||||
if (album.shared) buildSharedUserIconsRow(album),
|
||||
buildSharedUserIconsRow(album),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -231,17 +227,17 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
body: Stack(
|
||||
children: [
|
||||
album.widgetWhen(
|
||||
onData: (data) => MultiselectGrid(
|
||||
onData: (albumInfo) => MultiselectGrid(
|
||||
renderListProvider: albumRenderlistProvider(albumId),
|
||||
topWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildHeader(data),
|
||||
if (data.isRemote) buildControlButton(data),
|
||||
buildHeader(albumInfo),
|
||||
if (albumInfo.isRemote) buildControlButton(albumInfo),
|
||||
],
|
||||
),
|
||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||
editEnabled: data.ownerId == userId,
|
||||
editEnabled: albumInfo.ownerId == userId,
|
||||
),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
|||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class CreateAlbumPage extends HookConsumerWidget {
|
||||
final bool isSharedAlbum;
|
||||
final List<Asset>? initialAssets;
|
||||
final List<Asset>? assets;
|
||||
|
||||
const CreateAlbumPage({
|
||||
super.key,
|
||||
required this.isSharedAlbum,
|
||||
this.initialAssets,
|
||||
this.assets,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -34,18 +32,9 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||
final isAlbumTitleTextFieldFocus = useState(false);
|
||||
final isAlbumTitleEmpty = useState(true);
|
||||
final selectedAssets = useState<Set<Asset>>(
|
||||
initialAssets != null ? Set.from(initialAssets!) : const {},
|
||||
assets != null ? Set.from(assets!) : const {},
|
||||
);
|
||||
|
||||
showSelectUserPage() async {
|
||||
final bool? ok = await context.pushRoute<bool?>(
|
||||
AlbumSharedUserSelectionRoute(assets: selectedAssets.value),
|
||||
);
|
||||
if (ok == true) {
|
||||
selectedAssets.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
void onBackgroundTapped() {
|
||||
albumTitleTextFieldFocusNode.unfocus();
|
||||
isAlbumTitleTextFieldFocus.value = false;
|
||||
|
|
@ -199,7 +188,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||
);
|
||||
|
||||
if (newAlbum != null) {
|
||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||
selectedAssets.value = {};
|
||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
|
||||
|
|
@ -223,36 +212,20 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||
'share_create_album',
|
||||
).tr(),
|
||||
actions: [
|
||||
if (isSharedAlbum)
|
||||
TextButton(
|
||||
onPressed: albumTitleController.text.isNotEmpty
|
||||
? showSelectUserPage
|
||||
: null,
|
||||
child: Text(
|
||||
'create_shared_album_page_share'.tr(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: albumTitleController.text.isEmpty
|
||||
? context.themeData.disabledColor
|
||||
: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isSharedAlbum)
|
||||
TextButton(
|
||||
onPressed: albumTitleController.text.isNotEmpty
|
||||
? createNonSharedAlbum
|
||||
: null,
|
||||
child: Text(
|
||||
'create_shared_album_page_create'.tr(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: albumTitleController.text.isNotEmpty
|
||||
? context.primaryColor
|
||||
: context.themeData.disabledColor,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: albumTitleController.text.isNotEmpty
|
||||
? createNonSharedAlbum
|
||||
: null,
|
||||
child: Text(
|
||||
'create_shared_album_page_create'.tr(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: albumTitleController.text.isNotEmpty
|
||||
? context.primaryColor
|
||||
: context.themeData.disabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GestureDetector(
|
||||
|
|
|
|||
50
mobile/lib/pages/common/large_leading_tile.dart
Normal file
50
mobile/lib/pages/common/large_leading_tile.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class LargeLeadingTile extends StatelessWidget {
|
||||
const LargeLeadingTile({
|
||||
super.key,
|
||||
required this.leading,
|
||||
required this.onTap,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.leadingPadding = const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
this.borderRadius = 20.0,
|
||||
});
|
||||
|
||||
final Widget leading;
|
||||
final VoidCallback onTap;
|
||||
final Widget title;
|
||||
final Widget? subtitle;
|
||||
final EdgeInsetsGeometry leadingPadding;
|
||||
final double borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: leadingPadding,
|
||||
child: leading,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
child: title,
|
||||
),
|
||||
subtitle ?? const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
|
@ -16,10 +17,11 @@ class TabControllerPage extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final refreshing = ref.watch(assetProvider);
|
||||
final isRefreshingAssets = ref.watch(assetProvider);
|
||||
final isRefreshingRemoteAlbums = ref.watch(isRefreshingRemoteAlbumProvider);
|
||||
|
||||
Widget buildIcon(Widget icon) {
|
||||
if (!refreshing) return icon;
|
||||
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
||||
if (!isProcessing) return icon;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
|
|
@ -84,15 +86,15 @@ class TabControllerPage extends HookConsumerWidget {
|
|||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
selectedIcon: const Icon(Icons.share),
|
||||
label: const Text('tab_controller_nav_sharing').tr(),
|
||||
icon: const Icon(Icons.photo_album_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_album),
|
||||
label: const Text('albums').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.photo_album_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_album),
|
||||
label: const Text('tab_controller_nav_library').tr(),
|
||||
icon: const Icon(Icons.space_dashboard_outlined),
|
||||
selectedIcon: const Icon(Icons.space_dashboard_rounded),
|
||||
label: const Text('library').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -118,7 +120,8 @@ class TabControllerPage extends HookConsumerWidget {
|
|||
Icons.photo_library_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
isProcessing: isRefreshingAssets,
|
||||
icon: Icon(
|
||||
Icons.photo_library,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
|
|
@ -135,38 +138,42 @@ class TabControllerPage extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_sharing'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.group_outlined,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.group,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_library'.tr(),
|
||||
label: 'albums'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
isProcessing: isRefreshingRemoteAlbums,
|
||||
icon: Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'library'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.space_dashboard_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
isProcessing: isRefreshingAssets,
|
||||
icon: Icon(
|
||||
Icons.space_dashboard_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||
return AutoTabsRouter(
|
||||
routes: const [
|
||||
PhotosRoute(),
|
||||
SearchRoute(),
|
||||
SharingRoute(),
|
||||
LibraryRoute(),
|
||||
routes: [
|
||||
const PhotosRoute(),
|
||||
SearchInputRoute(),
|
||||
const AlbumsRoute(),
|
||||
const LibraryRoute(),
|
||||
],
|
||||
duration: const Duration(milliseconds: 600),
|
||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue