mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 14:47:30 -07:00
refactor: mobile browser and similar photos action
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OpenInBrowserAction extends ActionBuilder {
|
||||
final String remoteId;
|
||||
final TimelineOrigin origin;
|
||||
|
||||
const OpenInBrowserAction({required this.remoteId, required this.origin});
|
||||
|
||||
@override
|
||||
ActionData build(BuildContext context, WidgetRef ref) =>
|
||||
.new(icon: Icons.open_in_browser, label: context.t.open_in_browser, onAction: () => _open(ref));
|
||||
|
||||
Future<void> _open(WidgetRef ref) async {
|
||||
final serverEndpoint = ref.read(storeServiceProvider).get(.serverEndpoint).replaceFirst('/api', '');
|
||||
final url = Uri.parse('$serverEndpoint${webPathFor(origin)}/photos/$remoteId');
|
||||
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: .externalApplication);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
String webPathFor(TimelineOrigin origin) => switch (origin) {
|
||||
.favorite => '/favorites',
|
||||
.trash => '/trash',
|
||||
.archive => '/archive',
|
||||
_ => '',
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SetProfilePictureAction extends ActionBuilder {
|
||||
final BaseAsset asset;
|
||||
|
||||
const SetProfilePictureAction({required this.asset});
|
||||
|
||||
@override
|
||||
ActionData build(BuildContext context, WidgetRef ref) => .new(
|
||||
icon: Icons.account_circle_outlined,
|
||||
label: context.t.set_as_profile_picture,
|
||||
onAction: () async => unawaited(context.pushRoute(ProfilePictureCropRoute(asset: asset))),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SimilarPhotosAction extends ActionBuilder {
|
||||
final String assetId;
|
||||
|
||||
const SimilarPhotosAction({required this.assetId});
|
||||
|
||||
@override
|
||||
ActionData build(BuildContext context, WidgetRef ref) =>
|
||||
.new(icon: Icons.compare, label: context.t.view_similar_photos, onAction: () => _search(context, ref));
|
||||
|
||||
Future<void> _search(BuildContext context, WidgetRef ref) async {
|
||||
ref.invalidate(assetViewerProvider);
|
||||
ref.invalidate(paginatedSearchProvider);
|
||||
|
||||
ref.read(searchPreFilterProvider.notifier)
|
||||
..clear()
|
||||
..setFilter(
|
||||
.new(
|
||||
assetId: assetId,
|
||||
people: {},
|
||||
location: .new(),
|
||||
camera: .new(),
|
||||
date: .new(),
|
||||
display: .new(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
rating: .new(),
|
||||
mediaType: .other,
|
||||
),
|
||||
);
|
||||
|
||||
unawaited(context.navigateTo(const DriftSearchRoute()));
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OpenInBrowserActionButton extends ConsumerWidget {
|
||||
final String remoteId;
|
||||
final TimelineOrigin origin;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const OpenInBrowserActionButton({
|
||||
super.key,
|
||||
required this.remoteId,
|
||||
required this.origin,
|
||||
this.iconOnly = false,
|
||||
this.menuItem = false,
|
||||
});
|
||||
|
||||
void _onTap() async {
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', '');
|
||||
|
||||
String originPath = '';
|
||||
switch (origin) {
|
||||
case TimelineOrigin.favorite:
|
||||
originPath = '/favorites';
|
||||
break;
|
||||
case TimelineOrigin.trash:
|
||||
originPath = '/trash';
|
||||
break;
|
||||
case TimelineOrigin.archive:
|
||||
originPath = '/archive';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
final url = '$serverEndpoint$originPath/photos/$remoteId';
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
label: 'open_in_browser'.t(context: context),
|
||||
iconData: Icons.open_in_browser,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: _onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SetProfilePictureActionButton extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.pushRoute(ProfilePictureCropRoute(asset: asset));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.account_circle_outlined,
|
||||
label: "set_as_profile_picture".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context),
|
||||
maxWidth: 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SimilarPhotosActionButton extends ConsumerWidget {
|
||||
final String assetId;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const SimilarPhotosActionButton({super.key, required this.assetId, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.invalidate(assetViewerProvider);
|
||||
ref.invalidate(paginatedSearchProvider);
|
||||
|
||||
ref.read(searchPreFilterProvider.notifier)
|
||||
..clear()
|
||||
..setFilter(
|
||||
SearchFilter(
|
||||
assetId: assetId,
|
||||
people: {},
|
||||
location: SearchLocationFilter(),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
|
||||
rating: SearchRatingFilter(),
|
||||
mediaType: AssetType.other,
|
||||
),
|
||||
);
|
||||
|
||||
unawaited(context.navigateTo(const DriftSearchRoute()));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.compare,
|
||||
label: "view_similar_photos".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
maxWidth: 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,19 +12,19 @@ import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/lock.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/restore.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/similar_photos.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -203,22 +203,13 @@ enum ActionButtonType {
|
||||
),
|
||||
ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.unstack => ActionMenuItem(action: StackAction(source: context.source)),
|
||||
ActionButtonType.openInBrowser => OpenInBrowserActionButton(
|
||||
remoteId: context.asset.remoteId!,
|
||||
origin: context.timelineOrigin,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
ActionButtonType.openInBrowser => ActionMenuItem(
|
||||
action: OpenInBrowserAction(remoteId: context.asset.remoteId!, origin: context.timelineOrigin),
|
||||
),
|
||||
ActionButtonType.similarPhotos => SimilarPhotosActionButton(
|
||||
assetId: (context.asset as RemoteAsset).id,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.setProfilePicture => SetProfilePictureActionButton(
|
||||
asset: context.asset,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
ActionButtonType.similarPhotos => ActionMenuItem(
|
||||
action: SimilarPhotosAction(assetId: (context.asset as RemoteAsset).id),
|
||||
),
|
||||
ActionButtonType.setProfilePicture => ActionMenuItem(action: SetProfilePictureAction(asset: context.asset)),
|
||||
ActionButtonType.openInfo => BaseActionButton(
|
||||
label: 'info'.tr(),
|
||||
iconData: Icons.info_outline,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
import '../presentation_context.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
group('webPathFor', () {
|
||||
const dedicatedPages = {
|
||||
TimelineOrigin.favorite: '/favorites',
|
||||
TimelineOrigin.trash: '/trash',
|
||||
TimelineOrigin.archive: '/archive',
|
||||
};
|
||||
|
||||
for (final origin in TimelineOrigin.values) {
|
||||
final expected = dedicatedPages[origin] ?? '';
|
||||
|
||||
test('opens ${origin.name} on ${expected.isEmpty ? 'the main timeline' : expected}', () {
|
||||
expect(webPathFor(origin), expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('OpenInBrowserAction', () {
|
||||
testWidgets('always renders, since the kebab menu decides whether to offer it', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
const ActionIconButton(
|
||||
action: OpenInBrowserAction(remoteId: 'remote-1', origin: .main),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user