feat: Add description (#2237)

* Added dto, logic to insert description and web implementation

* create text field and update on remote database

* Update description and save changes

* styling

* fix web test

* fix server test

* preserve description on metadata extraction job run

* handle exif info is null situation

* pr feedback

* format openapi spec

* update createAssetDto

* refactor logic to service

* move files

* only owner can update description

* Render description correctly in shared album

* Render description correctly in shared link

* disable description edit for not owner of asset on mobile

* localization and clean up

* fix test

* Uses providers for description text (#2244)

* uses providers for description text

* comments

* fixes initial data setting

* fixes notifier

---------

Co-authored-by: martyfuhry <martyfuhry@gmail.com>
This commit is contained in:
Alex 2023-04-13 10:22:06 -05:00 committed by GitHub
parent 561b208508
commit a9859bc029
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 843 additions and 178 deletions

View File

@ -262,5 +262,11 @@
"version_announcement_overlay_text_1": "Hi friend, there is a new release of", "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"advanced_settings_tile_title": "Advanced",
"advanced_settings_tile_subtitle": "Advanced user's settings",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"description_input_submit_error": "Error updating description, check the log for more details",
"description_input_hint_text": "Add description..."
} }

BIN
mobile/flutter_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 KiB

View File

@ -0,0 +1,93 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/services/asset_description.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class AssetDescriptionNotifier extends StateNotifier<String> {
final Isar _db;
final AssetDescriptionService _service;
final Asset _asset;
AssetDescriptionNotifier(
this._db,
this._service,
this._asset,
) : super('') {
_fetchLocalDescription();
_fetchRemoteDescription();
}
String get description => state;
/// Fetches the local database value for description
/// and writes it to [state]
void _fetchLocalDescription() async {
final localExifId = _asset.exifInfo?.id;
// Guard [localExifId] null
if (localExifId == null) {
return;
}
// Subscribe to local changes
final exifInfo = await _db
.exifInfos
.get(localExifId);
// Guard
if (exifInfo?.description == null) {
return;
}
state = exifInfo!.description!;
}
/// Fetches the remote value and sets the state
void _fetchRemoteDescription() async {
final remoteAssetId = _asset.remoteId;
final localExifId = _asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
// Reads the latest from the remote and writes it to DB in the service
final latest = await _service.readLatest(remoteAssetId, localExifId);
state = latest;
}
/// Sets the description to [description]
/// Uses the service to set the asset value
Future<void> setDescription(String description) async {
state = description;
final remoteAssetId = _asset.remoteId;
final localExifId = _asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
return _service
.setDescription(description, remoteAssetId, localExifId);
}
}
final assetDescriptionProvider = StateNotifierProvider
.autoDispose
.family<AssetDescriptionNotifier, String, Asset>(
(ref, asset) => AssetDescriptionNotifier(
ref.watch(dbProvider),
ref.watch(assetDescriptionServiceProvider),
asset,
),
);

View File

@ -0,0 +1,62 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
class AssetDescriptionService {
AssetDescriptionService(this._db, this._api);
final Isar _db;
final ApiService _api;
setDescription(
String description,
String remoteAssetId,
int localExifId,
) async {
final result = await _api.assetApi.updateAsset(
remoteAssetId,
UpdateAssetDto(description: description),
);
if (result?.exifInfo?.description != null) {
var exifInfo = await _db.exifInfos.get(localExifId);
if (exifInfo != null) {
exifInfo.description = result!.exifInfo!.description;
await _db.writeTxn(
() => _db.exifInfos.put(exifInfo),
);
}
}
}
Future<String> readLatest(String assetRemoteId, int localExifId) async {
final latestAssetFromServer =
await _api.assetApi.getAssetById(assetRemoteId);
final localExifInfo = await _db.exifInfos.get(localExifId);
if (latestAssetFromServer != null && localExifInfo != null) {
localExifInfo.description =
latestAssetFromServer.exifInfo?.description ?? '';
await _db.writeTxn(
() => _db.exifInfos.put(localExifInfo),
);
return localExifInfo.description!;
}
return "";
}
}
final assetDescriptionServiceProvider = Provider(
(ref) => AssetDescriptionService(
ref.watch(dbProvider),
ref.watch(apiServiceProvider),
),
);

View File

