mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: drift description editor (#20383)
* feat: drift description editor * chore: use focus node * chore: code review fixes * chore: move description update to action.service * refactor * refactor --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
58521c9efb
commit
290e325c5c
@ -988,6 +988,7 @@
|
|||||||
},
|
},
|
||||||
"exif": "Exif",
|
"exif": "Exif",
|
||||||
"exif_bottom_sheet_description": "Add Description...",
|
"exif_bottom_sheet_description": "Add Description...",
|
||||||
|
"exif_bottom_sheet_description_error": "Error updating description",
|
||||||
"exif_bottom_sheet_details": "DETAILS",
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
"exif_bottom_sheet_location": "LOCATION",
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
"exif_bottom_sheet_people": "PEOPLE",
|
"exif_bottom_sheet_people": "PEOPLE",
|
||||||
|
@ -226,6 +226,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateDescription(String assetId, String description) async {
|
||||||
|
await (_db.remoteExifEntity.update()..where((row) => row.assetId.equals(assetId))).write(
|
||||||
|
RemoteExifEntityCompanion(description: Value(description)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _db.managers.remoteAssetEntity.count();
|
return _db.managers.remoteAssetEntity.count();
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,12 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
|
|||||||
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/bottom_sheet/location_details.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.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/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
const _kSeparator = ' • ';
|
const _kSeparator = ' • ';
|
||||||
|
|
||||||
@ -147,6 +149,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
title: _getDateTime(context, asset),
|
title: _getDateTime(context, asset),
|
||||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
|
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
|
||||||
const SheetLocationDetails(),
|
const SheetLocationDetails(),
|
||||||
// Details header
|
// Details header
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
@ -234,3 +237,78 @@ class _SheetTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SheetAssetDescription extends ConsumerStatefulWidget {
|
||||||
|
final ExifInfo exif;
|
||||||
|
|
||||||
|
const _SheetAssetDescription({required this.exif});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> {
|
||||||
|
late TextEditingController _controller;
|
||||||
|
final _descriptionFocus = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.exif.description ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveDescription(String? previousDescription) async {
|
||||||
|
final newDescription = _controller.text.trim();
|
||||||
|
|
||||||
|
if (newDescription == previousDescription) {
|
||||||
|
_descriptionFocus.unfocus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final editAction = await ref.read(actionProvider.notifier).updateDescription(ActionSource.viewer, newDescription);
|
||||||
|
|
||||||
|
if (!editAction.success) {
|
||||||
|
_controller.text = previousDescription ?? '';
|
||||||
|
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'exif_bottom_sheet_description_error'.t(context: context),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_descriptionFocus.unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Watch the current asset EXIF provider to get updates
|
||||||
|
final currentExifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||||
|
|
||||||
|
// Update controller text when EXIF data changes
|
||||||
|
final currentDescription = currentExifInfo?.description ?? '';
|
||||||
|
if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) {
|
||||||
|
_controller.text = currentDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
focusNode: _descriptionFocus,
|
||||||
|
maxLines: null, // makes it grow as text is added
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'exif_bottom_sheet_description'.t(context: context),
|
||||||
|
border: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
disabledBorder: InputBorder.none,
|
||||||
|
errorBorder: InputBorder.none,
|
||||||
|
focusedErrorBorder: InputBorder.none,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => saveDescription(currentExifInfo?.description),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -250,6 +250,22 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> updateDescription(ActionSource source, String description) async {
|
||||||
|
final ids = _getRemoteIdsForSource(source);
|
||||||
|
if (ids.length != 1) {
|
||||||
|
_logger.warning('updateDescription called with multiple assets, expected single asset');
|
||||||
|
return ActionResult(count: ids.length, success: false, error: 'Expected single asset for description update');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final isUpdated = await _service.updateDescription(ids.first, description);
|
||||||
|
return ActionResult(count: 1, success: isUpdated);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to update description for asset', error, stack);
|
||||||
|
return ActionResult(count: 1, success: false, error: error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<ActionResult> stack(String userId, ActionSource source) async {
|
Future<ActionResult> stack(String userId, ActionSource source) async {
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
final ids = _getOwnedRemoteIdsForSource(source);
|
||||||
try {
|
try {
|
||||||
|
@ -93,6 +93,10 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
// we need to get the MIME of the thumbnail once that gets added to the API
|
// we need to get the MIME of the thumbnail once that gets added to the API
|
||||||
return response.originalMimeType;
|
return response.originalMimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateDescription(String assetId, String description) {
|
||||||
|
return _api.updateAsset(assetId, UpdateAssetDto(description: description));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on StackResponseDto {
|
extension on StackResponseDto {
|
||||||
|
@ -170,6 +170,14 @@ class ActionService {
|
|||||||
return removedCount;
|
return removedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> updateDescription(String assetId, String description) async {
|
||||||
|
// update remote first, then local to ensure consistency
|
||||||
|
await _assetApiRepository.updateDescription(assetId, description);
|
||||||
|
await _remoteAssetRepository.updateDescription(assetId, description);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> stack(String userId, List<String> remoteIds) async {
|
Future<void> stack(String userId, List<String> remoteIds) async {
|
||||||
final stack = await _assetApiRepository.stack(remoteIds);
|
final stack = await _assetApiRepository.stack(remoteIds);
|
||||||
await _remoteAssetRepository.stack(userId, stack);
|
await _remoteAssetRepository.stack(userId, stack);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user