mirror of
https://github.com/immich-app/immich.git
synced 2026-07-07 21:17:09 -07:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f67629ff07 | |||
| 7130553634 | |||
| d26f6c0665 | |||
| da89c75bdd | |||
| 2272583a7e | |||
| 73f8e90f0f | |||
| 2999b00e5d | |||
| c668bd3342 | |||
| c3a02b179a | |||
| 6b77c90e1c |
@@ -9,8 +9,6 @@ enum SortOrder {
|
|||||||
|
|
||||||
enum TextSearchType { context, filename, description, ocr }
|
enum TextSearchType { context, filename, description, ocr }
|
||||||
|
|
||||||
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
|
|
||||||
|
|
||||||
enum ActionSource { timeline, viewer }
|
enum ActionSource { timeline, viewer }
|
||||||
|
|
||||||
enum ShareAssetType { original, preview }
|
enum ShareAssetType { original, preview }
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ class RemoteAsset extends BaseAsset {
|
|||||||
|
|
||||||
bool get isTrashed => deletedAt != null;
|
bool get isTrashed => deletedAt != null;
|
||||||
|
|
||||||
|
bool get isStacked => stackId != null;
|
||||||
|
|
||||||
|
bool get isArchived => visibility == .archive;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final RemoteAssetRepository _remoteRepository;
|
final RemoteAssetRepository _remoteRepository;
|
||||||
@@ -69,12 +70,43 @@ class AssetService {
|
|||||||
return _localRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
return _localRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateFavorite(List<String> remoteIds, bool isFavorite) async {
|
Future<void> restoreTrash(List<String> remoteIds) async {
|
||||||
if (remoteIds.isEmpty) {
|
if (remoteIds.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _apiRepository.updateFavorite(remoteIds, isFavorite);
|
await _apiRepository.restoreTrash(remoteIds);
|
||||||
await _remoteRepository.updateFavorite(remoteIds, isFavorite);
|
await _remoteRepository.restoreTrash(remoteIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stack(String userId, List<String> remoteIds) async {
|
||||||
|
if (remoteIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final stack = await _apiRepository.stack(remoteIds);
|
||||||
|
await _remoteRepository.stack(userId, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unstack(List<String> stackIds) async {
|
||||||
|
if (stackIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _remoteRepository.unStack(stackIds);
|
||||||
|
await _apiRepository.unStack(stackIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(
|
||||||
|
List<String> remoteIds, {
|
||||||
|
Option<bool> isFavorite = const .none(),
|
||||||
|
Option<AssetVisibility> visibility = const .none(),
|
||||||
|
}) async {
|
||||||
|
if (remoteIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _apiRepository.update(remoteIds, isFavorite: isFavorite, visibility: visibility);
|
||||||
|
await _remoteRepository.update(remoteIds, isFavorite: isFavorite, visibility: visibility);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class RemoteAssetRepository extends DriftDatabaseRepository {
|
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||||
@@ -286,4 +287,20 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
..orderBy([(row) => OrderingTerm.asc(row.sequence)]);
|
..orderBy([(row) => OrderingTerm.asc(row.sequence)]);
|
||||||
return query.map((row) => row.toDto()!).get();
|
return query.map((row) => row.toDto()!).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> update(
|
||||||
|
List<String> remoteIds, {
|
||||||
|
Option<bool> isFavorite = const .none(),
|
||||||
|
Option<AssetVisibility> visibility = const .none(),
|
||||||
|
}) {
|
||||||
|
final companion = RemoteAssetEntityCompanion(
|
||||||
|
visibility: visibility.toDriftValue(),
|
||||||
|
isFavorite: isFavorite.toDriftValue(),
|
||||||
|
);
|
||||||
|
return _db.batch((batch) {
|
||||||
|
for (final remoteId in remoteIds) {
|
||||||
|
batch.update(_db.remoteAssetEntity, companion, where: (e) => e.id.equals(remoteId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/partner.action.dart';
|
import 'package:immich_mobile/presentation/actions/partner.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart';
|
||||||
@@ -27,13 +28,14 @@ class PartnerPage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final sharedByAsync = ref.watch(partnersStateProvider);
|
final sharedByAsync = ref.watch(partnersStateProvider);
|
||||||
|
final scope = ActionScope.from(context, ref);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.t.partners),
|
title: Text(context.t.partners),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: const [ActionIconButtonWidget(action: PartnerAddAction())],
|
actions: [ActionIconButtonWidget(action: PartnerAddAction(scope: scope))],
|
||||||
),
|
),
|
||||||
body: sharedByAsync.when(
|
body: sharedByAsync.when(
|
||||||
data: (partners) => PartnerSharedByList(partners: partners.toList(growable: false)),
|
data: (partners) => PartnerSharedByList(partners: partners.toList(growable: false)),
|
||||||
@@ -44,11 +46,12 @@ class PartnerPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EmptyPartners extends StatelessWidget {
|
class _EmptyPartners extends ConsumerWidget {
|
||||||
const _EmptyPartners();
|
const _EmptyPartners();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final scope = ActionScope.from(context, ref);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const .symmetric(horizontal: 16.0),
|
padding: const .symmetric(horizontal: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -58,9 +61,9 @@ class _EmptyPartners extends StatelessWidget {
|
|||||||
padding: const .symmetric(vertical: 8),
|
padding: const .symmetric(vertical: 8),
|
||||||
child: Text(context.t.partner_page_empty_message, style: const TextStyle(fontSize: 14)),
|
child: Text(context.t.partner_page_empty_message, style: const TextStyle(fontSize: 14)),
|
||||||
),
|
),
|
||||||
const Align(
|
Align(
|
||||||
alignment: .center,
|
alignment: .center,
|
||||||
child: ActionButtonWidget(action: PartnerAddAction()),
|
child: ActionButtonWidget(action: PartnerAddAction(scope: scope)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -69,16 +72,17 @@ class _EmptyPartners extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
class PartnerSharedByList extends StatelessWidget {
|
class PartnerSharedByList extends ConsumerWidget {
|
||||||
const PartnerSharedByList({super.key, required this.partners});
|
const PartnerSharedByList({super.key, required this.partners});
|
||||||
|
|
||||||
final List<Partner> partners;
|
final List<Partner> partners;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
if (partners.isEmpty) {
|
if (partners.isEmpty) {
|
||||||
return const _EmptyPartners();
|
return const _EmptyPartners();
|
||||||
}
|
}
|
||||||
|
final scope = ActionScope.from(context, ref);
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: partners.length,
|
itemCount: partners.length,
|
||||||
@@ -89,7 +93,7 @@ class PartnerSharedByList extends StatelessWidget {
|
|||||||
title: Text(partner.name),
|
title: Text(partner.name),
|
||||||
subtitle: Text(partner.email),
|
subtitle: Text(partner.email),
|
||||||
trailing: ActionIconButtonWidget(
|
trailing: ActionIconButtonWidget(
|
||||||
action: PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name),
|
action: PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name, scope: scope),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
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/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
class ActionScope {
|
class ActionScope {
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
@@ -9,24 +11,24 @@ class ActionScope {
|
|||||||
final UserDto authUser;
|
final UserDto authUser;
|
||||||
|
|
||||||
const ActionScope({required this.context, required this.ref, required this.authUser});
|
const ActionScope({required this.context, required this.ref, required this.authUser});
|
||||||
|
|
||||||
|
factory ActionScope.from(BuildContext context, WidgetRef ref) {
|
||||||
|
final authUser = ref.watch(currentUserProvider);
|
||||||
|
if (authUser == null) {
|
||||||
|
throw StateError('Auth user is not available in ActionScope');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActionScope(context: context, ref: ref, authUser: authUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BaseAction {
|
abstract class BaseAction {
|
||||||
const BaseAction();
|
final ActionScope scope;
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final bool isVisible;
|
||||||
|
|
||||||
IconData get icon;
|
const BaseAction({required this.scope, required this.icon, required this.label, this.isVisible = true});
|
||||||
|
|
||||||
String label(ActionScope scope);
|
Future<void> onAction();
|
||||||
|
|
||||||
bool isVisible(ActionScope scope) => true;
|
|
||||||
|
|
||||||
Future<void> onAction(ActionScope scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class AssetAction<T extends BaseAsset> extends BaseAction {
|
|
||||||
final Iterable<BaseAsset> assets;
|
|
||||||
|
|
||||||
const AssetAction({required this.assets});
|
|
||||||
|
|
||||||
Iterable<T> filter(ActionScope scope) => assets.whereType<T>();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
|
||||||
import 'package:immich_mobile/utils/error_handler.dart';
|
import 'package:immich_mobile/utils/error_handler.dart';
|
||||||
import 'package:immich_ui/immich_ui.dart';
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
|
|
||||||
class _ActionWidgetScope {
|
class _ActionWidgetScope {
|
||||||
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onAction;
|
final FutureOr<void> Function() onAction;
|
||||||
|
|
||||||
const _ActionWidgetScope({required this.label, required this.onAction});
|
const _ActionWidgetScope({required this.icon, required this.label, required this.onAction});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActionWidget extends ConsumerWidget {
|
class _ActionWidget extends ConsumerWidget {
|
||||||
@@ -19,27 +21,21 @@ class _ActionWidget extends ConsumerWidget {
|
|||||||
|
|
||||||
const _ActionWidget({required this.action, required this.builder});
|
const _ActionWidget({required this.action, required this.builder});
|
||||||
|
|
||||||
Future<void> _onAction(ActionScope scope) async {
|
Future<void> _onAction() async {
|
||||||
try {
|
try {
|
||||||
await action.onAction(scope);
|
await action.onAction();
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
handleError(scope.context, error, stack: stackTrace, description: 'Action failed: ${action.runtimeType}');
|
handleError(error, stack: stackTrace, description: 'Action failed: ${action.runtimeType}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final authUser = ref.watch(currentUserProvider);
|
if (!action.isVisible) {
|
||||||
if (authUser == null) {
|
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final scope = ActionScope(context: context, ref: ref, authUser: authUser);
|
return builder(.new(icon: action.icon, label: action.label, onAction: _onAction));
|
||||||
if (!action.isVisible(scope)) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder(.new(label: action.label(scope), onAction: () => _onAction(scope)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +48,7 @@ class ActionIconButtonWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => _ActionWidget(
|
Widget build(BuildContext context) => _ActionWidget(
|
||||||
action: action,
|
action: action,
|
||||||
builder: (ctx) => ImmichIconButton(icon: action.icon, onPressed: ctx.onAction, variant: variant),
|
builder: (ctx) => ImmichIconButton(icon: ctx.icon, onPressed: ctx.onAction, variant: variant),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +61,7 @@ class ActionButtonWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => _ActionWidget(
|
Widget build(BuildContext context) => _ActionWidget(
|
||||||
action: action,
|
action: action,
|
||||||
builder: (ctx) =>
|
builder: (ctx) => ImmichTextButton(labelText: ctx.label, icon: ctx.icon, onPressed: ctx.onAction, variant: variant),
|
||||||
ImmichTextButton(labelText: ctx.label, icon: action.icon, onPressed: ctx.onAction, variant: variant),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +73,7 @@ class ActionColumnButtonWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => _ActionWidget(
|
Widget build(BuildContext context) => _ActionWidget(
|
||||||
action: action,
|
action: action,
|
||||||
builder: (ctx) => ImmichColumnButton(icon: action.icon, label: ctx.label, onPressed: ctx.onAction),
|
builder: (ctx) => ImmichColumnButton(icon: ctx.icon, label: ctx.label, onPressed: ctx.onAction),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +85,6 @@ class ActionMenuItemWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => _ActionWidget(
|
Widget build(BuildContext context) => _ActionWidget(
|
||||||
action: action,
|
action: action,
|
||||||
builder: (ctx) => ImmichMenuItem(icon: action.icon, label: ctx.label, onPressed: ctx.onAction),
|
builder: (ctx) => ImmichMenuItem(icon: ctx.icon, label: ctx.label, onPressed: ctx.onAction),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.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/providers/infrastructure/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||||
|
|
||||||
|
class ArchiveAction extends BaseAction {
|
||||||
|
final List<String> assetIds;
|
||||||
|
final bool archive;
|
||||||
|
|
||||||
|
const ArchiveAction._({
|
||||||
|
required this.assetIds,
|
||||||
|
required this.archive,
|
||||||
|
required super.scope,
|
||||||
|
required super.icon,
|
||||||
|
required super.label,
|
||||||
|
super.isVisible,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ArchiveAction({required Iterable<BaseAsset> assets, required ActionScope scope}) {
|
||||||
|
final ownedAssets = AssetFilter(assets).owned(scope.authUser.id);
|
||||||
|
final archive = ownedAssets.archived(isArchived: false).isNotEmpty;
|
||||||
|
final assetIds = ownedAssets.archived(isArchived: !archive).map((asset) => asset.id).toList(growable: false);
|
||||||
|
|
||||||
|
return ArchiveAction._(
|
||||||
|
assetIds: assetIds,
|
||||||
|
archive: archive,
|
||||||
|
scope: scope,
|
||||||
|
icon: archive ? Icons.archive_outlined : Icons.unarchive_outlined,
|
||||||
|
label: archive ? scope.context.t.archive : scope.context.t.unarchive,
|
||||||
|
isVisible: assetIds.isNotEmpty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onAction() async {
|
||||||
|
final ActionScope(:ref, :context) = scope;
|
||||||
|
|
||||||
|
await ref.read(assetServiceProvider).update(assetIds, visibility: .some(archive ? .archive : .timeline));
|
||||||
|
final message = archive
|
||||||
|
? context.t.archive_action_prompt(count: assetIds.length)
|
||||||
|
: context.t.unarchive_action_prompt(count: assetIds.length);
|
||||||
|
ref.read(toastRepositoryProvider).success(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
|
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/favorite.action.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||||
|
|
||||||
|
class AssetActions {
|
||||||
|
final AssetDebugAction debug;
|
||||||
|
final FavoriteAction favorite;
|
||||||
|
final ArchiveAction archive;
|
||||||
|
final StackAction stack;
|
||||||
|
|
||||||
|
const AssetActions({required this.debug, required this.favorite, required this.archive, required this.stack});
|
||||||
|
|
||||||
|
static AssetActions from(ActionScope scope, List<BaseAsset> assets) => .new(
|
||||||
|
debug: AssetDebugAction(assets: assets, scope: scope),
|
||||||
|
favorite: FavoriteAction(assets: assets, scope: scope),
|
||||||
|
archive: ArchiveAction(assets: assets, scope: scope),
|
||||||
|
stack: StackAction(assets: assets, scope: scope),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,20 +8,18 @@ import 'package:immich_mobile/presentation/actions/action.dart';
|
|||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
class AssetDebugAction extends AssetAction<BaseAsset> {
|
class AssetDebugAction extends BaseAction {
|
||||||
const AssetDebugAction({required super.assets});
|
final List<BaseAsset> asset;
|
||||||
|
|
||||||
|
AssetDebugAction._({this.asset = const [], required super.scope, super.isVisible})
|
||||||
|
: super(icon: Icons.help_outline_rounded, label: scope.context.t.troubleshoot);
|
||||||
|
|
||||||
|
factory AssetDebugAction({required Iterable<BaseAsset> assets, required ActionScope scope}) => AssetDebugAction._(
|
||||||
|
asset: assets.toList(growable: false),
|
||||||
|
scope: scope,
|
||||||
|
isVisible: scope.ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting) && assets.length == 1,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IconData get icon => Icons.help_outline_rounded;
|
Future<void> onAction() async => unawaited(scope.context.pushRoute(AssetTroubleshootRoute(asset: asset.first)));
|
||||||
|
|
||||||
@override
|
|
||||||
String label(ActionScope scope) => scope.context.t.troubleshoot;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isVisible(ActionScope scope) =>
|
|
||||||
assets.length == 1 && scope.ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onAction(ActionScope scope) async =>
|
|
||||||
unawaited(scope.context.pushRoute(AssetTroubleshootRoute(asset: assets.first)));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,38 +3,45 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/generated/translations.g.dart';
|
import 'package:immich_mobile/generated/translations.g.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_ui/immich_ui.dart';
|
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||||
|
|
||||||
class FavoriteAction extends AssetAction<RemoteAsset> {
|
class FavoriteAction extends BaseAction {
|
||||||
final bool shouldFavorite;
|
final List<String> assetIds;
|
||||||
|
final bool favorite;
|
||||||
|
|
||||||
FavoriteAction({required super.assets}) : shouldFavorite = assets.any((asset) => !asset.isFavorite);
|
const FavoriteAction._({
|
||||||
|
required this.assetIds,
|
||||||
|
required this.favorite,
|
||||||
|
required super.scope,
|
||||||
|
required super.icon,
|
||||||
|
required super.label,
|
||||||
|
super.isVisible,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FavoriteAction({required Iterable<BaseAsset> assets, required ActionScope scope}) {
|
||||||
|
final ownedAssets = AssetFilter(assets).owned(scope.authUser.id);
|
||||||
|
final favorite = ownedAssets.favorite(isFavorite: false).isNotEmpty;
|
||||||
|
final assetIds = ownedAssets.favorite(isFavorite: !favorite).map((asset) => asset.id).toList(growable: false);
|
||||||
|
|
||||||
|
return FavoriteAction._(
|
||||||
|
assetIds: assetIds,
|
||||||
|
favorite: favorite,
|
||||||
|
scope: scope,
|
||||||
|
icon: favorite ? Icons.favorite_border_rounded : Icons.favorite_rounded,
|
||||||
|
label: favorite ? scope.context.t.favorite : scope.context.t.unfavorite,
|
||||||
|
isVisible: assetIds.isNotEmpty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IconData get icon => shouldFavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
|
Future<void> onAction() async {
|
||||||
|
final ActionScope(:ref, :context) = scope;
|
||||||
|
|
||||||
@override
|
await ref.read(assetServiceProvider).update(assetIds, isFavorite: .some(favorite));
|
||||||
String label(ActionScope scope) => shouldFavorite ? scope.context.t.favorite : scope.context.t.unfavorite;
|
final message = favorite
|
||||||
|
? context.t.favorite_action_prompt(count: assetIds.length)
|
||||||
@override
|
: context.t.unfavorite_action_prompt(count: assetIds.length);
|
||||||
Iterable<RemoteAsset> filter(ActionScope scope) => assets
|
ref.read(toastRepositoryProvider).success(message);
|
||||||
.where(
|
|
||||||
(asset) => asset is RemoteAsset && asset.ownerId == scope.authUser.id && asset.isFavorite == !shouldFavorite,
|
|
||||||
)
|
|
||||||
.cast<RemoteAsset>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isVisible(ActionScope scope) => filter(scope).isNotEmpty;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onAction(ActionScope scope) async {
|
|
||||||
final ActionScope(:ref) = scope;
|
|
||||||
final assets = filter(scope).map((asset) => asset.id).toList(growable: false);
|
|
||||||
|
|
||||||
await ref.read(assetServiceProvider).updateFavorite(assets, shouldFavorite);
|
|
||||||
final message = shouldFavorite
|
|
||||||
? StaticTranslations.instance.favorite_action_prompt(count: assets.length)
|
|
||||||
: StaticTranslations.instance.unfavorite_action_prompt(count: assets.length);
|
|
||||||
snackbar.success(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,10 @@ import 'package:immich_mobile/providers/user.provider.dart';
|
|||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
|
|
||||||
class PartnerAddAction extends BaseAction {
|
class PartnerAddAction extends BaseAction {
|
||||||
const PartnerAddAction();
|
PartnerAddAction({required super.scope}) : super(icon: Icons.person_add_rounded, label: scope.context.t.add_partner);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IconData get icon => Icons.person_add_rounded;
|
Future<void> onAction() async {
|
||||||
|
|
||||||
@override
|
|
||||||
String label(ActionScope scope) => scope.context.t.add_partner;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onAction(ActionScope scope) async {
|
|
||||||
final ActionScope(:context, :ref, :authUser) = scope;
|
final ActionScope(:context, :ref, :authUser) = scope;
|
||||||
final selected = await showDialog<User>(context: context, builder: (_) => const PartnerSelectionDialog());
|
final selected = await showDialog<User>(context: context, builder: (_) => const PartnerSelectionDialog());
|
||||||
if (selected == null) {
|
if (selected == null) {
|
||||||
@@ -30,19 +24,14 @@ class PartnerAddAction extends BaseAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PartnerRemoveAction extends BaseAction {
|
class PartnerRemoveAction extends BaseAction {
|
||||||
const PartnerRemoveAction({required this.sharedWithId, required this.partnerName});
|
PartnerRemoveAction({required super.scope, required this.sharedWithId, required this.partnerName})
|
||||||
|
: super(icon: Icons.person_remove_rounded, label: scope.context.t.remove);
|
||||||
|
|
||||||
final String sharedWithId;
|
final String sharedWithId;
|
||||||
final String partnerName;
|
final String partnerName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IconData get icon => Icons.person_remove_rounded;
|
Future<void> onAction() async {
|
||||||
|
|
||||||
@override
|
|
||||||
String label(ActionScope scope) => scope.context.t.remove;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onAction(ActionScope scope) async {
|
|
||||||
final ActionScope(:context, :ref, :authUser) = scope;
|
final ActionScope(:context, :ref, :authUser) = scope;
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.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/providers/infrastructure/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||||
|
|
||||||
|
class RestoreAction extends BaseAction {
|
||||||
|
final List<String> assetIds;
|
||||||
|
|
||||||
|
RestoreAction._({required super.scope, required this.assetIds, super.isVisible})
|
||||||
|
: super(icon: Icons.history_rounded, label: scope.context.t.restore);
|
||||||
|
|
||||||
|
factory RestoreAction({required Iterable<BaseAsset> assets, required ActionScope scope}) {
|
||||||
|
final assetIds = AssetFilter(
|
||||||
|
assets,
|
||||||
|
).owned(scope.authUser.id).trashed().map((asset) => asset.id).toList(growable: false);
|
||||||
|
|
||||||
|
return RestoreAction._(assetIds: assetIds, scope: scope, isVisible: assetIds.isNotEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onAction() async {
|
||||||
|
final ActionScope(:ref, :context) = scope;
|
||||||
|
await ref.read(assetServiceProvider).restoreTrash(assetIds);
|
||||||
|
ref.read(toastRepositoryProvider).success(context.t.assets_restored_count(count: assetIds.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.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/providers/infrastructure/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||||
|
|
||||||
|
class StackAction extends BaseAction {
|
||||||
|
final List<String> assetIds;
|
||||||
|
final List<String> stackIds;
|
||||||
|
final bool stack;
|
||||||
|
|
||||||
|
const StackAction._({
|
||||||
|
required this.assetIds,
|
||||||
|
required this.stackIds,
|
||||||
|
required this.stack,
|
||||||
|
required super.scope,
|
||||||
|
required super.icon,
|
||||||
|
required super.label,
|
||||||
|
super.isVisible,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory StackAction({required Iterable<BaseAsset> assets, required ActionScope scope}) {
|
||||||
|
final ownedAssets = AssetFilter(assets).owned(scope.authUser.id);
|
||||||
|
// Stack when any owned asset is not yet stacked; otherwise unstack them all.
|
||||||
|
final stack = ownedAssets.stacked(isStacked: false).isNotEmpty;
|
||||||
|
final assetIds = ownedAssets.map((asset) => asset.id).toList(growable: false);
|
||||||
|
final stackIds = ownedAssets.map((asset) => asset.stackId).nonNulls.toList(growable: false);
|
||||||
|
|
||||||
|
return StackAction._(
|
||||||
|
assetIds: assetIds,
|
||||||
|
stackIds: stackIds,
|
||||||
|
stack: stack,
|
||||||
|
scope: scope,
|
||||||
|
icon: stack ? Icons.filter_none_rounded : Icons.layers_clear_outlined,
|
||||||
|
label: stack ? scope.context.t.stack : scope.context.t.unstack,
|
||||||
|
isVisible: stack ? assetIds.length > 1 : stackIds.isNotEmpty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onAction() async {
|
||||||
|
final ActionScope(:ref, :context) = scope;
|
||||||
|
if (stack) {
|
||||||
|
await ref.read(assetServiceProvider).stack(scope.authUser.id, assetIds);
|
||||||
|
} else {
|
||||||
|
await ref.read(assetServiceProvider).unstack(stackIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = stack
|
||||||
|
? context.t.stacked_assets_count(count: assetIds.length)
|
||||||
|
: context.t.unstacked_assets_count(count: assetIds.length);
|
||||||
|
ref.read(toastRepositoryProvider).success(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
class TimelineAction extends BaseAction {
|
class TimelineAction extends BaseAction {
|
||||||
final BaseAction action;
|
final BaseAction action;
|
||||||
|
|
||||||
const TimelineAction({required this.action});
|
TimelineAction({required this.action})
|
||||||
|
: super(scope: action.scope, icon: action.icon, label: action.label, isVisible: action.isVisible);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IconData get icon => action.icon;
|
Future<void> onAction() async {
|
||||||
|
await action.onAction();
|
||||||
@override
|
|
||||||
String label(ActionScope scope) => action.label(scope);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isVisible(ActionScope scope) => action.isVisible(scope);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onAction(ActionScope scope) async {
|
|
||||||
await action.onAction(scope);
|
|
||||||
scope.ref.read(multiSelectProvider.notifier).reset();
|
scope.ref.read(multiSelectProvider.notifier).reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
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/build_context_extensions.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
|
|
||||||
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
enum AddToMenuItem { album, lockedFolder }
|
||||||
|
|
||||||
class AddActionButton extends ConsumerStatefulWidget {
|
class AddActionButton extends ConsumerStatefulWidget {
|
||||||
const AddActionButton({super.key, this.originalTheme});
|
const AddActionButton({super.key, this.originalTheme});
|
||||||
@@ -37,12 +36,6 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
case AddToMenuItem.album:
|
case AddToMenuItem.album:
|
||||||
_openAlbumSelector();
|
_openAlbumSelector();
|
||||||
break;
|
break;
|
||||||
case AddToMenuItem.archive:
|
|
||||||
performArchiveAction(context, ref, source: ActionSource.viewer);
|
|
||||||
break;
|
|
||||||
case AddToMenuItem.unarchive:
|
|
||||||
performUnArchiveAction(context, ref, source: ActionSource.viewer);
|
|
||||||
break;
|
|
||||||
case AddToMenuItem.lockedFolder:
|
case AddToMenuItem.lockedFolder:
|
||||||
performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
|
performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
|
||||||
break;
|
break;
|
||||||
@@ -57,11 +50,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
|
|
||||||
final user = ref.read(currentUserProvider);
|
final user = ref.read(currentUserProvider);
|
||||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final scope = ActionScope.from(context, ref);
|
||||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
|
||||||
final hasRemote = asset is RemoteAsset;
|
|
||||||
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
|
|
||||||
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Padding(
|
Padding(
|
||||||
@@ -81,20 +70,9 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
||||||
),
|
),
|
||||||
if (showArchive)
|
ActionMenuItemWidget(
|
||||||
BaseActionButton(
|
action: ArchiveAction(assets: [asset], scope: scope),
|
||||||
iconData: Icons.archive_outlined,
|
),
|
||||||
label: "archive".tr(),
|
|
||||||
menuItem: true,
|
|
||||||
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
|
|
||||||
),
|
|
||||||
if (showUnarchive)
|
|
||||||
BaseActionButton(
|
|
||||||
iconData: Icons.unarchive_outlined,
|
|
||||||
label: "unarchive".tr(),
|
|
||||||
menuItem: true,
|
|
||||||
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
|
|
||||||
),
|
|
||||||
BaseActionButton(
|
BaseActionButton(
|
||||||
iconData: Icons.lock_outline,
|
iconData: Icons.lock_outline,
|
||||||
label: "locked_folder".tr(),
|
label: "locked_folder".tr(),
|
||||||
@@ -184,7 +162,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
|
|
||||||
final themeData = widget.originalTheme ?? context.themeData;
|
final themeData = widget.originalTheme ?? context.themeData;
|
||||||
|
|
||||||
return MenuAnchor(
|
return ImmichMenu(
|
||||||
consumeOutsideTap: true,
|
consumeOutsideTap: true,
|
||||||
style: MenuStyle(
|
style: MenuStyle(
|
||||||
backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor),
|
backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor),
|
||||||
@@ -195,7 +173,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
),
|
),
|
||||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||||
),
|
),
|
||||||
menuChildren: widget.originalTheme != null
|
children: widget.originalTheme != null
|
||||||
? [
|
? [
|
||||||
Theme(
|
Theme(
|
||||||
data: widget.originalTheme!,
|
data: widget.originalTheme!,
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.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/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
// used to allow performing archive action from different sources (without duplicating code)
|
|
||||||
Future<void> performArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source == ActionSource.viewer) {
|
|
||||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).archive(source);
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: result.success ? ToastType.success : ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArchiveActionButton extends ConsumerWidget {
|
|
||||||
final ActionSource source;
|
|
||||||
final bool iconOnly;
|
|
||||||
final bool menuItem;
|
|
||||||
|
|
||||||
const ArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
|
||||||
|
|
||||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
await performArchiveAction(context, ref, source: source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
iconData: Icons.archive_outlined,
|
|
||||||
label: "to_archive".t(context: context),
|
|
||||||
iconOnly: iconOnly,
|
|
||||||
menuItem: menuItem,
|
|
||||||
onPressed: () => _onTap(context, ref),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.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/providers/asset_viewer/asset_viewer.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
class FavoriteActionButton extends ConsumerWidget {
|
|
||||||
final ActionSource source;
|
|
||||||
final bool iconOnly;
|
|
||||||
final bool menuItem;
|
|
||||||
|
|
||||||
const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).favorite(source);
|
|
||||||
|
|
||||||
if (source == ActionSource.viewer) {
|
|
||||||
if (result.success) {
|
|
||||||
final currentAsset = ref.read(assetViewerProvider).currentAsset;
|
|
||||||
if (currentAsset is RemoteAsset && !currentAsset.isFavorite) {
|
|
||||||
ref.read(assetViewerProvider.notifier).setAsset(currentAsset.copyWith(isFavorite: true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
final successMessage = 'favorite_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: result.success ? ToastType.success : ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
iconData: Icons.favorite_border_rounded,
|
|
||||||
label: "favorite".t(context: context),
|
|
||||||
iconOnly: iconOnly,
|
|
||||||
menuItem: menuItem,
|
|
||||||
onPressed: () => _onTap(context, ref),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.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/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
class RestoreActionButton extends ConsumerWidget {
|
|
||||||
final ActionSource source;
|
|
||||||
final bool iconOnly;
|
|
||||||
final bool menuItem;
|
|
||||||
|
|
||||||
const RestoreActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).restoreTrash(source);
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
if (source == ActionSource.viewer) {
|
|
||||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()});
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: result.success ? ToastType.success : ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
iconData: Icons.history_rounded,
|
|
||||||
label: 'restore'.t(context: context),
|
|
||||||
iconOnly: iconOnly,
|
|
||||||
menuItem: menuItem,
|
|
||||||
onPressed: () => _onTap(context, ref),
|
|
||||||
maxWidth: 100.0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
class RestoreTrashActionButton extends ConsumerWidget {
|
|
||||||
final ActionSource source;
|
|
||||||
|
|
||||||
const RestoreTrashActionButton({super.key, required this.source});
|
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).restoreTrash(source);
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()});
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: result.success ? ToastType.success : ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return TextButton.icon(
|
|
||||||
icon: const Icon(Icons.history_rounded),
|
|
||||||
label: Text('restore'.t(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
|
||||||
onPressed: () => _onTap(context, ref),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.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/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
class StackActionButton extends ConsumerWidget {
|
|
||||||
final ActionSource source;
|
|
||||||
|
|
||||||
const StackActionButton({super.key, required this.source});
|
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final user = ref.watch(currentUserProvider);
|
|
||||||
if (user == null) {
|
|
||||||
throw Exception('User must be logged in to access stack action');
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).stack(user.id, source);
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
final successMessage = 'stack_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: result.success ? ToastType.success : ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
iconData: Icons.filter_none_rounded,
|
|
||||||
label: "stack".t(context: context),
|
|
||||||
onPressed: () => _onTap(context, ref),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// dart
|
|
||||||
// File: `lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart`
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.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/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
|
||||||
|
|
||||||
// used to allow performing unarchive action from different sources (without duplicating code)
|
|
||||||
Future<void> performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source == ActionSource.viewer) {
|
|
||||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).unArchive(source);
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: result.success ? ToastType.success : ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnArchiveActionButton extends ConsumerWidget {
|
|
||||||
final ActionSource source;
|
|
||||||
final bool iconOnly;
|
|
||||||
final bool menuItem;
|
|
||||||
|
|
||||||
const UnArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
|
||||||
|
|
||||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
await performUnArchiveAction(context, ref, source: source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
iconData: Icons.unarchive_outlined,
|
|
||||||
label: "unarchive".t(context: context),
|
|
||||||
iconOnly: iconOnly,
|
|
||||||
menuItem: menuItem,
|
|
||||||
onPressed: () => _onTap(context, ref),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.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/providers/asset_viewer/asset_viewer.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
class UnFavoriteActionButton extends ConsumerWidget {
|
|
||||||
final ActionSource source;
|
|
||||||
final bool iconOnly;
|
|
||||||
final bool menuItem;
|
|
||||||
|
|
||||||
const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).unFavorite(source);
|
|
||||||
|
|
||||||
if (source == ActionSource.viewer) {
|
|
||||||
if (result.success) {
|
|
||||||
final currentAsset = ref.read(assetViewerProvider).currentAsset;
|
|
||||||
if (currentAsset is RemoteAsset && currentAsset.isFavorite) {
|
|
||||||
ref.read(assetViewerProvider.notifier).setAsset(currentAsset.copyWith(isFavorite: false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
final successMessage = 'unfavorite_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: result.success ? ToastType.success : ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
iconData: Icons.favorite_rounded,
|
|
||||||
label: "unfavorite".t(context: context),
|
|
||||||
onPressed: () => _onTap(context, ref),
|
|
||||||
iconOnly: iconOnly,
|
|
||||||
menuItem: menuItem,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.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/providers/infrastructure/action.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
class UnStackActionButton extends ConsumerWidget {
|
|
||||||
final ActionSource source;
|
|
||||||
final bool iconOnly;
|
|
||||||
final bool menuItem;
|
|
||||||
|
|
||||||
const UnStackActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).unStack(source);
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
|
||||||
|
|
||||||
final successMessage = 'unstack_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: result.success ? ToastType.success : ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
iconData: Icons.layers_clear_outlined,
|
|
||||||
label: "unstack".t(context: context),
|
|
||||||
iconOnly: iconOnly,
|
|
||||||
menuItem: menuItem,
|
|
||||||
onPressed: () => _onTap(context, ref),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,14 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/restore.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/restore_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_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart';
|
||||||
@@ -42,11 +44,13 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
|
|
||||||
final originalTheme = context.themeData;
|
final originalTheme = context.themeData;
|
||||||
|
|
||||||
|
final assets = [asset];
|
||||||
|
final scope = ActionScope.from(context, ref);
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
if (isInTrash && isOwner && asset.hasRemote)
|
ActionColumnButtonWidget(
|
||||||
const RestoreActionButton(source: ActionSource.viewer)
|
action: RestoreAction(assets: assets, scope: scope),
|
||||||
else
|
),
|
||||||
const ShareActionButton(source: ActionSource.viewer),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
|
|
||||||
if (!isInLockedView) ...[
|
if (!isInLockedView) ...[
|
||||||
if (!isInTrash) ...[
|
if (!isInTrash) ...[
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class ViewerKebabMenu extends ConsumerWidget {
|
|||||||
timelineOrigin: timelineOrigin,
|
timelineOrigin: timelineOrigin,
|
||||||
);
|
);
|
||||||
|
|
||||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context);
|
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);
|
||||||
|
|
||||||
return ImmichMenu(
|
return ImmichMenu(
|
||||||
consumeOutsideTap: true,
|
consumeOutsideTap: true,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||||
@@ -44,6 +45,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
final originalTheme = context.themeData;
|
final originalTheme = context.themeData;
|
||||||
final assetForAction = [asset];
|
final assetForAction = [asset];
|
||||||
|
final scope = ActionScope.from(context, ref);
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||||
@@ -61,7 +63,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)),
|
ActionIconButtonWidget(
|
||||||
|
action: FavoriteAction(assets: assetForAction, scope: scope),
|
||||||
|
),
|
||||||
|
|
||||||
ViewerKebabMenu(originalTheme: originalTheme),
|
ViewerKebabMenu(originalTheme: originalTheme),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
import 'package:immich_mobile/presentation/actions/asset_actions.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||||
@@ -14,10 +15,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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_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/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
@@ -76,8 +74,8 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
|||||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
final scope = ActionScope.from(context, ref);
|
||||||
final actions = [FavoriteAction(assets: assets)];
|
final actions = AssetActions.from(scope, multiselect.selectedAssets.toList(growable: false));
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
controller: sheetController,
|
controller: sheetController,
|
||||||
@@ -88,8 +86,11 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
|||||||
const ShareActionButton(source: ActionSource.timeline),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
...[
|
||||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
actions.favorite,
|
||||||
|
actions.archive,
|
||||||
|
actions.stack,
|
||||||
|
].map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
@@ -97,8 +98,6 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
|||||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
|
||||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
],
|
||||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
import 'package:immich_mobile/presentation/actions/asset_actions.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_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/download_action_button.widget.dart';
|
||||||
@@ -16,9 +16,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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_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/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
@@ -67,8 +65,8 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
final scope = ActionScope.from(context, ref);
|
||||||
final actions = [FavoriteAction(assets: assets)];
|
final actions = AssetActions.from(scope, multiselect.selectedAssets.toList(growable: false));
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
initialChildSize: 0.4,
|
initialChildSize: 0.4,
|
||||||
@@ -78,8 +76,11 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
const ShareActionButton(source: ActionSource.timeline),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
...[
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
actions.favorite,
|
||||||
|
actions.archive,
|
||||||
|
actions.stack,
|
||||||
|
].map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
@@ -87,8 +88,6 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
|
||||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
],
|
||||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
import 'package:immich_mobile/presentation/actions/asset_actions.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
@@ -14,13 +14,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_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/edit_date_time_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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_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/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
@@ -83,8 +80,8 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
|||||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
final scope = ActionScope.from(context, ref);
|
||||||
final actions = [AssetDebugAction(assets: assets)];
|
final actions = AssetActions.from(scope, multiselect.selectedAssets.toList(growable: false));
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
controller: sheetController,
|
controller: sheetController,
|
||||||
@@ -93,7 +90,12 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
|||||||
maxChildSize: 0.85,
|
maxChildSize: 0.85,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
...[
|
||||||
|
actions.debug,
|
||||||
|
actions.favorite,
|
||||||
|
actions.archive,
|
||||||
|
actions.stack,
|
||||||
|
].map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||||
const ShareActionButton(source: ActionSource.timeline),
|
const ShareActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasRemote) ...[
|
if (multiselect.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
@@ -101,14 +103,10 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
|||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
const FavoriteActionButton(source: ActionSource.timeline),
|
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
|
||||||
if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline),
|
if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline),
|
||||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
|
||||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
|
||||||
if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline),
|
if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
if (multiselect.onlyLocal || multiselect.hasMerged)
|
if (multiselect.onlyLocal || multiselect.hasMerged)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
import 'package:immich_mobile/presentation/actions/asset_actions.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_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/download_action_button.widget.dart';
|
||||||
@@ -17,9 +17,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_al
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_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/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
@@ -85,8 +83,8 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
|||||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
final scope = ActionScope.from(context, ref);
|
||||||
final actions = [FavoriteAction(assets: assets)];
|
final actions = AssetActions.from(scope, multiselect.selectedAssets.toList(growable: false));
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
controller: sheetController,
|
controller: sheetController,
|
||||||
@@ -100,8 +98,11 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
|||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
|
|
||||||
if (ownsAlbum) ...[
|
if (ownsAlbum) ...[
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
...[
|
||||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
actions.favorite,
|
||||||
|
actions.archive,
|
||||||
|
actions.stack,
|
||||||
|
].map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||||
],
|
],
|
||||||
const DownloadActionButton(source: ActionSource.timeline),
|
const DownloadActionButton(source: ActionSource.timeline),
|
||||||
if (ownsAlbum) ...[
|
if (ownsAlbum) ...[
|
||||||
@@ -111,8 +112,6 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
|||||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
|
||||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
|
|||||||
@@ -2,26 +2,37 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/restore.action.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
class TrashBottomBar extends ConsumerWidget {
|
class TrashBottomBar extends ConsumerWidget {
|
||||||
const TrashBottomBar({super.key});
|
const TrashBottomBar({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final assets = ref.watch(multiSelectProvider.select((s) => s.selectedAssets)).toList(growable: false);
|
||||||
|
final scope = ActionScope.from(context, ref);
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: context.themeData.canvasColor,
|
color: context.themeData.canvasColor,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: const SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
DeleteTrashActionButton(source: ActionSource.timeline),
|
const DeleteTrashActionButton(source: ActionSource.timeline),
|
||||||
RestoreTrashActionButton(source: ActionSource.timeline),
|
ActionColumnButtonWidget(
|
||||||
|
action: TimelineAction(
|
||||||
|
action: RestoreAction(assets: assets, scope: scope),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -156,50 +156,6 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> favorite(ActionSource source) async {
|
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
|
||||||
try {
|
|
||||||
await _service.favorite(ids);
|
|
||||||
return ActionResult(count: ids.length, success: true);
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe('Failed to favorite assets', error, stack);
|
|
||||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ActionResult> unFavorite(ActionSource source) async {
|
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
|
||||||
try {
|
|
||||||
await _service.unFavorite(ids);
|
|
||||||
return ActionResult(count: ids.length, success: true);
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe('Failed to unfavorite assets', error, stack);
|
|
||||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ActionResult> archive(ActionSource source) async {
|
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
|
||||||
try {
|
|
||||||
await _service.archive(ids);
|
|
||||||
return ActionResult(count: ids.length, success: true);
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe('Failed to archive assets', error, stack);
|
|
||||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ActionResult> unArchive(ActionSource source) async {
|
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
|
||||||
try {
|
|
||||||
await _service.unArchive(ids);
|
|
||||||
return ActionResult(count: ids.length, success: true);
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe('Failed to unarchive assets', error, stack);
|
|
||||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
final ids = _getOwnedRemoteIdsForSource(source);
|
||||||
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
|
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
|
||||||
@@ -235,17 +191,6 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> restoreTrash(ActionSource source) async {
|
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
|
||||||
try {
|
|
||||||
await _service.restoreTrash(ids);
|
|
||||||
return ActionResult(count: ids.length, success: true);
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe('Failed to restore trash assets', error, stack);
|
|
||||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ActionResult> emptyTrash(String userId) async {
|
Future<ActionResult> emptyTrash(String userId) async {
|
||||||
try {
|
try {
|
||||||
final count = await _service.emptyTrash(userId);
|
final count = await _service.emptyTrash(userId);
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/repositories/toast.repository.dart';
|
||||||
|
|
||||||
|
final toastRepositoryProvider = Provider<ToastRepository>((ref) => const .new());
|
||||||
@@ -22,8 +22,6 @@ class MultiSelectState {
|
|||||||
bool get hasRemote =>
|
bool get hasRemote =>
|
||||||
selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged);
|
selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged);
|
||||||
|
|
||||||
bool get hasStacked => selectedAssets.any((asset) => asset is RemoteAsset && asset.stackId != null);
|
|
||||||
|
|
||||||
bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged);
|
bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged);
|
||||||
|
|
||||||
bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
|
bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction;
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction;
|
||||||
import 'package:immich_mobile/domain/models/stack.model.dart';
|
import 'package:immich_mobile/domain/models/stack.model.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart' as api show AssetVisibility;
|
||||||
|
import 'package:openapi/api.dart' hide AssetVisibility;
|
||||||
|
|
||||||
final assetApiRepositoryProvider = Provider(
|
final assetApiRepositoryProvider = Provider(
|
||||||
(ref) => AssetApiRepository(
|
(ref) => AssetApiRepository(
|
||||||
@@ -41,14 +43,10 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
return response?.count ?? 0;
|
return response?.count ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateVisibility(List<String> ids, AssetVisibilityEnum visibility) async {
|
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) async {
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateFavorite(List<String> ids, bool isFavorite) async {
|
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: Optional.present(isFavorite)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
||||||
return _api.updateAssets(
|
return _api.updateAssets(
|
||||||
AssetBulkUpdateDto(
|
AssetBulkUpdateDto(
|
||||||
@@ -77,11 +75,11 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
return _api.downloadAssetWithHttpInfo(id, edited: edited);
|
return _api.downloadAssetWithHttpInfo(id, edited: edited);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
api.AssetVisibility _mapVisibility(AssetVisibility visibility) => switch (visibility) {
|
||||||
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
AssetVisibility.timeline => api.AssetVisibility.timeline,
|
||||||
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
AssetVisibility.hidden => api.AssetVisibility.hidden,
|
||||||
AssetVisibilityEnum.locked => AssetVisibility.locked,
|
AssetVisibility.locked => api.AssetVisibility.locked,
|
||||||
AssetVisibilityEnum.archive => AssetVisibility.archive,
|
AssetVisibility.archive => api.AssetVisibility.archive,
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<String?> getAssetMIMEType(String assetId) async {
|
Future<String?> getAssetMIMEType(String assetId) async {
|
||||||
@@ -106,6 +104,20 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
Future<void> removeEdits(String assetId) async {
|
Future<void> removeEdits(String assetId) async {
|
||||||
return _api.removeAssetEdits(assetId);
|
return _api.removeAssetEdits(assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> update(
|
||||||
|
List<String> remoteIds, {
|
||||||
|
Option<bool> isFavorite = const .none(),
|
||||||
|
Option<AssetVisibility> visibility = const .none(),
|
||||||
|
}) {
|
||||||
|
return _api.updateAssets(
|
||||||
|
AssetBulkUpdateDto(
|
||||||
|
ids: remoteIds,
|
||||||
|
isFavorite: isFavorite.toOptional(),
|
||||||
|
visibility: visibility.map(_mapVisibility).toOptional(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on StackResponseDto {
|
extension on StackResponseDto {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
|
|
||||||
|
class ToastOption {
|
||||||
|
final Duration? timeout;
|
||||||
|
final FutureOr<void> Function()? onUndo;
|
||||||
|
|
||||||
|
const ToastOption({this.timeout, this.onUndo});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToastRepository {
|
||||||
|
const ToastRepository();
|
||||||
|
|
||||||
|
FutureOr<void> success(String message, {ToastOption? toast}) {
|
||||||
|
snackbar.success(message, duration: toast?.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<void> info(String message, {ToastOption? toast}) {
|
||||||
|
snackbar.info(message, duration: toast?.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<void> error(String message, {ToastOption? toast}) {
|
||||||
|
snackbar.error(message, duration: toast?.timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,28 +68,8 @@ class ActionService {
|
|||||||
unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds)));
|
unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> favorite(List<String> remoteIds) async {
|
|
||||||
await _assetApiRepository.updateFavorite(remoteIds, true);
|
|
||||||
await _remoteAssetRepository.updateFavorite(remoteIds, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> unFavorite(List<String> remoteIds) async {
|
|
||||||
await _assetApiRepository.updateFavorite(remoteIds, false);
|
|
||||||
await _remoteAssetRepository.updateFavorite(remoteIds, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> archive(List<String> remoteIds) async {
|
|
||||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.archive);
|
|
||||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.archive);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> unArchive(List<String> remoteIds) async {
|
|
||||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.timeline);
|
|
||||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> moveToLockFolder(List<String> remoteIds, List<String> localIds) async {
|
Future<void> moveToLockFolder(List<String> remoteIds, List<String> localIds) async {
|
||||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked);
|
await _assetApiRepository.updateVisibility(remoteIds, .locked);
|
||||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
||||||
|
|
||||||
// Ask user if they want to delete local copies
|
// Ask user if they want to delete local copies
|
||||||
@@ -99,7 +79,7 @@ class ActionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFromLockFolder(List<String> remoteIds) async {
|
Future<void> removeFromLockFolder(List<String> remoteIds) async {
|
||||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.timeline);
|
await _assetApiRepository.updateVisibility(remoteIds, .timeline);
|
||||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
|
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,11 +88,6 @@ class ActionService {
|
|||||||
await _remoteAssetRepository.trash(remoteIds);
|
await _remoteAssetRepository.trash(remoteIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreTrash(List<String> ids) async {
|
|
||||||
await _assetApiRepository.restoreTrash(ids);
|
|
||||||
await _remoteAssetRepository.restoreTrash(ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> emptyTrash(String userId) async {
|
Future<int> emptyTrash(String userId) async {
|
||||||
final count = await _assetApiRepository.emptyTrash();
|
final count = await _assetApiRepository.emptyTrash();
|
||||||
await _remoteAssetRepository.emptyTrash(userId);
|
await _remoteAssetRepository.emptyTrash(userId);
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
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:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
|
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/asset_debug.action.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/actions/restore.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/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/cast_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
@@ -21,7 +25,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_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/remove_from_album_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/restore_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_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/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_action_button.widget.dart';
|
||||||
@@ -29,8 +32,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_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/slideshow_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
@@ -186,13 +187,18 @@ enum ActionButtonType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildButton(
|
Widget buildButton(
|
||||||
ActionButtonContext context, [
|
ActionButtonContext context,
|
||||||
BuildContext? buildContext,
|
BuildContext buildContext,
|
||||||
|
|
||||||
|
WidgetRef ref, [
|
||||||
bool iconOnly = false,
|
bool iconOnly = false,
|
||||||
bool menuItem = false,
|
bool menuItem = false,
|
||||||
]) {
|
]) {
|
||||||
|
final scope = ActionScope.from(buildContext, ref);
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
ActionButtonType.advancedInfo => ActionMenuItemWidget(action: AssetDebugAction(assets: [context.asset])),
|
ActionButtonType.advancedInfo => ActionMenuItemWidget(
|
||||||
|
action: AssetDebugAction(assets: [context.asset], scope: scope),
|
||||||
|
),
|
||||||
ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.shareLink => ShareLinkActionButton(
|
ActionButtonType.shareLink => ShareLinkActionButton(
|
||||||
source: context.source,
|
source: context.source,
|
||||||
@@ -200,18 +206,13 @@ enum ActionButtonType {
|
|||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
),
|
),
|
||||||
ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.archive => ArchiveActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.archive || ActionButtonType.unarchive => ActionMenuItemWidget(
|
||||||
ActionButtonType.unarchive => UnArchiveActionButton(
|
action: ArchiveAction(assets: [context.asset], scope: scope),
|
||||||
source: context.source,
|
|
||||||
iconOnly: iconOnly,
|
|
||||||
menuItem: menuItem,
|
|
||||||
),
|
),
|
||||||
ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.restoreTrash => RestoreActionButton(
|
ActionButtonType.restoreTrash => ActionMenuItemWidget(
|
||||||
source: context.source,
|
action: RestoreAction(assets: [context.asset], scope: scope),
|
||||||
iconOnly: iconOnly,
|
|
||||||
menuItem: menuItem,
|
|
||||||
),
|
),
|
||||||
ActionButtonType.deletePermanent => DeletePermanentActionButton(
|
ActionButtonType.deletePermanent => DeletePermanentActionButton(
|
||||||
source: context.source,
|
source: context.source,
|
||||||
@@ -248,7 +249,9 @@ enum ActionButtonType {
|
|||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
),
|
),
|
||||||
ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.unstack => ActionMenuItemWidget(
|
||||||
|
action: StackAction(assets: [context.asset], scope: scope),
|
||||||
|
),
|
||||||
ActionButtonType.openInBrowser => OpenInBrowserActionButton(
|
ActionButtonType.openInBrowser => OpenInBrowserActionButton(
|
||||||
remoteId: context.asset.remoteId!,
|
remoteId: context.asset.remoteId!,
|
||||||
origin: context.timelineOrigin,
|
origin: context.timelineOrigin,
|
||||||
@@ -276,12 +279,10 @@ enum ActionButtonType {
|
|||||||
iconData: Icons.image_search,
|
iconData: Icons.image_search,
|
||||||
iconOnly: iconOnly,
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
onPressed: buildContext == null
|
onPressed: () async {
|
||||||
? null
|
await buildContext.router.navigate(const TabShellRoute(children: [MainTimelineRoute()]));
|
||||||
: () async {
|
EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt));
|
||||||
await buildContext.router.navigate(const TabShellRoute(children: [MainTimelineRoute()]));
|
},
|
||||||
EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||||
};
|
};
|
||||||
@@ -326,11 +327,14 @@ class ActionButtonBuilder {
|
|||||||
ActionButtonType.deletePermanent,
|
ActionButtonType.deletePermanent,
|
||||||
};
|
};
|
||||||
|
|
||||||
static List<Widget> build(ActionButtonContext context) {
|
static List<Widget> build(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) {
|
||||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
return _actionTypes
|
||||||
|
.where((type) => type.shouldShow(context))
|
||||||
|
.map((type) => type.buildButton(context, buildContext, ref))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext) {
|
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) {
|
||||||
final visibleButtons = defaultViewerKebabMenuOrder
|
final visibleButtons = defaultViewerKebabMenuOrder
|
||||||
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
|
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -346,7 +350,7 @@ class ActionButtonBuilder {
|
|||||||
if (lastGroup != null && type.kebabMenuGroup != lastGroup) {
|
if (lastGroup != null && type.kebabMenuGroup != lastGroup) {
|
||||||
result.add(const Divider(height: 1));
|
result.add(const Divider(height: 1));
|
||||||
}
|
}
|
||||||
result.add(type.buildButton(context, buildContext, false, true));
|
result.add(type.buildButton(context, buildContext, ref, false, true));
|
||||||
lastGroup = type.kebabMenuGroup;
|
lastGroup = type.kebabMenuGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
|
||||||
|
extension type const AssetFilter<T extends BaseAsset>(Iterable<T> assets) implements Iterable<T> {
|
||||||
|
AssetFilter<T> where(bool Function(T asset) test) => AssetFilter(assets.where(test));
|
||||||
|
AssetFilter<T> whereNot(bool Function(T asset) test) => AssetFilter(assets.where((asset) => !test(asset)));
|
||||||
|
|
||||||
|
AssetFilter<T> type(AssetType type) => where((asset) => asset.type == type);
|
||||||
|
AssetFilter<T> favorite({bool isFavorite = true}) => where((asset) => asset.isFavorite == isFavorite);
|
||||||
|
|
||||||
|
AssetFilter<RemoteAsset> remote() => AssetFilter(assets.whereType<RemoteAsset>());
|
||||||
|
AssetFilter<RemoteAsset> owned(String ownerId) => remote().where((asset) => asset.ownerId == ownerId);
|
||||||
|
AssetFilter<RemoteAsset> visibility(AssetVisibility visibility) =>
|
||||||
|
remote().where((asset) => asset.visibility == visibility);
|
||||||
|
AssetFilter<RemoteAsset> notVisibility(AssetVisibility visibility) =>
|
||||||
|
remote().where((asset) => asset.visibility != visibility);
|
||||||
|
AssetFilter<RemoteAsset> archived({bool isArchived = true}) =>
|
||||||
|
remote().where((asset) => asset.isArchived == isArchived);
|
||||||
|
AssetFilter<RemoteAsset> stacked({bool isStacked = true}) => remote().where((asset) => asset.isStacked == isStacked);
|
||||||
|
AssetFilter<RemoteAsset> trashed({bool isTrashed = true}) => remote().where((asset) => asset.isTrashed == isTrashed);
|
||||||
|
|
||||||
|
AssetFilter<LocalAsset> local() => AssetFilter(assets.whereType<LocalAsset>());
|
||||||
|
AssetFilter<LocalAsset> backedUp() => local().where((asset) => asset.remoteAssetId != null);
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
|
|||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
import 'package:stack_trace/stack_trace.dart';
|
import 'package:stack_trace/stack_trace.dart';
|
||||||
|
|
||||||
void handleError(BuildContext context, Object error, {StackTrace? stack, String? description}) {
|
void handleError(Object error, {StackTrace? stack, String? description}) {
|
||||||
String? stackTrace;
|
String? stackTrace;
|
||||||
if (stack != null) {
|
if (stack != null) {
|
||||||
final trace = Trace.from(stack);
|
final trace = Trace.from(stack);
|
||||||
@@ -23,17 +23,13 @@ void handleError(BuildContext context, Object error, {StackTrace? stack, String?
|
|||||||
() => 'Error${description != null ? ' ($description)' : ''}: $error${stackTrace != null ? '\n$stackTrace' : ''}',
|
() => 'Error${description != null ? ' ($description)' : ''}: $error${stackTrace != null ? '\n$stackTrace' : ''}',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
if (serverErrorMessage(error) case String serverMessage) {
|
if (serverErrorMessage(error) case String serverMessage) {
|
||||||
message = serverMessage;
|
message = serverMessage;
|
||||||
} else if (isConnectionError(error)) {
|
} else if (isConnectionError(error)) {
|
||||||
message = context.t.login_form_server_error;
|
message = StaticTranslations.instance.login_form_server_error;
|
||||||
} else {
|
} else {
|
||||||
message = context.t.scaffold_body_error_occurred;
|
message = StaticTranslations.instance.scaffold_body_error_occurred;
|
||||||
}
|
}
|
||||||
|
|
||||||
snackbar.error(message);
|
snackbar.error(message);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:openapi/api.dart' show Optional;
|
import 'package:openapi/api.dart' show Optional;
|
||||||
|
|
||||||
sealed class Option<T> {
|
sealed class Option<T> {
|
||||||
@@ -21,6 +22,11 @@ sealed class Option<T> {
|
|||||||
None() => null,
|
None() => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Option<U> map<U>(U Function(T value) f) => switch (this) {
|
||||||
|
Some(:final value) => Some(f(value)),
|
||||||
|
None() => None<U>(),
|
||||||
|
};
|
||||||
|
|
||||||
U fold<U>(U Function(T value) onSome, U Function() onNone) => switch (this) {
|
U fold<U>(U Function(T value) onSome, U Function() onNone) => switch (this) {
|
||||||
Some(:final value) => onSome(value),
|
Some(:final value) => onSome(value),
|
||||||
None() => onNone(),
|
None() => onNone(),
|
||||||
@@ -65,3 +71,10 @@ extension OptionToOptional<T> on Option<T> {
|
|||||||
Some(:final value) => Optional.present(value),
|
Some(:final value) => Optional.present(value),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension OptionToDriftValue<T> on Option<T> {
|
||||||
|
Value<T> toDriftValue() => switch (this) {
|
||||||
|
Some(:final value) => Value(value),
|
||||||
|
None() => const Value.absent(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,18 +6,23 @@ final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
|||||||
class SnackbarManager {
|
class SnackbarManager {
|
||||||
const SnackbarManager();
|
const SnackbarManager();
|
||||||
|
|
||||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? show(String message, SnackbarType type) {
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? show(
|
||||||
|
String message,
|
||||||
|
SnackbarType type, {
|
||||||
|
Duration? duration,
|
||||||
|
}) {
|
||||||
final messenger = scaffoldMessengerKey.currentState;
|
final messenger = scaffoldMessengerKey.currentState;
|
||||||
final context = scaffoldMessengerKey.currentContext;
|
final context = scaffoldMessengerKey.currentContext;
|
||||||
if (messenger == null || context == null) {
|
if (messenger == null || context == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duration ??= const .new(seconds: 4);
|
||||||
messenger.hideCurrentSnackBar();
|
messenger.hideCurrentSnackBar();
|
||||||
return messenger.showSnackBar(_build(context, message, type));
|
return messenger.showSnackBar(_build(context, message, type, duration));
|
||||||
}
|
}
|
||||||
|
|
||||||
SnackBar _build(BuildContext context, String message, SnackbarType type) {
|
SnackBar _build(BuildContext context, String message, SnackbarType type, Duration duration) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colors = theme.extension<ImmichColors>() ?? ImmichColors.harmonized(theme.colorScheme);
|
final colors = theme.extension<ImmichColors>() ?? ImmichColors.harmonized(theme.colorScheme);
|
||||||
final (IconData icon, Color background, Color foreground) = switch (type) {
|
final (IconData icon, Color background, Color foreground) = switch (type) {
|
||||||
@@ -29,7 +34,7 @@ class SnackbarManager {
|
|||||||
return SnackBar(
|
return SnackBar(
|
||||||
behavior: .floating,
|
behavior: .floating,
|
||||||
backgroundColor: background,
|
backgroundColor: background,
|
||||||
duration: const .new(seconds: 4),
|
duration: duration,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: .all(.circular(ImmichRadius.sm))),
|
shape: const RoundedRectangleBorder(borderRadius: .all(.circular(ImmichRadius.sm))),
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -48,11 +53,14 @@ class SnackbarManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(String message) => show(message, .info);
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(String message, {Duration? duration}) =>
|
||||||
|
show(message, .info, duration: duration);
|
||||||
|
|
||||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(String message) => show(message, .success);
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(String message, {Duration? duration}) =>
|
||||||
|
show(message, .success, duration: duration);
|
||||||
|
|
||||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(String message) => show(message, .error);
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(String message, {Duration? duration}) =>
|
||||||
|
show(message, .error, duration: duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
const snackbar = SnackbarManager();
|
const snackbar = SnackbarManager();
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||||
@@ -15,6 +15,7 @@ import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.re
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/toast.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
@@ -50,6 +51,8 @@ class MockUserRepository extends Mock implements UserRepository {}
|
|||||||
|
|
||||||
class MockPartnerRepository extends Mock implements PartnerRepository {}
|
class MockPartnerRepository extends Mock implements PartnerRepository {}
|
||||||
|
|
||||||
|
class MockToastRepository extends Mock implements ToastRepository {}
|
||||||
|
|
||||||
// API Repos
|
// API Repos
|
||||||
class MockUserApiRepository extends Mock implements UserApiRepository {}
|
class MockUserApiRepository extends Mock implements UserApiRepository {}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ import '../../utils.dart';
|
|||||||
class RemoteAssetFactory {
|
class RemoteAssetFactory {
|
||||||
const RemoteAssetFactory();
|
const RemoteAssetFactory();
|
||||||
|
|
||||||
static RemoteAsset create({String? id, String? name, String? ownerId, bool isFavorite = false}) {
|
static RemoteAsset create({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? ownerId,
|
||||||
|
bool isFavorite = false,
|
||||||
|
AssetVisibility visibility = AssetVisibility.timeline,
|
||||||
|
String? stackId,
|
||||||
|
DateTime? deletedAt,
|
||||||
|
}) {
|
||||||
id = TestUtils.uuid(id);
|
id = TestUtils.uuid(id);
|
||||||
|
|
||||||
return RemoteAsset(
|
return RemoteAsset(
|
||||||
@@ -17,7 +25,10 @@ class RemoteAssetFactory {
|
|||||||
createdAt: TestUtils.yesterday(),
|
createdAt: TestUtils.yesterday(),
|
||||||
updatedAt: TestUtils.now(),
|
updatedAt: TestUtils.now(),
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
|
visibility: visibility,
|
||||||
|
stackId: stackId,
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
|
deletedAt: deletedAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
import 'package:mocktail/mocktail.dart' as mock;
|
import 'package:mocktail/mocktail.dart' as mock;
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ class RepositoryMocks {
|
|||||||
final localAlbum = LocalAlbumRepositoryStub(MockLocalAlbumRepository());
|
final localAlbum = LocalAlbumRepositoryStub(MockLocalAlbumRepository());
|
||||||
final localAsset = LocalAssetRepositoryStub(MockDriftLocalAssetRepository());
|
final localAsset = LocalAssetRepositoryStub(MockDriftLocalAssetRepository());
|
||||||
final trashedAsset = MockTrashedLocalAssetRepository();
|
final trashedAsset = MockTrashedLocalAssetRepository();
|
||||||
|
final toast = MockToastRepository();
|
||||||
|
|
||||||
final nativeApi = NativeSyncApiStub(MockNativeSyncApi());
|
final nativeApi = NativeSyncApiStub(MockNativeSyncApi());
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ class RepositoryMocks {
|
|||||||
localAsset.reset();
|
localAsset.reset();
|
||||||
reset(trashedAsset);
|
reset(trashedAsset);
|
||||||
nativeApi.reset();
|
nativeApi.reset();
|
||||||
|
reset(toast);
|
||||||
_stubLocalAlbumRepository();
|
_stubLocalAlbumRepository();
|
||||||
_stubLocalAssetRepository();
|
_stubLocalAssetRepository();
|
||||||
_stubNativeSyncApi();
|
_stubNativeSyncApi();
|
||||||
@@ -88,7 +91,10 @@ class ServiceMocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _stubAssetService() {
|
void _stubAssetService() {
|
||||||
when(asset.updateFavorite).thenAnswer((_) async {});
|
when(asset.update).thenAnswer((_) async {});
|
||||||
|
when(asset.stack).thenAnswer((_) async {});
|
||||||
|
when(asset.unstack).thenAnswer((_) async {});
|
||||||
|
when(asset.restoreTrash).thenAnswer((_) async {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +102,9 @@ void _registerFallbacks() {
|
|||||||
registerFallbackValue(LocalAlbumFactory.create());
|
registerFallbackValue(LocalAlbumFactory.create());
|
||||||
registerFallbackValue(LocalAssetFactory.create());
|
registerFallbackValue(LocalAssetFactory.create());
|
||||||
registerFallbackValue(Uint8List(0));
|
registerFallbackValue(Uint8List(0));
|
||||||
|
registerFallbackValue(AssetVisibility.timeline);
|
||||||
|
registerFallbackValue(const Option<bool>.none());
|
||||||
|
registerFallbackValue(const Option<AssetVisibility>.none());
|
||||||
}
|
}
|
||||||
|
|
||||||
extension type const Stub<T extends Mock>(T mockedClass) {
|
extension type const Stub<T extends Mock>(T mockedClass) {
|
||||||
@@ -165,8 +174,21 @@ extension type const UserServiceStub(MockUserService service) implements Stub<Mo
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension type const AssetServiceStub(MockAssetService service) implements Stub<MockAssetService> {
|
extension type const AssetServiceStub(MockAssetService service) implements Stub<MockAssetService> {
|
||||||
Future<void> Function() get updateFavorite =>
|
Future<void> Function() get update =>
|
||||||
() => service.updateFavorite(any(), any());
|
() => service.update(
|
||||||
|
any(),
|
||||||
|
isFavorite: any(named: 'isFavorite'),
|
||||||
|
visibility: any(named: 'visibility'),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> Function() get stack =>
|
||||||
|
() => service.stack(any(), any());
|
||||||
|
|
||||||
|
Future<void> Function() get unstack =>
|
||||||
|
() => service.unstack(any());
|
||||||
|
|
||||||
|
Future<void> Function() get restoreTrash =>
|
||||||
|
() => service.restoreTrash(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
extension type const NativeSyncApiStub(MockNativeSyncApi api) implements Stub<MockNativeSyncApi> {
|
extension type const NativeSyncApiStub(MockNativeSyncApi api) implements Stub<MockNativeSyncApi> {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.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/archive.action.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../../domain/service.mock.dart';
|
||||||
|
import '../../factories/remote_asset_factory.dart';
|
||||||
|
import '../presentation_context.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late PresentationContext context;
|
||||||
|
late MockAssetService assetService;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
context = await PresentationContext.create();
|
||||||
|
assetService = context.service.asset.service;
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
context.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
RemoteAsset owned({AssetVisibility visibility = AssetVisibility.timeline}) =>
|
||||||
|
RemoteAssetFactory.create(ownerId: context.currentUser.id, visibility: visibility);
|
||||||
|
|
||||||
|
group('ArchiveAction', () {
|
||||||
|
testWidgets('archives the eligible owned assets', (tester) async {
|
||||||
|
final asset = owned();
|
||||||
|
final action = await tester.pumpTestAction(context, (scope) => ArchiveAction(assets: [asset], scope: scope));
|
||||||
|
|
||||||
|
expect(action.icon, Icons.archive_outlined);
|
||||||
|
expect(action.label, StaticTranslations.instance.archive);
|
||||||
|
|
||||||
|
verify(() => assetService.update([asset.id], visibility: const Some(AssetVisibility.archive))).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('unarchive the eligible owned assets', (tester) async {
|
||||||
|
final asset = owned(visibility: .archive);
|
||||||
|
final action = await tester.pumpTestAction(context, (scope) => ArchiveAction(assets: [asset], scope: scope));
|
||||||
|
|
||||||
|
expect(action.icon, Icons.unarchive_outlined);
|
||||||
|
expect(action.label, StaticTranslations.instance.unarchive);
|
||||||
|
|
||||||
|
verify(() => assetService.update([asset.id], visibility: const Some(AssetVisibility.timeline))).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('dispatches on owned state, ignoring assets owned by others', (tester) async {
|
||||||
|
final mine = owned(visibility: .archive);
|
||||||
|
final theirs = RemoteAssetFactory.create();
|
||||||
|
final action = await tester.pumpTestAction(
|
||||||
|
context,
|
||||||
|
(scope) => ArchiveAction(assets: [mine, theirs], scope: scope),
|
||||||
|
);
|
||||||
|
expect(action.label, StaticTranslations.instance.unarchive);
|
||||||
|
|
||||||
|
verify(() => assetService.update([mine.id], visibility: const Some(AssetVisibility.timeline))).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('batches every eligible owned asset into a single call', (tester) async {
|
||||||
|
final first = owned();
|
||||||
|
final second = owned();
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => ArchiveAction(assets: [first, second], scope: scope));
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => assetService.update([first.id, second.id], visibility: const Some(AssetVisibility.archive)),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('archives only the owned assets not already archived', (tester) async {
|
||||||
|
final stale = owned();
|
||||||
|
final alreadyArchived = owned(visibility: .archive);
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => ArchiveAction(assets: [stale, alreadyArchived], scope: scope));
|
||||||
|
|
||||||
|
verify(() => assetService.update([stale.id], visibility: const Some(AssetVisibility.archive))).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('reports the archived count through the toast repository', (tester) async {
|
||||||
|
final toast = context.repository.toast;
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => ArchiveAction(assets: [owned(), owned()], scope: scope));
|
||||||
|
|
||||||
|
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||||
|
expect(message, StaticTranslations.instance.archive_action_prompt(count: 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('reports the unarchive count through the toast repository', (tester) async {
|
||||||
|
final toast = context.repository.toast;
|
||||||
|
|
||||||
|
await tester.pumpTestAction(
|
||||||
|
context,
|
||||||
|
(scope) => ArchiveAction(
|
||||||
|
assets: [
|
||||||
|
owned(visibility: .archive),
|
||||||
|
owned(visibility: .archive),
|
||||||
|
],
|
||||||
|
scope: scope,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||||
|
expect(message, StaticTranslations.instance.unarchive_action_prompt(count: 2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||||
import 'package:immich_ui/immich_ui.dart';
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
|
|
||||||
@@ -22,20 +21,18 @@ void main() {
|
|||||||
|
|
||||||
group('AssetDebugAction', () {
|
group('AssetDebugAction', () {
|
||||||
testWidgets('visible for a single asset when advanced troubleshooting is on', (tester) async {
|
testWidgets('visible for a single asset when advanced troubleshooting is on', (tester) async {
|
||||||
await tester.pumpTestWidget(
|
await tester.pumpActionButton(
|
||||||
context,
|
context,
|
||||||
ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])),
|
(scope) => AssetDebugAction(assets: [RemoteAssetFactory.create()], scope: scope),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.byType(ImmichIconButton), findsOneWidget);
|
expect(find.byType(ImmichIconButton), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('hidden for multiple assets', (tester) async {
|
testWidgets('hidden for multiple assets', (tester) async {
|
||||||
await tester.pumpTestWidget(
|
await tester.pumpActionButton(
|
||||||
context,
|
context,
|
||||||
ActionIconButtonWidget(
|
(scope) => AssetDebugAction(assets: [RemoteAssetFactory.create(), RemoteAssetFactory.create()], scope: scope),
|
||||||
action: AssetDebugAction(assets: [RemoteAssetFactory.create(), RemoteAssetFactory.create()]),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.byType(ImmichIconButton), findsNothing);
|
expect(find.byType(ImmichIconButton), findsNothing);
|
||||||
@@ -43,9 +40,9 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('hidden when advanced troubleshooting is off', (tester) async {
|
testWidgets('hidden when advanced troubleshooting is off', (tester) async {
|
||||||
await StoreService.I.put(StoreKey.advancedTroubleshooting, false);
|
await StoreService.I.put(StoreKey.advancedTroubleshooting, false);
|
||||||
await tester.pumpTestWidget(
|
await tester.pumpActionButton(
|
||||||
context,
|
context,
|
||||||
ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])),
|
(scope) => AssetDebugAction(assets: [RemoteAssetFactory.create()], scope: scope),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.byType(ImmichIconButton), findsNothing);
|
expect(find.byType(ImmichIconButton), findsNothing);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.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/favorite.action.dart';
|
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../../../domain/service.mock.dart';
|
import '../../../domain/service.mock.dart';
|
||||||
@@ -27,52 +29,73 @@ void main() {
|
|||||||
group('FavoriteAction', () {
|
group('FavoriteAction', () {
|
||||||
testWidgets('favorites the eligible owned assets', (tester) async {
|
testWidgets('favorites the eligible owned assets', (tester) async {
|
||||||
final asset = owned();
|
final asset = owned();
|
||||||
|
final action = await tester.pumpTestAction(context, (scope) => FavoriteAction(assets: [asset], scope: scope));
|
||||||
|
|
||||||
await tester.pumpTestAction(context, FavoriteAction(assets: [asset]));
|
expect(action.icon, Icons.favorite_border_rounded);
|
||||||
|
expect(action.label, StaticTranslations.instance.favorite);
|
||||||
|
|
||||||
verify(() => assetService.updateFavorite([asset.id], true)).called(1);
|
verify(() => assetService.update([asset.id], isFavorite: const Some(true))).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('unfavorite the eligible owned assets', (tester) async {
|
testWidgets('unfavorite the eligible owned assets', (tester) async {
|
||||||
final asset = owned(isFavorite: true);
|
final asset = owned(isFavorite: true);
|
||||||
|
final action = await tester.pumpTestAction(context, (scope) => FavoriteAction(assets: [asset], scope: scope));
|
||||||
|
|
||||||
await tester.pumpTestAction(context, FavoriteAction(assets: [asset]));
|
expect(action.icon, Icons.favorite_rounded);
|
||||||
|
expect(action.label, StaticTranslations.instance.unfavorite);
|
||||||
|
|
||||||
verify(() => assetService.updateFavorite([asset.id], false)).called(1);
|
verify(() => assetService.update([asset.id], isFavorite: const Some(false))).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('ignores assets owned by someone else', (tester) async {
|
testWidgets('dispatches on owned state, ignoring assets owned by others', (tester) async {
|
||||||
final mine = owned();
|
final mine = owned(isFavorite: true);
|
||||||
final theirs = RemoteAssetFactory.create();
|
final theirs = RemoteAssetFactory.create();
|
||||||
|
final action = await tester.pumpTestAction(
|
||||||
|
context,
|
||||||
|
(scope) => FavoriteAction(assets: [mine, theirs], scope: scope),
|
||||||
|
);
|
||||||
|
expect(action.label, StaticTranslations.instance.unfavorite);
|
||||||
|
|
||||||
await tester.pumpTestAction(context, FavoriteAction(assets: [mine, theirs]));
|
verify(() => assetService.update([mine.id], isFavorite: const Some(false))).called(1);
|
||||||
|
|
||||||
verify(() => assetService.updateFavorite([mine.id], true)).called(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('batches every eligible owned asset into a single call', (tester) async {
|
testWidgets('batches every eligible owned asset into a single call', (tester) async {
|
||||||
final first = owned();
|
final first = owned();
|
||||||
final second = owned();
|
final second = owned();
|
||||||
|
|
||||||
await tester.pumpTestAction(context, FavoriteAction(assets: [first, second]));
|
await tester.pumpTestAction(context, (scope) => FavoriteAction(assets: [first, second], scope: scope));
|
||||||
|
|
||||||
verify(() => assetService.updateFavorite([first.id, second.id], true)).called(1);
|
verify(() => assetService.update([first.id, second.id], isFavorite: const Some(true))).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('skips owned assets already in the target state', (tester) async {
|
testWidgets('favorites only the owned assets not already favorite', (tester) async {
|
||||||
final stale = owned();
|
final stale = owned();
|
||||||
final alreadyFavorite = owned(isFavorite: true);
|
final alreadyFavorite = owned(isFavorite: true);
|
||||||
|
|
||||||
await tester.pumpTestAction(context, FavoriteAction(assets: [stale, alreadyFavorite]));
|
await tester.pumpTestAction(context, (scope) => FavoriteAction(assets: [stale, alreadyFavorite], scope: scope));
|
||||||
|
|
||||||
verify(() => assetService.updateFavorite([stale.id], true)).called(1);
|
verify(() => assetService.update([stale.id], isFavorite: const Some(true))).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('shows a confirmation snackbar on success', (tester) async {
|
testWidgets('reports the favorite count through the toast repository', (tester) async {
|
||||||
await tester.pumpTestAction(context, FavoriteAction(assets: [owned()]));
|
final toast = context.repository.toast;
|
||||||
await tester.pumpUntilFound(find.byType(SnackBar));
|
|
||||||
|
|
||||||
expect(find.byType(SnackBar), findsOneWidget);
|
await tester.pumpTestAction(context, (scope) => FavoriteAction(assets: [owned(), owned()], scope: scope));
|
||||||
|
|
||||||
|
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||||
|
expect(message, StaticTranslations.instance.favorite_action_prompt(count: 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('reports the unfavorite count through the toast repository', (tester) async {
|
||||||
|
final toast = context.repository.toast;
|
||||||
|
|
||||||
|
await tester.pumpTestAction(
|
||||||
|
context,
|
||||||
|
(scope) => FavoriteAction(assets: [owned(isFavorite: true), owned(isFavorite: true)], scope: scope),
|
||||||
|
);
|
||||||
|
|
||||||
|
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||||
|
expect(message, StaticTranslations.instance.unfavorite_action_prompt(count: 2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ void main() {
|
|||||||
testWidgets('creates a partner for the selected candidate', (tester) async {
|
testWidgets('creates a partner for the selected candidate', (tester) async {
|
||||||
final candidate = UserFactory.create();
|
final candidate = UserFactory.create();
|
||||||
|
|
||||||
await tester.pumpTestAction(context, const PartnerAddAction(), overrides: overrides(candidates: [candidate]));
|
await tester.pumpTestAction(
|
||||||
|
context,
|
||||||
|
(scope) => PartnerAddAction(scope: scope),
|
||||||
|
overrides: overrides(candidates: [candidate]),
|
||||||
|
);
|
||||||
await tester.pumpUntilFound(find.text(candidate.name));
|
await tester.pumpUntilFound(find.text(candidate.name));
|
||||||
await tester.tap(find.text(candidate.name));
|
await tester.tap(find.text(candidate.name));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -42,7 +46,7 @@ void main() {
|
|||||||
testWidgets('creates nothing when the selection dialog is dismissed', (tester) async {
|
testWidgets('creates nothing when the selection dialog is dismissed', (tester) async {
|
||||||
await tester.pumpTestAction(
|
await tester.pumpTestAction(
|
||||||
context,
|
context,
|
||||||
const PartnerAddAction(),
|
(scope) => PartnerAddAction(scope: scope),
|
||||||
overrides: overrides(candidates: [UserFactory.create()]),
|
overrides: overrides(candidates: [UserFactory.create()]),
|
||||||
);
|
);
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape); // dismiss without selecting
|
await tester.sendKeyEvent(LogicalKeyboardKey.escape); // dismiss without selecting
|
||||||
@@ -57,7 +61,7 @@ void main() {
|
|||||||
final partner = UserFactory.create();
|
final partner = UserFactory.create();
|
||||||
await tester.pumpTestAction(
|
await tester.pumpTestAction(
|
||||||
context,
|
context,
|
||||||
PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name),
|
(scope) => PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name, scope: scope),
|
||||||
overrides: overrides(),
|
overrides: overrides(),
|
||||||
);
|
);
|
||||||
await tester.tap(find.byType(TextButton).last); // confirm
|
await tester.tap(find.byType(TextButton).last); // confirm
|
||||||
@@ -70,7 +74,7 @@ void main() {
|
|||||||
final partner = UserFactory.create();
|
final partner = UserFactory.create();
|
||||||
await tester.pumpTestAction(
|
await tester.pumpTestAction(
|
||||||
context,
|
context,
|
||||||
PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name),
|
(scope) => PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name, scope: scope),
|
||||||
overrides: overrides(),
|
overrides: overrides(),
|
||||||
);
|
);
|
||||||
await tester.tap(find.byType(TextButton).first); // cancel
|
await tester.tap(find.byType(TextButton).first); // cancel
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.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/restore.action.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../../domain/service.mock.dart';
|
||||||
|
import '../../factories/remote_asset_factory.dart';
|
||||||
|
import '../presentation_context.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late PresentationContext context;
|
||||||
|
late MockAssetService assetService;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
context = await PresentationContext.create();
|
||||||
|
assetService = context.service.asset.service;
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
context.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
RemoteAsset owned({bool trashed = true}) =>
|
||||||
|
RemoteAssetFactory.create(ownerId: context.currentUser.id, deletedAt: trashed ? DateTime(2020) : null);
|
||||||
|
|
||||||
|
group('RestoreAction', () {
|
||||||
|
testWidgets('restores the eligible owned trashed assets', (tester) async {
|
||||||
|
final asset = owned();
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => RestoreAction(assets: [asset], scope: scope));
|
||||||
|
|
||||||
|
verify(() => assetService.restoreTrash([asset.id])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ignores assets owned by someone else', (tester) async {
|
||||||
|
final mine = owned();
|
||||||
|
final theirs = RemoteAssetFactory.create(deletedAt: DateTime(2020));
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => RestoreAction(assets: [mine, theirs], scope: scope));
|
||||||
|
|
||||||
|
verify(() => assetService.restoreTrash([mine.id])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('restores only the owned assets that are trashed', (tester) async {
|
||||||
|
final trashed = owned();
|
||||||
|
final live = owned(trashed: false);
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => RestoreAction(assets: [trashed, live], scope: scope));
|
||||||
|
|
||||||
|
verify(() => assetService.restoreTrash([trashed.id])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('batches every eligible owned asset into a single call', (tester) async {
|
||||||
|
final first = owned();
|
||||||
|
final second = owned();
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => RestoreAction(assets: [first, second], scope: scope));
|
||||||
|
|
||||||
|
verify(() => assetService.restoreTrash([first.id, second.id])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('reports success through the toast repository with the restored count', (tester) async {
|
||||||
|
final toast = context.repository.toast;
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => RestoreAction(assets: [owned(), owned()], scope: scope));
|
||||||
|
|
||||||
|
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||||
|
expect(message, StaticTranslations.instance.assets_restored_count(count: 2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.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/stack.action.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../../domain/service.mock.dart';
|
||||||
|
import '../../factories/remote_asset_factory.dart';
|
||||||
|
import '../presentation_context.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late PresentationContext context;
|
||||||
|
late MockAssetService assetService;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
context = await PresentationContext.create();
|
||||||
|
assetService = context.service.asset.service;
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
context.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
RemoteAsset owned({String? stackId}) => RemoteAssetFactory.create(ownerId: context.currentUser.id, stackId: stackId);
|
||||||
|
|
||||||
|
group('StackAction', () {
|
||||||
|
testWidgets('stacks the eligible owned assets', (tester) async {
|
||||||
|
final first = owned();
|
||||||
|
final second = owned();
|
||||||
|
final action = await tester.pumpTestAction(
|
||||||
|
context,
|
||||||
|
(scope) => StackAction(assets: [first, second], scope: scope),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(action.icon, Icons.filter_none_rounded);
|
||||||
|
expect(action.label, StaticTranslations.instance.stack);
|
||||||
|
|
||||||
|
verify(() => assetService.stack(context.currentUser.id, [first.id, second.id])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('unstacks the eligible owned assets', (tester) async {
|
||||||
|
final asset = owned(stackId: 'stack');
|
||||||
|
final action = await tester.pumpTestAction(context, (scope) => StackAction(assets: [asset], scope: scope));
|
||||||
|
|
||||||
|
expect(action.icon, Icons.layers_clear_outlined);
|
||||||
|
expect(action.label, StaticTranslations.instance.unstack);
|
||||||
|
|
||||||
|
verify(() => assetService.unstack(['stack'])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('prioritizes stack when the owned selection is mixed', (tester) async {
|
||||||
|
final first = owned();
|
||||||
|
final second = owned(stackId: 'stack');
|
||||||
|
final action = await tester.pumpTestAction(
|
||||||
|
context,
|
||||||
|
(scope) => StackAction(assets: [first, second], scope: scope),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(action.label, StaticTranslations.instance.stack);
|
||||||
|
|
||||||
|
verify(() => assetService.stack(context.currentUser.id, [first.id, second.id])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('dispatches on owned state, ignoring assets owned by others', (tester) async {
|
||||||
|
final mine = owned();
|
||||||
|
final other = owned();
|
||||||
|
final theirs = RemoteAssetFactory.create();
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => StackAction(assets: [mine, other, theirs], scope: scope));
|
||||||
|
|
||||||
|
verify(() => assetService.stack(context.currentUser.id, [mine.id, other.id])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('unstacks every selected stack in a single call', (tester) async {
|
||||||
|
final first = owned(stackId: 'stack-1');
|
||||||
|
final second = owned(stackId: 'stack-2');
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => StackAction(assets: [first, second], scope: scope));
|
||||||
|
|
||||||
|
verify(() => assetService.unstack(['stack-1', 'stack-2'])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('reports the stacked count through the toast repository', (tester) async {
|
||||||
|
final toast = context.repository.toast;
|
||||||
|
|
||||||
|
await tester.pumpTestAction(context, (scope) => StackAction(assets: [owned(), owned()], scope: scope));
|
||||||
|
|
||||||
|
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||||
|
expect(message, StaticTranslations.instance.stacked_assets_count(count: 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('reports the unstacked count through the toast repository', (tester) async {
|
||||||
|
final toast = context.repository.toast;
|
||||||
|
|
||||||
|
await tester.pumpTestAction(
|
||||||
|
context,
|
||||||
|
(scope) => StackAction(
|
||||||
|
assets: [
|
||||||
|
owned(stackId: 'stack-1'),
|
||||||
|
owned(stackId: 'stack-2'),
|
||||||
|
],
|
||||||
|
scope: scope,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||||
|
expect(message, StaticTranslations.instance.unstacked_assets_count(count: 2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -10,25 +10,16 @@ import '../../factories/remote_asset_factory.dart';
|
|||||||
import '../presentation_context.dart';
|
import '../presentation_context.dart';
|
||||||
|
|
||||||
class _FakeAction extends BaseAction {
|
class _FakeAction extends BaseAction {
|
||||||
_FakeAction({this.visible = true, this.error});
|
_FakeAction({required super.scope, bool visible = true, this.error})
|
||||||
|
: super(icon: Icons.bolt, label: 'fake', isVisible: visible);
|
||||||
|
|
||||||
final bool visible;
|
|
||||||
final Object? error;
|
final Object? error;
|
||||||
|
|
||||||
bool ran = false;
|
bool ran = false;
|
||||||
bool? selectionDuringOnAction;
|
bool? selectionDuringOnAction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IconData get icon => Icons.bolt;
|
Future<void> onAction() async {
|
||||||
|
|
||||||
@override
|
|
||||||
String label(ActionScope scope) => 'fake';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isVisible(ActionScope scope) => visible;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onAction(ActionScope scope) async {
|
|
||||||
ran = true;
|
ran = true;
|
||||||
selectionDuringOnAction = scope.ref.read(multiSelectProvider).isEnabled;
|
selectionDuringOnAction = scope.ref.read(multiSelectProvider).isEnabled;
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
@@ -75,9 +66,9 @@ void main() {
|
|||||||
|
|
||||||
group('TimelineAction', () {
|
group('TimelineAction', () {
|
||||||
testWidgets('runs the wrapped action and then clears the selection', (tester) async {
|
testWidgets('runs the wrapped action and then clears the selection', (tester) async {
|
||||||
final inner = _FakeAction();
|
|
||||||
final (scope, container) = await pumpScope(tester);
|
final (scope, container) = await pumpScope(tester);
|
||||||
await TimelineAction(action: inner).onAction(scope);
|
final inner = _FakeAction(scope: scope);
|
||||||
|
await TimelineAction(action: inner).onAction();
|
||||||
|
|
||||||
expect(inner.ran, isTrue);
|
expect(inner.ran, isTrue);
|
||||||
expect(inner.selectionDuringOnAction, isTrue, reason: 'reset must run after the inner action, not before');
|
expect(inner.selectionDuringOnAction, isTrue, reason: 'reset must run after the inner action, not before');
|
||||||
@@ -86,19 +77,19 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('rethrows and keeps the selection when the wrapped action throws', (tester) async {
|
testWidgets('rethrows and keeps the selection when the wrapped action throws', (tester) async {
|
||||||
final error = Exception('boom');
|
final error = Exception('boom');
|
||||||
final inner = _FakeAction(error: error);
|
|
||||||
final (scope, container) = await pumpScope(tester);
|
final (scope, container) = await pumpScope(tester);
|
||||||
|
final inner = _FakeAction(scope: scope, error: error);
|
||||||
|
|
||||||
await expectLater(TimelineAction(action: inner).onAction(scope), throwsA(same(error)));
|
await expectLater(TimelineAction(action: inner).onAction(), throwsA(same(error)));
|
||||||
|
|
||||||
expect(inner.ran, isTrue);
|
expect(inner.ran, isTrue);
|
||||||
expect(container.read(multiSelectProvider).isEnabled, isTrue);
|
expect(container.read(multiSelectProvider).isEnabled, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('delegates visibility to the wrapped action', (tester) async {
|
testWidgets('delegates visibility to the wrapped action', (tester) async {
|
||||||
await tester.pumpTestWidget(
|
await tester.pumpActionButton(
|
||||||
context,
|
context,
|
||||||
ActionIconButtonWidget(action: TimelineAction(action: _FakeAction(visible: false))),
|
(scope) => TimelineAction(action: _FakeAction(scope: scope, visible: false)),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.byType(ActionIconButtonWidget), findsOneWidget);
|
expect(find.byType(ActionIconButtonWidget), findsOneWidget);
|
||||||
|
|||||||
@@ -17,12 +17,10 @@ void main() {
|
|||||||
|
|
||||||
group('PartnerSharedByList', () {
|
group('PartnerSharedByList', () {
|
||||||
testWidgets('shows the empty-state add button when there are no partners', (tester) async {
|
testWidgets('shows the empty-state add button when there are no partners', (tester) async {
|
||||||
final action = const PartnerAddAction();
|
|
||||||
|
|
||||||
await tester.pumpTestWidget(context, const PartnerSharedByList(partners: []));
|
await tester.pumpTestWidget(context, const PartnerSharedByList(partners: []));
|
||||||
|
|
||||||
expect(find.byType(ListView), findsNothing);
|
expect(find.byType(ListView), findsNothing);
|
||||||
expect(find.widgetWithIcon(TextButton, action.icon), findsOneWidget);
|
expect(find.widgetWithIcon(TextButton, Icons.person_add_rounded), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('renders a tile per partner with name and email', (tester) async {
|
testWidgets('renders a tile per partner with name and email', (tester) async {
|
||||||
@@ -39,9 +37,8 @@ void main() {
|
|||||||
testWidgets('renders a remove action for each partner', (tester) async {
|
testWidgets('renders a remove action for each partner', (tester) async {
|
||||||
final partner1 = PartnerFactory.create(inTimeline: true);
|
final partner1 = PartnerFactory.create(inTimeline: true);
|
||||||
final partner2 = PartnerFactory.create();
|
final partner2 = PartnerFactory.create();
|
||||||
final action = const PartnerRemoveAction(sharedWithId: '', partnerName: '');
|
|
||||||
await tester.pumpTestWidget(context, PartnerSharedByList(partners: [partner1, partner2]));
|
await tester.pumpTestWidget(context, PartnerSharedByList(partners: [partner1, partner2]));
|
||||||
expect(find.byIcon(action.icon), findsNWidgets(2));
|
expect(find.byIcon(Icons.person_remove_rounded), findsNWidgets(2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'
|
|||||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_ui/immich_ui.dart';
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
@@ -43,6 +44,7 @@ class PresentationContext {
|
|||||||
currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)),
|
currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)),
|
||||||
assetServiceProvider.overrideWithValue(service.asset.service),
|
assetServiceProvider.overrideWithValue(service.asset.service),
|
||||||
partnerServiceProvider.overrideWithValue(service.partner.service),
|
partnerServiceProvider.overrideWithValue(service.partner.service),
|
||||||
|
toastRepositoryProvider.overrideWithValue(repository.toast),
|
||||||
];
|
];
|
||||||
|
|
||||||
static Future<PresentationContext> create() async {
|
static Future<PresentationContext> create() async {
|
||||||
@@ -96,14 +98,34 @@ extension PumpPresentationWidget on WidgetTester {
|
|||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pumpTestAction(
|
Future<BaseAction> pumpTestAction(
|
||||||
PresentationContext context,
|
PresentationContext context,
|
||||||
BaseAction action, {
|
BaseAction Function(ActionScope scope) build, {
|
||||||
List<Override> overrides = const [],
|
List<Override> overrides = const [],
|
||||||
}) async {
|
}) async {
|
||||||
await pumpTestWidget(context, ActionIconButtonWidget(action: action), overrides: overrides);
|
final action = await pumpActionButton(context, build, overrides: overrides);
|
||||||
await tap(find.byType(ImmichIconButton));
|
await tap(find.byType(ImmichIconButton));
|
||||||
await pump();
|
await pump();
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<BaseAction> pumpActionButton(
|
||||||
|
PresentationContext context,
|
||||||
|
BaseAction Function(ActionScope scope) build, {
|
||||||
|
List<Override> overrides = const [],
|
||||||
|
}) async {
|
||||||
|
late BaseAction action;
|
||||||
|
await pumpTestWidget(
|
||||||
|
context,
|
||||||
|
Consumer(
|
||||||
|
builder: (innerContext, ref, _) {
|
||||||
|
action = build(ActionScope(context: innerContext, ref: ref, authUser: context.currentUser));
|
||||||
|
return ActionIconButtonWidget(action: action);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
overrides: overrides,
|
||||||
|
);
|
||||||
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pumpUntilFound(Finder finder, {int maxFrames = 10}) async {
|
Future<void> pumpUntilFound(Finder finder, {int maxFrames = 10}) async {
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||||
|
|
||||||
|
import '../factories/local_asset_factory.dart';
|
||||||
|
import '../factories/remote_asset_factory.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AssetFilter', () {
|
||||||
|
group('type promotion', () {
|
||||||
|
test('a bare filter retains every BaseAsset', () {
|
||||||
|
final remoteAsset = RemoteAssetFactory.create();
|
||||||
|
final localAsset = LocalAssetFactory.create();
|
||||||
|
|
||||||
|
final AssetFilter<BaseAsset> filter = AssetFilter(<BaseAsset>[remoteAsset, localAsset]);
|
||||||
|
|
||||||
|
expect(filter.toList(), [remoteAsset, localAsset]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remote keeps only remote assets', () {
|
||||||
|
final remoteAsset = RemoteAssetFactory.create();
|
||||||
|
final localAsset = LocalAssetFactory.create();
|
||||||
|
|
||||||
|
final AssetFilter<RemoteAsset> remoteOnly = AssetFilter(<BaseAsset>[remoteAsset, localAsset]).remote();
|
||||||
|
|
||||||
|
expect(remoteOnly.toList(), [remoteAsset]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local keeps only local assets', () {
|
||||||
|
final remoteAsset = RemoteAssetFactory.create();
|
||||||
|
final localAsset = LocalAssetFactory.create();
|
||||||
|
|
||||||
|
final AssetFilter<LocalAsset> localOnly = AssetFilter(<BaseAsset>[remoteAsset, localAsset]).local();
|
||||||
|
|
||||||
|
expect(localOnly.toList(), [localAsset]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('owned promotes to RemoteAsset and drops local assets', () {
|
||||||
|
final remoteAsset = RemoteAssetFactory.create();
|
||||||
|
final localAsset = LocalAssetFactory.create();
|
||||||
|
|
||||||
|
final AssetFilter<RemoteAsset> remoteOnly = AssetFilter(<BaseAsset>[
|
||||||
|
remoteAsset,
|
||||||
|
localAsset,
|
||||||
|
]).owned(remoteAsset.ownerId);
|
||||||
|
|
||||||
|
expect(remoteOnly.toList(), [remoteAsset]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('backedUp promotes to LocalAsset and drops remote assets', () {
|
||||||
|
final syncedPhoto = LocalAssetFactory.create().copyWith(remoteId: 'remote');
|
||||||
|
final offlinePhoto = LocalAssetFactory.create();
|
||||||
|
final remotePhoto = RemoteAssetFactory.create();
|
||||||
|
|
||||||
|
final AssetFilter<LocalAsset> syncedPhotos = AssetFilter(<BaseAsset>[
|
||||||
|
syncedPhoto,
|
||||||
|
offlinePhoto,
|
||||||
|
remotePhoto,
|
||||||
|
]).backedUp();
|
||||||
|
|
||||||
|
expect(syncedPhotos.toList(), [syncedPhoto]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('named filters', () {
|
||||||
|
test('owned keeps only assets of the given owner', () {
|
||||||
|
final asset1 = RemoteAssetFactory.create();
|
||||||
|
final asset2 = RemoteAssetFactory.create();
|
||||||
|
|
||||||
|
final alexPhotos = AssetFilter([asset1, asset2]).owned(asset1.ownerId);
|
||||||
|
|
||||||
|
expect(alexPhotos.toList(), [asset1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('favorites keeps only favorite assets', () {
|
||||||
|
final asset1 = RemoteAssetFactory.create(isFavorite: true);
|
||||||
|
final asset2 = RemoteAssetFactory.create(ownerId: asset1.ownerId);
|
||||||
|
|
||||||
|
final favorites = AssetFilter([asset1, asset2]).favorite();
|
||||||
|
|
||||||
|
expect(favorites.toList(), [asset1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type keeps only assets of the given type', () {
|
||||||
|
final image = RemoteAssetFactory.create();
|
||||||
|
final video = RemoteAssetFactory.create(ownerId: image.ownerId).copyWith(type: .video);
|
||||||
|
|
||||||
|
final videos = AssetFilter([image, video]).type(.video);
|
||||||
|
|
||||||
|
expect(videos.toList(), [video]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('visibility keeps only assets with the given visibility', () {
|
||||||
|
final locked = RemoteAssetFactory.create(visibility: AssetVisibility.locked);
|
||||||
|
final onTimeline = RemoteAssetFactory.create(ownerId: locked.ownerId);
|
||||||
|
|
||||||
|
final lockedPhotos = AssetFilter([locked, onTimeline]).visibility(.locked);
|
||||||
|
|
||||||
|
expect(lockedPhotos.toList(), [locked]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('archived keeps only archived assets', () {
|
||||||
|
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
|
||||||
|
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId);
|
||||||
|
|
||||||
|
final archivedPhotos = AssetFilter([archived, onTimeline]).archived();
|
||||||
|
|
||||||
|
expect(archivedPhotos.toList(), [archived]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stacked keeps only assets belonging to a stack', () {
|
||||||
|
final stacked = RemoteAssetFactory.create(stackId: 'stack');
|
||||||
|
final loose = RemoteAssetFactory.create(ownerId: stacked.ownerId);
|
||||||
|
|
||||||
|
final stackedPhotos = AssetFilter([stacked, loose]).stacked();
|
||||||
|
|
||||||
|
expect(stackedPhotos.toList(), [stacked]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('inversion', () {
|
||||||
|
test('notArchived keeps every non-archived visibility', () {
|
||||||
|
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
|
||||||
|
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.timeline);
|
||||||
|
final hidden = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.hidden);
|
||||||
|
final locked = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.locked);
|
||||||
|
|
||||||
|
final visiblePhotos = AssetFilter([archived, onTimeline, hidden, locked]).archived(isArchived: false);
|
||||||
|
|
||||||
|
expect(visiblePhotos.toSet(), {onTimeline, hidden, locked});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notVisibility keeps every asset not at the target visibility', () {
|
||||||
|
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
|
||||||
|
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.timeline);
|
||||||
|
final locked = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.locked);
|
||||||
|
|
||||||
|
final toArchive = AssetFilter([archived, onTimeline, locked]).notVisibility(.archive);
|
||||||
|
|
||||||
|
expect(toArchive.toSet(), {onTimeline, locked});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notStacked keeps only assets without a stack', () {
|
||||||
|
final stacked = RemoteAssetFactory.create(stackId: 'stack');
|
||||||
|
final loose = RemoteAssetFactory.create(ownerId: stacked.ownerId);
|
||||||
|
|
||||||
|
final loosePhotos = AssetFilter([stacked, loose]).stacked(isStacked: false);
|
||||||
|
|
||||||
|
expect(loosePhotos.toList(), [loose]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('whereNot inverts an arbitrary predicate', () {
|
||||||
|
final favorite = RemoteAssetFactory.create(isFavorite: true);
|
||||||
|
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
|
||||||
|
|
||||||
|
final nonFavorites = AssetFilter([favorite, regular]).whereNot((asset) => asset.isFavorite);
|
||||||
|
|
||||||
|
expect(nonFavorites.toList(), [regular]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notFavorites keeps only non-favorite assets', () {
|
||||||
|
final favorite = RemoteAssetFactory.create(isFavorite: true);
|
||||||
|
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
|
||||||
|
|
||||||
|
final nonFavorites = AssetFilter([favorite, regular]).favorite(isFavorite: false);
|
||||||
|
|
||||||
|
expect(nonFavorites.toList(), [regular]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('chaining', () {
|
||||||
|
test('combines predicates across owner, visibility and stack', () {
|
||||||
|
final asset = RemoteAssetFactory.create();
|
||||||
|
final wrongOwner = RemoteAssetFactory.create();
|
||||||
|
final archived = RemoteAssetFactory.create(ownerId: asset.ownerId, visibility: AssetVisibility.archive);
|
||||||
|
final stacked = RemoteAssetFactory.create(ownerId: asset.ownerId, stackId: 'stack-1');
|
||||||
|
final localPhoto = LocalAssetFactory.create();
|
||||||
|
|
||||||
|
final result = AssetFilter(<BaseAsset>[
|
||||||
|
asset,
|
||||||
|
wrongOwner,
|
||||||
|
archived,
|
||||||
|
stacked,
|
||||||
|
localPhoto,
|
||||||
|
]).owned(asset.ownerId).archived(isArchived: false).stacked(isStacked: false);
|
||||||
|
|
||||||
|
expect(result.toList(), [asset]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a base filter after a promotion retains the promoted type', () {
|
||||||
|
final favorite = RemoteAssetFactory.create(isFavorite: true);
|
||||||
|
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
|
||||||
|
|
||||||
|
final AssetFilter<RemoteAsset> result = AssetFilter([favorite, regular]).owned(favorite.ownerId).favorite();
|
||||||
|
|
||||||
|
expect(result.toList(), [favorite]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
|
import '../unit/presentation/presentation_context.dart';
|
||||||
|
|
||||||
LocalAsset createLocalAsset({
|
LocalAsset createLocalAsset({
|
||||||
String? remoteId,
|
String? remoteId,
|
||||||
String name = 'test.jpg',
|
String name = 'test.jpg',
|
||||||
@@ -77,6 +80,23 @@ RemoteAlbum createRemoteAlbum({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActionButtonContext _buttonContext({
|
||||||
|
required BaseAsset asset,
|
||||||
|
RemoteAlbum? currentAlbum,
|
||||||
|
bool isArchived = false,
|
||||||
|
bool isStacked = false,
|
||||||
|
}) => ActionButtonContext(
|
||||||
|
asset: asset,
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: isArchived,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: currentAlbum,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: isStacked,
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('ActionButtonContext', () {
|
group('ActionButtonContext', () {
|
||||||
test('should create context with all required parameters', () {
|
test('should create context with all required parameters', () {
|
||||||
@@ -1054,188 +1074,101 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('ActionButtonType.buildButton', () {
|
group('ActionButtonType.buildButton', () {
|
||||||
|
late PresentationContext presentation;
|
||||||
late BaseAsset asset;
|
late BaseAsset asset;
|
||||||
late ActionButtonContext context;
|
|
||||||
|
|
||||||
setUp(() {
|
setUp(() async {
|
||||||
|
presentation = await PresentationContext.create();
|
||||||
asset = createLocalAsset(remoteId: 'remote-id');
|
asset = createLocalAsset(remoteId: 'remote-id');
|
||||||
context = ActionButtonContext(
|
|
||||||
asset: asset,
|
|
||||||
isOwner: true,
|
|
||||||
isArchived: false,
|
|
||||||
isTrashEnabled: true,
|
|
||||||
isInLockedView: false,
|
|
||||||
currentAlbum: null,
|
|
||||||
advancedTroubleshooting: false,
|
|
||||||
isStacked: false,
|
|
||||||
source: ActionSource.timeline,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should build correct widget for each button type', () {
|
tearDown(() {
|
||||||
for (final buttonType in ActionButtonType.values) {
|
presentation.dispose();
|
||||||
var buttonContext = context;
|
});
|
||||||
|
|
||||||
if (buttonType == ActionButtonType.removeFromAlbum) {
|
testWidgets('builds a widget for every button type', (tester) async {
|
||||||
final album = createRemoteAlbum();
|
final built = <Widget>[];
|
||||||
final contextWithAlbum = ActionButtonContext(
|
await tester.pumpTestWidget(
|
||||||
asset: asset,
|
presentation,
|
||||||
isOwner: true,
|
Consumer(
|
||||||
isArchived: false,
|
builder: (buildContext, ref, _) {
|
||||||
isTrashEnabled: true,
|
for (final buttonType in ActionButtonType.values) {
|
||||||
isInLockedView: false,
|
final buttonContext = switch (buttonType) {
|
||||||
currentAlbum: album,
|
ActionButtonType.similarPhotos => _buttonContext(asset: createRemoteAsset()),
|
||||||
advancedTroubleshooting: false,
|
ActionButtonType.removeFromAlbum ||
|
||||||
isStacked: false,
|
ActionButtonType.setAlbumCover => _buttonContext(asset: asset, currentAlbum: createRemoteAlbum()),
|
||||||
source: ActionSource.timeline,
|
ActionButtonType.unstack => _buttonContext(
|
||||||
);
|
asset: asset,
|
||||||
final widget = buttonType.buildButton(contextWithAlbum);
|
currentAlbum: createRemoteAlbum(),
|
||||||
expect(widget, isA<Widget>());
|
isStacked: true,
|
||||||
} else if (buttonType == ActionButtonType.similarPhotos) {
|
),
|
||||||
final contextWithAlbum = ActionButtonContext(
|
_ => _buttonContext(asset: asset),
|
||||||
asset: createRemoteAsset(),
|
};
|
||||||
isOwner: true,
|
built.add(buttonType.buildButton(buttonContext, buildContext, ref));
|
||||||
isArchived: false,
|
}
|
||||||
isTrashEnabled: true,
|
return const SizedBox.shrink();
|
||||||
isInLockedView: false,
|
},
|
||||||
currentAlbum: null,
|
),
|
||||||
advancedTroubleshooting: false,
|
);
|
||||||
isStacked: false,
|
|
||||||
source: ActionSource.timeline,
|
expect(built, hasLength(ActionButtonType.values.length));
|
||||||
);
|
expect(built, everyElement(isA<Widget>()));
|
||||||
final widget = buttonType.buildButton(contextWithAlbum);
|
|
||||||
expect(widget, isA<Widget>());
|
|
||||||
} else if (buttonType == ActionButtonType.setAlbumCover) {
|
|
||||||
final album = createRemoteAlbum();
|
|
||||||
final contextWithAlbum = ActionButtonContext(
|
|
||||||
asset: asset,
|
|
||||||
isOwner: true,
|
|
||||||
isArchived: false,
|
|
||||||
isTrashEnabled: true,
|
|
||||||
isInLockedView: false,
|
|
||||||
currentAlbum: album,
|
|
||||||
advancedTroubleshooting: false,
|
|
||||||
isStacked: false,
|
|
||||||
source: ActionSource.timeline,
|
|
||||||
);
|
|
||||||
final widget = buttonType.buildButton(contextWithAlbum);
|
|
||||||
expect(widget, isA<Widget>());
|
|
||||||
} else if (buttonType == ActionButtonType.unstack) {
|
|
||||||
final album = createRemoteAlbum();
|
|
||||||
final contextWithAlbum = ActionButtonContext(
|
|
||||||
asset: asset,
|
|
||||||
isOwner: true,
|
|
||||||
isArchived: false,
|
|
||||||
isTrashEnabled: true,
|
|
||||||
isInLockedView: false,
|
|
||||||
currentAlbum: album,
|
|
||||||
advancedTroubleshooting: false,
|
|
||||||
isStacked: true,
|
|
||||||
source: ActionSource.timeline,
|
|
||||||
);
|
|
||||||
final widget = buttonType.buildButton(contextWithAlbum);
|
|
||||||
expect(widget, isA<Widget>());
|
|
||||||
} else {
|
|
||||||
final widget = buttonType.buildButton(buttonContext);
|
|
||||||
expect(widget, isA<Widget>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('ActionButtonBuilder', () {
|
group('ActionButtonBuilder', () {
|
||||||
test('should return buttons that should show', () {
|
late PresentationContext presentation;
|
||||||
final remoteAsset = createRemoteAsset();
|
|
||||||
final context = ActionButtonContext(
|
|
||||||
asset: remoteAsset,
|
|
||||||
isOwner: true,
|
|
||||||
isArchived: false,
|
|
||||||
isTrashEnabled: true,
|
|
||||||
isInLockedView: false,
|
|
||||||
currentAlbum: null,
|
|
||||||
advancedTroubleshooting: false,
|
|
||||||
isStacked: false,
|
|
||||||
source: ActionSource.timeline,
|
|
||||||
);
|
|
||||||
|
|
||||||
final widgets = ActionButtonBuilder.build(context);
|
setUp(() async {
|
||||||
|
presentation = await PresentationContext.create();
|
||||||
expect(widgets, isNotEmpty);
|
|
||||||
expect(widgets.length, greaterThan(0));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should include album-specific buttons when album is present', () {
|
tearDown(() {
|
||||||
final remoteAsset = createRemoteAsset();
|
presentation.dispose();
|
||||||
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
|
});
|
||||||
final context = ActionButtonContext(
|
|
||||||
asset: remoteAsset,
|
Future<List<Widget>> buildButtons(WidgetTester tester, ActionButtonContext context) async {
|
||||||
isOwner: true,
|
late List<Widget> widgets;
|
||||||
isArchived: false,
|
await tester.pumpTestWidget(
|
||||||
isTrashEnabled: true,
|
presentation,
|
||||||
isInLockedView: false,
|
Consumer(
|
||||||
currentAlbum: album,
|
builder: (buildContext, ref, _) {
|
||||||
advancedTroubleshooting: false,
|
widgets = ActionButtonBuilder.build(context, buildContext, ref);
|
||||||
isStacked: false,
|
return const SizedBox.shrink();
|
||||||
source: ActionSource.timeline,
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
final widgets = ActionButtonBuilder.build(context);
|
testWidgets('returns buttons that should show', (tester) async {
|
||||||
|
final widgets = await buildButtons(tester, _buttonContext(asset: createRemoteAsset()));
|
||||||
expect(widgets, isNotEmpty);
|
expect(widgets, isNotEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should only include local buttons for local assets', () {
|
testWidgets('includes album-specific buttons when album is present', (tester) async {
|
||||||
final localAsset = createLocalAsset();
|
final widgets = await buildButtons(
|
||||||
final context = ActionButtonContext(
|
tester,
|
||||||
asset: localAsset,
|
_buttonContext(
|
||||||
isOwner: true,
|
asset: createRemoteAsset(),
|
||||||
isArchived: false,
|
currentAlbum: createRemoteAlbum(isActivityEnabled: true, isShared: true),
|
||||||
isTrashEnabled: true,
|
),
|
||||||
isInLockedView: false,
|
|
||||||
currentAlbum: null,
|
|
||||||
advancedTroubleshooting: false,
|
|
||||||
isStacked: false,
|
|
||||||
source: ActionSource.timeline,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final widgets = ActionButtonBuilder.build(context);
|
|
||||||
|
|
||||||
expect(widgets, isNotEmpty);
|
expect(widgets, isNotEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respect archived state', () {
|
testWidgets('includes local buttons for local assets', (tester) async {
|
||||||
|
final widgets = await buildButtons(tester, _buttonContext(asset: createLocalAsset()));
|
||||||
|
expect(widgets, isNotEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('respects archived state', (tester) async {
|
||||||
final remoteAsset = createRemoteAsset();
|
final remoteAsset = createRemoteAsset();
|
||||||
|
final archived = await buildButtons(tester, _buttonContext(asset: remoteAsset, isArchived: true));
|
||||||
|
final nonArchived = await buildButtons(tester, _buttonContext(asset: remoteAsset, isArchived: false));
|
||||||
|
|
||||||
final archivedContext = ActionButtonContext(
|
expect(archived, isNotEmpty);
|
||||||
asset: remoteAsset,
|
expect(nonArchived, isNotEmpty);
|
||||||
isOwner: true,
|
|
||||||
isArchived: true,
|
|
||||||
isTrashEnabled: true,
|
|
||||||
isInLockedView: false,
|
|
||||||
currentAlbum: null,
|
|
||||||
advancedTroubleshooting: false,
|
|
||||||
isStacked: false,
|
|
||||||
source: ActionSource.timeline,
|
|
||||||
);
|
|
||||||
|
|
||||||
final archivedWidgets = ActionButtonBuilder.build(archivedContext);
|
|
||||||
|
|
||||||
final nonArchivedContext = ActionButtonContext(
|
|
||||||
asset: remoteAsset,
|
|
||||||
isOwner: true,
|
|
||||||
isArchived: false,
|
|
||||||
isTrashEnabled: true,
|
|
||||||
isInLockedView: false,
|
|
||||||
currentAlbum: null,
|
|
||||||
advancedTroubleshooting: false,
|
|
||||||
isStacked: false,
|
|
||||||
source: ActionSource.timeline,
|
|
||||||
);
|
|
||||||
|
|
||||||
final nonArchivedWidgets = ActionButtonBuilder.build(nonArchivedContext);
|
|
||||||
|
|
||||||
expect(archivedWidgets, isNotEmpty);
|
|
||||||
expect(nonArchivedWidgets, isNotEmpty);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user