@ -0,0 +1,103 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/shared/models/store.dart' as store;
class DescriptionInput extends HookConsumerWidget {
DescriptionInput({
super.key,
required this.asset,
});
final Asset asset;
final Logger _log = Logger('DescriptionInput');
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final textColor = isDarkTheme ? Colors.white : Colors.black;
final controller = useTextEditingController();
final focusNode = useFocusNode();
final isFocus = useState(false);
final isTextEmpty = useState(controller.text.isEmpty);
final descriptionProvider = ref.watch(assetDescriptionProvider(asset).notifier);
final description = ref.watch(assetDescriptionProvider(asset));
final owner = store.Store.get(store.StoreKey.currentUser);
final hasError = useState(false);
controller.text = description;
submitDescription(String description) async {
hasError.value = false;
try {
await descriptionProvider.setDescription(
description,
);
} catch (error, stack) {
hasError.value = true;
_log.severe("Error updating description $error", error, stack);
ImmichToast.show(
context: context,
msg: "description_input_submit_error".tr(),
toastType: ToastType.error,
);
}
}
Widget? suffixIcon;
if (hasError.value) {
suffixIcon = const Icon(Icons.warning_outlined);
} else if (!isTextEmpty.value && isFocus.value) {
suffixIcon = IconButton(
onPressed: () {
controller.clear();
isTextEmpty.value = true;
},
icon: Icon(
Icons.cancel_rounded,
color: Colors.grey[500],
),
splashRadius: 10,
);
}
return TextField(
enabled: owner.isarId == asset.ownerId,
focusNode: focusNode,
onTap: () => isFocus.value = true,
onChanged: (value) {
isTextEmpty.value = false;
},
onTapOutside: (a) async {
isFocus.value = false;
focusNode.unfocus();
if (description != controller.text) {
await submitDescription(controller.text);
}
},
autofocus: false,
maxLines: null,
keyboardType: TextInputType.multiline,
controller: controller,
style: const TextStyle(
fontSize: 14,
),
decoration: InputDecoration(
hintText: 'description_input_hint_text'.tr(),
border: InputBorder.none,
hintStyle: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 12,
color: textColor.withOpacity(0.5),
),
suffixIcon: suffixIcon,
),
);
}
}

View File

@ -2,25 +2,25 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart'; import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/bytes_units.dart';
class ExifBottomSheet extends HookConsumerWidget { class ExifBottomSheet extends HookConsumerWidget {
final Asset assetDetail; final Asset asset;
const ExifBottomSheet({Key? key, required this.assetDetail}) const ExifBottomSheet({Key? key, required this.asset}) : super(key: key);
: super(key: key);
bool get showMap => bool get showMap =>
assetDetail.exifInfo?.latitude != null && asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null;
assetDetail.exifInfo?.longitude != null;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final ExifInfo? exifInfo = assetDetail.exifInfo; final exifInfo = asset.exifInfo;
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
var textColor = isDarkTheme ? Colors.white : Colors.black;
buildMap() { buildMap() {
return Padding( return Padding(
@ -76,19 +76,6 @@ class ExifBottomSheet extends HookConsumerWidget {
); );
} }
final textColor = Theme.of(context).primaryColor;
buildLocationText() {
return Text(
"${exifInfo?.city}, ${exifInfo?.state}",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: textColor,
),
);
}
buildSizeText(Asset a) { buildSizeText(Asset a) {
String resolution = a.width != null && a.height != null String resolution = a.width != null && a.height != null
? "${a.height} x ${a.width} " ? "${a.height} x ${a.width} "
@ -128,13 +115,39 @@ class ExifBottomSheet extends HookConsumerWidget {
children: [ children: [
Text( Text(
"exif_bottom_sheet_location", "exif_bottom_sheet_location",
style: TextStyle(fontSize: 11, color: textColor), style: TextStyle(
fontSize: 11,
color: textColor,
fontWeight: FontWeight.bold,
),
).tr(), ).tr(),
buildMap(), buildMap(),
if (exifInfo != null && RichText(
exifInfo.city != null && text: TextSpan(
exifInfo.state != null) style: TextStyle(
buildLocationText(), fontSize: 12,
fontWeight: FontWeight.bold,
color: textColor,
fontFamily: 'WorkSans',
),
children: [
if (exifInfo != null && exifInfo.city != null)
TextSpan(
text: exifInfo.city,
),
if (exifInfo != null &&
exifInfo.city != null &&
exifInfo.state != null)
const TextSpan(
text: ", ",
),
if (exifInfo != null && exifInfo.state != null)
TextSpan(
text: "${exifInfo.state}",
),
],
),
),
Text( Text(
"${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}",
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
@ -146,7 +159,7 @@ class ExifBottomSheet extends HookConsumerWidget {
} }
buildDate() { buildDate() {
final fileCreatedAt = assetDetail.fileCreatedAt.toLocal(); final fileCreatedAt = asset.fileCreatedAt.toLocal();
final date = DateFormat.yMMMEd().format(fileCreatedAt); final date = DateFormat.yMMMEd().format(fileCreatedAt);
final time = DateFormat.jm().format(fileCreatedAt); final time = DateFormat.jm().format(fileCreatedAt);
@ -167,27 +180,37 @@ class ExifBottomSheet extends HookConsumerWidget {
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: Text( child: Text(
"exif_bottom_sheet_details", "exif_bottom_sheet_details",
style: TextStyle(fontSize: 11, color: textColor), style: TextStyle(
fontSize: 11,
color: textColor,
fontWeight: FontWeight.bold,
),
).tr(), ).tr(),
), ),
ListTile( ListTile(
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
dense: true, dense: true,
leading: const Icon(Icons.image), leading: Icon(
Icons.image,
color: textColor.withAlpha(200),
),
title: Text( title: Text(
assetDetail.fileName, asset.fileName,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: textColor, color: textColor,
), ),
), ),
subtitle: buildSizeText(assetDetail), subtitle: buildSizeText(asset),
), ),
if (exifInfo?.make != null) if (exifInfo?.make != null)
ListTile( ListTile(
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
dense: true, dense: true,
leading: const Icon(Icons.camera), leading: Icon(
Icons.camera,
color: textColor.withAlpha(200),
),
title: Text( title: Text(
"${exifInfo!.make} ${exifInfo.model}", "${exifInfo!.make} ${exifInfo.model}",
style: TextStyle( style: TextStyle(
@ -203,80 +226,75 @@ class ExifBottomSheet extends HookConsumerWidget {
); );
} }
return SingleChildScrollView( return GestureDetector(
child: Card( onTap: () {
shape: const RoundedRectangleBorder( // FocusScope.of(context).unfocus();
borderRadius: BorderRadius.only( },
topLeft: Radius.circular(15), child: SingleChildScrollView(
topRight: Radius.circular(15), child: Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
), ),
), margin: const EdgeInsets.all(0),
margin: const EdgeInsets.all(0), child: Container(
child: Container( margin: const EdgeInsets.symmetric(horizontal: 16.0),
margin: const EdgeInsets.symmetric(horizontal: 8.0), child: LayoutBuilder(
child: LayoutBuilder( builder: (context, constraints) {
builder: (context, constraints) { if (constraints.maxWidth > 600) {
if (constraints.maxWidth > 600) { // Two column
// Two column return Padding(
return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ buildDragHeader(),
buildDragHeader(), buildDate(),
buildDate(), if (asset.isRemote) DescriptionInput(asset: asset),
const SizedBox(height: 32.0), Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Flexible(
Flexible( flex: showMap ? 5 : 0,
flex: showMap ? 5 : 0, child: Padding(
child: Padding( padding: const EdgeInsets.only(right: 8.0),
padding: const EdgeInsets.only(right: 8.0), child: buildLocation(),
child: buildLocation(), ),
), ),
), Flexible(
Flexible( flex: 5,
flex: 5, child: Padding(
child: Padding( padding: const EdgeInsets.only(left: 8.0),
padding: const EdgeInsets.only(left: 8.0), child: buildDetail(),
child: buildDetail(), ),
), ),
), ],
], ),
), const SizedBox(height: 50),
const SizedBox(height: 50), ],
],
),
);
}
// One column
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildDragHeader(),
buildDate(),
const SizedBox(height: 16.0),
if (showMap)
Divider(
thickness: 1,
color: Colors.grey[600],
), ),
const SizedBox(height: 16.0), );
buildLocation(), }
const SizedBox(height: 16.0),
Divider( // One column
thickness: 1, return Column(
color: Colors.grey[600], crossAxisAlignment: CrossAxisAlignment.stretch,
), children: [
const SizedBox(height: 16.0), buildDragHeader(),
buildDetail(), buildDate(),
const SizedBox(height: 50), if (asset.isRemote) DescriptionInput(asset: asset),
], const SizedBox(height: 8.0),
); buildLocation(),
}, SizedBox(height: showMap ? 16.0 : 0.0),
buildDetail(),
const SizedBox(height: 50),
],
);
},
),
), ),
), ),
), ),

