You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-15 07:14:42 +02:00
feat(mobile): new mobile UI (#12582)
This commit is contained in:
@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@ -27,13 +26,11 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||
final albumService = ref.watch(albumServiceProvider);
|
||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
// Fetch album updates, e.g., cover image
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||
|
||||
return null;
|
||||
},
|
||||
@ -41,9 +38,9 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
void addToAlbum(Album album) async {
|
||||
final result = await albumService.addAdditionalAssetToAlbum(
|
||||
assets,
|
||||
final result = await albumService.addAssets(
|
||||
album,
|
||||
assets,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
@ -107,8 +104,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||
onPressed: () {
|
||||
context.pushRoute(
|
||||
CreateAlbumRoute(
|
||||
isSharedAlbum: false,
|
||||
initialAssets: assets,
|
||||
assets: assets,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -123,7 +119,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: AddToAlbumSliverList(
|
||||
albums: albums,
|
||||
sharedAlbums: sharedAlbums,
|
||||
sharedAlbums: albums.where((a) => a.shared).toList(),
|
||||
onAddToAlbum: addToAlbum,
|
||||
),
|
||||
),
|
||||
|
@ -12,12 +12,14 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
/// Whether or not to show the owner of the album (or "Owned")
|
||||
/// in the subtitle of the album
|
||||
final bool showOwner;
|
||||
final bool showTitle;
|
||||
|
||||
const AlbumThumbnailCard({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
this.showOwner = false,
|
||||
this.showTitle = true,
|
||||
});
|
||||
|
||||
final Album album;
|
||||
@ -76,7 +78,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
: 'album_thumbnail_card_items'
|
||||
.tr(args: ['${album.assetCount}']),
|
||||
),
|
||||
if (owner != null) const TextSpan(text: ' · '),
|
||||
if (owner != null) const TextSpan(text: ' • '),
|
||||
if (owner != null) TextSpan(text: owner),
|
||||
],
|
||||
),
|
||||
@ -102,21 +104,23 @@ class AlbumThumbnailCard extends StatelessWidget {
|
||||
: buildAlbumThumbnail(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: SizedBox(
|
||||
width: cardSize,
|
||||
child: Text(
|
||||
album.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
if (showTitle) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: SizedBox(
|
||||
width: cardSize,
|
||||
child: Text(
|
||||
album.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
@ -46,10 +45,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
|
||||
final bool success;
|
||||
if (album.shared) {
|
||||
success =
|
||||
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
|
||||
context
|
||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
||||
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||
} else {
|
||||
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||
context
|
||||
@ -113,11 +110,10 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
isProcessing.value = true;
|
||||
|
||||
bool isSuccess =
|
||||
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
|
||||
await ref.watch(albumProvider.notifier).leaveAlbum(album);
|
||||
|
||||
if (isSuccess) {
|
||||
context
|
||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
||||
context.navigateTo(TabControllerRoute(children: [AlbumsRoute()]));
|
||||
} else {
|
||||
context.pop();
|
||||
ImmichToast.show(
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
||||
import 'package:immich_mobile/models/asset_selection_state.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||
@ -72,7 +71,8 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
||||
final trashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
final albums = ref.watch(albumProvider).where((a) => a.isRemote).toList();
|
||||
final sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||
final sharedAlbums =
|
||||
ref.watch(albumProvider).where((a) => a.shared).toList();
|
||||
const bottomPadding = 0.20;
|
||||
final scrollController = useDraggableScrollController();
|
||||
|
||||
|
@ -9,7 +9,6 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/stack.service.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
@ -272,11 +271,10 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
if (assets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final result =
|
||||
await ref.read(albumServiceProvider).addAdditionalAssetToAlbum(
|
||||
assets,
|
||||
album,
|
||||
);
|
||||
final result = await ref.read(albumServiceProvider).addAssets(
|
||||
album,
|
||||
assets,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
if (result.alreadyInAlbum.isNotEmpty) {
|
||||
@ -323,8 +321,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
.createAlbumWithGeneratedName(assets);
|
||||
|
||||
if (result != null) {
|
||||
ref.watch(albumProvider.notifier).getAllAlbums();
|
||||
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||
selectionEnabledHook.value = false;
|
||||
|
||||
context.pushRoute(AlbumViewerRoute(albumId: result.id));
|
||||
|
@ -6,8 +6,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
@ -230,9 +230,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
handleRemoveFromAlbum() async {
|
||||
final album = ref.read(currentAlbumProvider);
|
||||
final bool isSuccess = album != null &&
|
||||
await ref
|
||||
.read(sharedAlbumProvider.notifier)
|
||||
.removeAssetFromAlbum(album, [asset]);
|
||||
await ref.read(albumProvider.notifier).removeAsset(album, [asset]);
|
||||
|
||||
if (isSuccess) {
|
||||
// Workaround for asset remaining in the gallery
|
||||
|
@ -18,9 +18,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
final Widget? action;
|
||||
final List<Widget>? actions;
|
||||
final bool showUploadButton;
|
||||
|
||||
const ImmichAppBar({super.key, this.action});
|
||||
const ImmichAppBar({super.key, this.actions, this.showUploadButton = true});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -184,12 +185,18 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (action != null)
|
||||
Padding(padding: const EdgeInsets.only(right: 20), child: action!),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: buildBackupIndicator(),
|
||||
),
|
||||
if (actions != null)
|
||||
...actions!.map(
|
||||
(action) => Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: action,
|
||||
),
|
||||
),
|
||||
if (showUploadButton)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: buildBackupIndicator(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: buildProfileIndicator(),
|
||||
|
@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
populateTestLoginInfo1() {
|
||||
usernameController.text = 'testuser@email.com';
|
||||
passwordController.text = 'password';
|
||||
serverEndpointController.text = 'http://192.168.1.16:2283/api';
|
||||
serverEndpointController.text = 'http://192.168.1.118:2283/api';
|
||||
}
|
||||
|
||||
login() async {
|
||||
|
@ -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 bool showTitle = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
Reference in New Issue
Block a user