View File

@ -195,7 +195,12 @@ class GalleryViewerPage extends HookConsumerWidget {
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) { .getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) {
return AdvancedBottomSheet(assetDetail: assetDetail!); return AdvancedBottomSheet(assetDetail: assetDetail!);
} }
return ExifBottomSheet(assetDetail: assetDetail!); return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: ExifBottomSheet(asset: assetDetail!),
);
}, },
); );
} }

View File

@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -25,19 +26,25 @@ class AdvancedSettings extends HookConsumerWidget {
return ExpansionTile( return ExpansionTile(
textColor: Theme.of(context).primaryColor, textColor: Theme.of(context).primaryColor,
title: const Text( title: const Text(
"Advanced", "advanced_settings_tile_title",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ).tr(),
subtitle: const Text(
"advanced_settings_tile_subtitle",
style: TextStyle(
fontSize: 13,
),
).tr(),
children: [ children: [
SettingsSwitchListTile( SettingsSwitchListTile(
enabled: true, enabled: true,
appSettingService: appSettingService, appSettingService: appSettingService,
valueNotifier: isEnabled, valueNotifier: isEnabled,
settingsEnum: AppSettingsEnum.advancedTroubleshooting, settingsEnum: AppSettingsEnum.advancedTroubleshooting,
title: "Troubleshooting", title: "advanced_settings_troubleshooting_title".tr(),
subtitle: "Enable additional features for troubleshooting", subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
), ),
], ],
); );

View File

@ -21,6 +21,7 @@ class ExifInfo {
String? city; String? city;
String? state; String? state;
String? country; String? country;
String? description;
@ignore @ignore
String get exposureTime { String get exposureTime {
@ -58,7 +59,8 @@ class ExifInfo {
long = dto.longitude?.toDouble(), long = dto.longitude?.toDouble(),
city = dto.city, city = dto.city,
state = dto.state, state = dto.state,
country = dto.country; country = dto.country,
description = dto.description;
ExifInfo({ ExifInfo({
this.fileSize, this.fileSize,
@ -74,6 +76,7 @@ class ExifInfo {
this.city, this.city,
this.state, this.state,
this.country, this.country,
this.description,
}); });
} }

View File

@ -27,58 +27,63 @@ const ExifInfoSchema = CollectionSchema(
name: r'country', name: r'country',
type: IsarType.string, type: IsarType.string,
), ),
r'exposureSeconds': PropertySchema( r'description': PropertySchema(
id: 2, id: 2,
name: r'description',
type: IsarType.string,
),
r'exposureSeconds': PropertySchema(
id: 3,
name: r'exposureSeconds', name: r'exposureSeconds',
type: IsarType.float, type: IsarType.float,
), ),
r'f': PropertySchema( r'f': PropertySchema(
id: 3, id: 4,
name: r'f', name: r'f',
type: IsarType.float, type: IsarType.float,
), ),
r'fileSize': PropertySchema( r'fileSize': PropertySchema(
id: 4, id: 5,
name: r'fileSize', name: r'fileSize',
type: IsarType.long, type: IsarType.long,
), ),
r'iso': PropertySchema( r'iso': PropertySchema(
id: 5, id: 6,
name: r'iso', name: r'iso',
type: IsarType.int, type: IsarType.int,
), ),
r'lat': PropertySchema( r'lat': PropertySchema(
id: 6, id: 7,
name: r'lat', name: r'lat',
type: IsarType.float, type: IsarType.float,
), ),
r'lens': PropertySchema( r'lens': PropertySchema(
id: 7, id: 8,
name: r'lens', name: r'lens',
type: IsarType.string, type: IsarType.string,
), ),
r'long': PropertySchema( r'long': PropertySchema(
id: 8, id: 9,
name: r'long', name: r'long',
type: IsarType.float, type: IsarType.float,
), ),
r'make': PropertySchema( r'make': PropertySchema(
id: 9, id: 10,
name: r'make', name: r'make',
type: IsarType.string, type: IsarType.string,
), ),
r'mm': PropertySchema( r'mm': PropertySchema(
id: 10, id: 11,
name: r'mm', name: r'mm',
type: IsarType.float, type: IsarType.float,
), ),
r'model': PropertySchema( r'model': PropertySchema(
id: 11, id: 12,
name: r'model', name: r'model',
type: IsarType.string, type: IsarType.string,
), ),
r'state': PropertySchema( r'state': PropertySchema(
id: 12, id: 13,
name: r'state', name: r'state',
type: IsarType.string, type: IsarType.string,
) )
@ -115,6 +120,12 @@ int _exifInfoEstimateSize(
bytesCount += 3 + value.length * 3; bytesCount += 3 + value.length * 3;
} }
} }
{
final value = object.description;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
{ {
final value = object.lens; final value = object.lens;
if (value != null) { if (value != null) {
@ -150,17 +161,18 @@ void _exifInfoSerialize(
) { ) {
writer.writeString(offsets[0], object.city); writer.writeString(offsets[0], object.city);
writer.writeString(offsets[1], object.country); writer.writeString(offsets[1], object.country);
writer.writeFloat(offsets[2], object.exposureSeconds); writer.writeString(offsets[2], object.description);
writer.writeFloat(offsets[3], object.f); writer.writeFloat(offsets[3], object.exposureSeconds);
writer.writeLong(offsets[4], object.fileSize); writer.writeFloat(offsets[4], object.f);
writer.writeInt(offsets[5], object.iso); writer.writeLong(offsets[5], object.fileSize);
writer.writeFloat(offsets[6], object.lat); writer.writeInt(offsets[6], object.iso);
writer.writeString(offsets[7], object.lens); writer.writeFloat(offsets[7], object.lat);
writer.writeFloat(offsets[8], object.long); writer.writeString(offsets[8], object.lens);
writer.writeString(offsets[9], object.make); writer.writeFloat(offsets[9], object.long);
writer.writeFloat(offsets[10], object.mm); writer.writeString(offsets[10], object.make);
writer.writeString(offsets[11], object.model); writer.writeFloat(offsets[11], object.mm);
writer.writeString(offsets[12], object.state); writer.writeString(offsets[12], object.model);
writer.writeString(offsets[13], object.state);
} }
ExifInfo _exifInfoDeserialize( ExifInfo _exifInfoDeserialize(
@ -172,17 +184,18 @@ ExifInfo _exifInfoDeserialize(
final object = ExifInfo( final object = ExifInfo(
city: reader.readStringOrNull(offsets[0]), city: reader.readStringOrNull(offsets[0]),
country: reader.readStringOrNull(offsets[1]), country: reader.readStringOrNull(offsets[1]),
exposureSeconds: reader.readFloatOrNull(offsets[2]), description: reader.readStringOrNull(offsets[2]),
f: reader.readFloatOrNull(offsets[3]), exposureSeconds: reader.readFloatOrNull(offsets[3]),
fileSize: reader.readLongOrNull(offsets[4]), f: reader.readFloatOrNull(offsets[4]),
iso: reader.readIntOrNull(offsets[5]), fileSize: reader.readLongOrNull(offsets[5]),
lat: reader.readFloatOrNull(offsets[6]), iso: reader.readIntOrNull(offsets[6]),
lens: reader.readStringOrNull(offsets[7]), lat: reader.readFloatOrNull(offsets[7]),
long: reader.readFloatOrNull(offsets[8]), lens: reader.readStringOrNull(offsets[8]),
make: reader.readStringOrNull(offsets[9]), long: reader.readFloatOrNull(offsets[9]),
mm: reader.readFloatOrNull(offsets[10]), make: reader.readStringOrNull(offsets[10]),
model: reader.readStringOrNull(offsets[11]), mm: reader.readFloatOrNull(offsets[11]),
state: reader.readStringOrNull(offsets[12]), model: reader.readStringOrNull(offsets[12]),
state: reader.readStringOrNull(offsets[13]),
); );
object.id = id; object.id = id;
return object; return object;
@ -200,27 +213,29 @@ P _exifInfoDeserializeProp<P>(
case 1: case 1:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 2: case 2:
return (reader.readFloatOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 3: case 3:
return (reader.readFloatOrNull(offset)) as P; return (reader.readFloatOrNull(offset)) as P;
case 4: case 4:
return (reader.readLongOrNull(offset)) as P; return (reader.readFloatOrNull(offset)) as P;
case 5: case 5:
return (reader.readIntOrNull(offset)) as P; return (reader.readLongOrNull(offset)) as P;
case 6: case 6:
return (reader.readFloatOrNull(offset)) as P; return (reader.readIntOrNull(offset)) as P;
case 7: case 7:
return (reader.readStringOrNull(offset)) as P; return (reader.readFloatOrNull(offset)) as P;
case 8: case 8:
return (reader.readFloatOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 9: case 9:
return (reader.readStringOrNull(offset)) as P;
case 10:
return (reader.readFloatOrNull(offset)) as P; return (reader.readFloatOrNull(offset)) as P;
case 11: case 10:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 11:
return (reader.readFloatOrNull(offset)) as P;
case 12: case 12:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 13:
return (reader.readStringOrNull(offset)) as P;
default: default:
throw IsarError('Unknown property with id $propertyId'); throw IsarError('Unknown property with id $propertyId');
} }
@ -607,6 +622,155 @@ extension ExifInfoQueryFilter
}); });
} }
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'description',
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
descriptionIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'description',
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
descriptionGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'description',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'description',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'description',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'description',
value: '',
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
descriptionIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'description',
value: '',
));
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition>
exposureSecondsIsNull() { exposureSecondsIsNull() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
@ -1825,6 +1989,18 @@ extension ExifInfoQuerySortBy on QueryBuilder<ExifInfo, ExifInfo, QSortBy> {
}); });
} }
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByDescription() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.asc);
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByDescriptionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.desc);
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByExposureSeconds() { QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByExposureSeconds() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'exposureSeconds', Sort.asc); return query.addSortBy(r'exposureSeconds', Sort.asc);
@ -1984,6 +2160,18 @@ extension ExifInfoQuerySortThenBy
}); });
} }
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByDescription() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.asc);
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByDescriptionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'description', Sort.desc);
});
}
QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByExposureSeconds() { QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByExposureSeconds() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'exposureSeconds', Sort.asc); return query.addSortBy(r'exposureSeconds', Sort.asc);
@ -2145,6 +2333,13 @@ extension ExifInfoQueryWhereDistinct
}); });
} }
QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByDescription(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'description', caseSensitive: caseSensitive);
});
}
QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByExposureSeconds() { QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByExposureSeconds() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'exposureSeconds'); return query.addDistinctBy(r'exposureSeconds');
@ -2236,6 +2431,12 @@ extension ExifInfoQueryProperty
}); });
} }
QueryBuilder<ExifInfo, String?, QQueryOperations> descriptionProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'description');
});
}
QueryBuilder<ExifInfo, double?, QQueryOperations> exposureSecondsProperty() { QueryBuilder<ExifInfo, double?, QQueryOperations> exposureSecondsProperty() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'exposureSeconds'); return query.addPropertyName(r'exposureSeconds');

View File

@ -27,6 +27,7 @@ Name | Type | Description | Notes
**city** | **String** | | [optional] **city** | **String** | | [optional]
**state** | **String** | | [optional] **state** | **String** | | [optional]
**country** | **String** | | [optional] **country** | **String** | | [optional]
**description** | **String** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -11,6 +11,7 @@ Name | Type | Description | Notes
**tagIds** | **List<String>** | | [optional] [default to const []] **tagIds** | **List<String>** | | [optional] [default to const []]
**isFavorite** | **bool** | | [optional] **isFavorite** | **bool** | | [optional]
**isArchived** | **bool** | | [optional] **isArchived** | **bool** | | [optional]
**description** | **String** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -32,6 +32,7 @@ class ExifResponseDto {
this.city, this.city,
this.state, this.state,
this.country, this.country,
this.description,
}); });
int? fileSizeInByte; int? fileSizeInByte;
@ -72,6 +73,8 @@ class ExifResponseDto {
String? country; String? country;
String? description;
@override @override
bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto && bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto &&
other.fileSizeInByte == fileSizeInByte && other.fileSizeInByte == fileSizeInByte &&
@ -92,7 +95,8 @@ class ExifResponseDto {
other.longitude == longitude && other.longitude == longitude &&
other.city == city && other.city == city &&
other.state == state && other.state == state &&
other.country == country; other.country == country &&
other.description == description;
@override @override
int get hashCode => int get hashCode =>
@ -115,10 +119,11 @@ class ExifResponseDto {
(longitude == null ? 0 : longitude!.hashCode) + (longitude == null ? 0 : longitude!.hashCode) +
(city == null ? 0 : city!.hashCode) + (city == null ? 0 : city!.hashCode) +
(state == null ? 0 : state!.hashCode) + (state == null ? 0 : state!.hashCode) +
(country == null ? 0 : country!.hashCode); (country == null ? 0 : country!.hashCode) +
(description == null ? 0 : description!.hashCode);
@override @override
String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, timeZone=$timeZone, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country]'; String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, timeZone=$timeZone, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country, description=$description]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -217,6 +222,11 @@ class ExifResponseDto {
} else { } else {
// json[r'country'] = null; // json[r'country'] = null;
} }
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
return json; return json;
} }
@ -272,6 +282,7 @@ class ExifResponseDto {
city: mapValueOfType<String>(json, r'city'), city: mapValueOfType<String>(json, r'city'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
country: mapValueOfType<String>(json, r'country'), country: mapValueOfType<String>(json, r'country'),
description: mapValueOfType<String>(json, r'description'),
); );
} }
return null; return null;

View File

@ -16,6 +16,7 @@ class UpdateAssetDto {
this.tagIds = const [], this.tagIds = const [],
this.isFavorite, this.isFavorite,
this.isArchived, this.isArchived,
this.description,
}); });
List<String> tagIds; List<String> tagIds;
@ -36,21 +37,31 @@ class UpdateAssetDto {
/// ///
bool? isArchived; bool? isArchived;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? description;
@override @override
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
other.tagIds == tagIds && other.tagIds == tagIds &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.isArchived == isArchived; other.isArchived == isArchived &&
other.description == description;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(tagIds.hashCode) + (tagIds.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode); (isArchived == null ? 0 : isArchived!.hashCode) +
(description == null ? 0 : description!.hashCode);
@override @override
String toString() => 'UpdateAssetDto[tagIds=$tagIds, isFavorite=$isFavorite, isArchived=$isArchived]'; String toString() => 'UpdateAssetDto[tagIds=$tagIds, isFavorite=$isFavorite, isArchived=$isArchived, description=$description]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -65,6 +76,11 @@ class UpdateAssetDto {
} else { } else {
// json[r'isArchived'] = null; // json[r'isArchived'] = null;
} }
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
return json; return json;
} }
@ -92,6 +108,7 @@ class UpdateAssetDto {
: const [], : const [],
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isArchived: mapValueOfType<bool>(json, r'isArchived'), isArchived: mapValueOfType<bool>(json, r'isArchived'),
description: mapValueOfType<String>(json, r'description'),
); );
} }
return null; return null;

View File

@ -111,6 +111,11 @@ void main() {
// TODO // TODO
}); });
// String description
test('to test the property `description`', () async {
// TODO
});
}); });

View File

@ -31,6 +31,11 @@ void main() {
// TODO // TODO
}); });
// String description
test('to test the property `description`', () async {
// TODO
});
}); });

View File

@ -1,6 +1,6 @@
import { SearchPropertiesDto } from './dto/search-properties.dto'; import { SearchPropertiesDto } from './dto/search-properties.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetEntity, AssetType } from '@app/infra/entities'; import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm/repository/Repository'; import { Repository } from 'typeorm/repository/Repository';
@ -55,6 +55,7 @@ export class AssetRepository implements IAssetRepository {
private assetRepository: Repository<AssetEntity>, private assetRepository: Repository<AssetEntity>,
@Inject(ITagRepository) private _tagRepository: ITagRepository, @Inject(ITagRepository) private _tagRepository: ITagRepository,
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
) {} ) {}
async getAllVideos(): Promise<AssetEntity[]> { async getAllVideos(): Promise<AssetEntity[]> {
@ -268,6 +269,17 @@ export class AssetRepository implements IAssetRepository {
asset.tags = tags; asset.tags = tags;
} }
if (asset.exifInfo != null) {
asset.exifInfo.description = dto.description || '';
await this.exifRepository.save(asset.exifInfo);
} else {
const exifInfo = new ExifEntity();
exifInfo.description = dto.description || '';
exifInfo.asset = asset;
await this.exifRepository.save(exifInfo);
asset.exifInfo = exifInfo;
}
return await this.assetRepository.save(asset); return await this.assetRepository.save(asset);
} }

View File

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
import { AssetController } from './asset.controller'; import { AssetController } from './asset.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from '@app/infra/entities'; import { AssetEntity, ExifEntity } from '@app/infra/entities';
import { AssetRepository, IAssetRepository } from './asset-repository'; import { AssetRepository, IAssetRepository } from './asset-repository';
import { DownloadModule } from '../../modules/download/download.module'; import { DownloadModule } from '../../modules/download/download.module';
import { TagModule } from '../tag/tag.module'; import { TagModule } from '../tag/tag.module';
@ -16,7 +16,7 @@ const ASSET_REPOSITORY_PROVIDER = {
@Module({ @Module({
imports: [ imports: [
// //
TypeOrmModule.forFeature([AssetEntity]), TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
DownloadModule, DownloadModule,
TagModule, TagModule,
AlbumModule, AlbumModule,

View File

@ -25,4 +25,8 @@ export class UpdateAssetDto {
], ],
}) })
tagIds?: string[]; tagIds?: string[];
@IsOptional()
@IsString()
description?: string;
} }

View File

@ -223,7 +223,6 @@ export class MetadataExtractionProcessor {
const newExif = new ExifEntity(); const newExif = new ExifEntity();
newExif.assetId = asset.id; newExif.assetId = asset.id;
newExif.description = '';
newExif.fileSizeInByte = data.format.size || null; newExif.fileSizeInByte = data.format.size || null;
newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null; newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null;
newExif.modifyDate = null; newExif.modifyDate = null;

View File

@ -3675,6 +3675,11 @@
"type": "string", "type": "string",
"nullable": true, "nullable": true,
"default": null "default": null
},
"description": {
"type": "string",
"nullable": true,
"default": null
} }
} }
}, },
@ -5283,6 +5288,9 @@
}, },
"isArchived": { "isArchived": {
"type": "boolean" "type": "boolean"
},
"description": {
"type": "string"
} }
} }
}, },

View File

@ -23,6 +23,7 @@ export class ExifResponseDto {
city?: string | null = null; city?: string | null = null;
state?: string | null = null; state?: string | null = null;
country?: string | null = null; country?: string | null = null;
description?: string | null = null;
} }
export function mapExif(entity: ExifEntity): ExifResponseDto { export function mapExif(entity: ExifEntity): ExifResponseDto {
@ -46,5 +47,6 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
city: entity.city, city: entity.city,
state: entity.state, state: entity.state,
country: entity.country, country: entity.country,
description: entity.description,
}; };
} }

View File

@ -343,6 +343,7 @@ const assetInfo: ExifResponseDto = {
city: 'city', city: 'city',
state: 'state', state: 'state',
country: 'country', country: 'country',
description: 'description',
}; };
const assetResponse: AssetResponseDto = { const assetResponse: AssetResponseDto = {

View File

@ -1208,6 +1208,12 @@ export interface ExifResponseDto {
* @memberof ExifResponseDto * @memberof ExifResponseDto
*/ */
'country'?: string | null; 'country'?: string | null;
/**
*
* @type {string}
* @memberof ExifResponseDto
*/
'description'?: string | null;
} }
/** /**
* *
@ -2341,6 +2347,12 @@ export interface UpdateAssetDto {
* @memberof UpdateAssetDto * @memberof UpdateAssetDto
*/ */
'isArchived'?: boolean; 'isArchived'?: boolean;
/**
*
* @type {string}
* @memberof UpdateAssetDto
*/
'description'?: string;
} }
/** /**
* *

View File

@ -251,6 +251,18 @@
} }
}); });
}; };
const disableKeyDownEvent = () => {
if (browser) {
document.removeEventListener('keydown', onKeyboardPress);
}
};
const enableKeyDownEvent = () => {
if (browser) {
document.addEventListener('keydown', onKeyboardPress);
}
};
</script> </script>
<section <section
@ -352,7 +364,13 @@
class="bg-immich-bg w-[360px] row-span-full transition-all overflow-y-auto dark:bg-immich-dark-bg dark:border-l dark:border-l-immich-dark-gray" class="bg-immich-bg w-[360px] row-span-full transition-all overflow-y-auto dark:bg-immich-dark-bg dark:border-l dark:border-l-immich-dark-gray"
translate="yes" translate="yes"
> >
<DetailPanel {asset} albums={appearsInAlbums} on:close={() => (isShowDetail = false)} /> <DetailPanel
{asset}
albums={appearsInAlbums}
on:close={() => (isShowDetail = false)}
on:description-focus-in={disableKeyDownEvent}
on:description-focus-out={enableKeyDownEvent}
/>
</div> </div>
{/if} {/if}

View File

@ -5,14 +5,26 @@
import CameraIris from 'svelte-material-icons/CameraIris.svelte'; import CameraIris from 'svelte-material-icons/CameraIris.svelte';
import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte'; import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { AssetResponseDto, AlbumResponseDto } from '@api'; import { AssetResponseDto, AlbumResponseDto, api } from '@api';
import { asByteUnitString } from '../../utils/byte-units'; import { asByteUnitString } from '../../utils/byte-units';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import type { LatLngTuple } from 'leaflet'; import type { LatLngTuple } from 'leaflet';
import { page } from '$app/stores';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = []; export let albums: AlbumResponseDto[] = [];
let textarea: HTMLTextAreaElement;
let description: string;
$: {
// Get latest description from server
if (asset.id) {
api.assetApi
.getAssetById(asset.id)
.then((res) => (textarea.value = res.data?.exifInfo?.description || ''));
}
}
$: latlng = (() => { $: latlng = (() => {
const lat = asset.exifInfo?.latitude; const lat = asset.exifInfo?.latitude;
@ -34,6 +46,27 @@
return undefined; return undefined;
}; };
const autoGrowHeight = (e: Event) => {
const target = e.target as HTMLTextAreaElement;
target.style.height = 'auto';
target.style.height = `${target.scrollHeight}px`;
};
const handleFocusIn = () => {
dispatch('description-focus-in');
};
const handleFocusOut = async () => {
dispatch('description-focus-out');
try {
await api.assetApi.updateAsset(asset.id, {
description: description
});
} catch (error) {
console.error(error);
}
};
</script> </script>
<section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> <section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
@ -48,6 +81,23 @@
<p class="text-immich-fg dark:text-immich-dark-fg text-lg">Info</p> <p class="text-immich-fg dark:text-immich-dark-fg text-lg">Info</p>
</div> </div>
<div class="mx-4 mt-10">
<textarea
bind:this={textarea}
class="max-h-[500px]
text-base text-black bg-transparent dark:text-white border-b focus:border-b-2 border-gray-500 w-full focus:border-immich-primary dark:focus:border-immich-dark-primary transition-all resize-none overflow-hidden outline-none disabled:border-none"
placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'}
style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == ''
? 'none'
: 'block'}
on:focusin={handleFocusIn}
on:focusout={handleFocusOut}
on:input={autoGrowHeight}
bind:value={description}
disabled={$page?.data?.user?.id !== asset.ownerId}
/>
</div>
<div class="px-4 py-4"> <div class="px-4 py-4">
{#if !asset.exifInfo} {#if !asset.exifInfo}
<p class="text-sm pb-4">NO EXIF INFO AVAILABLE</p> <p class="text-sm pb-4">NO EXIF INFO AVAILABLE</p>
@ -178,7 +228,7 @@
<section class="p-2 dark:text-immich-dark-fg"> <section class="p-2 dark:text-immich-dark-fg">
<div class="px-4 py-4"> <div class="px-4 py-4">
{#if albums.length > 0} {#if albums.length > 0}
<p class="text-sm pb-4 ">APPEARS IN</p> <p class="text-sm pb-4">APPEARS IN</p>
{/if} {/if}
{#each albums as album} {#each albums as album}
<a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}> <a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}>

View File

@ -24,29 +24,50 @@
}; };
const getFavoriteCount = async () => { const getFavoriteCount = async () => {
const { data: assets } = await api.assetApi.getAllAssets(true, undefined); try {
const { data: assets } = await api.assetApi.getAllAssets(true, undefined);
return { return {
favorites: assets.length favorites: assets.length
}; };
} catch {
return {
favorites: 0
};
}
}; };
const getAlbumCount = async () => { const getAlbumCount = async () => {
const { data: albumCount } = await api.albumApi.getAlbumCountByUserId(); try {
return { const { data: albumCount } = await api.albumApi.getAlbumCountByUserId();
shared: albumCount.shared, return {
sharing: albumCount.sharing, shared: albumCount.shared,
owned: albumCount.owned sharing: albumCount.sharing,
}; owned: albumCount.owned
};
} catch {
return {
shared: 0,
sharing: 0,
owned: 0
};
}
}; };
const getArchivedAssetsCount = async () => { const getArchivedAssetsCount = async () => {
const { data: assetCount } = await api.assetApi.getArchivedAssetCountByUserId(); try {
const { data: assetCount } = await api.assetApi.getArchivedAssetCountByUserId();
return { return {
videos: assetCount.videos, videos: assetCount.videos,
photos: assetCount.photos photos: assetCount.photos
}; };
} catch {
return {
videos: 0,
photos: 0
};
}
}; };
</script> </script>