mirror of
https://github.com/immich-app/immich.git
synced 2024-11-15 09:59:00 -07:00
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:
parent
561b208508
commit
a9859bc029
@ -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
BIN
mobile/flutter_01.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 679 KiB |
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
103
mobile/lib/modules/asset_viewer/ui/description_input.dart
Normal file
103
mobile/lib/modules/asset_viewer/ui/description_input.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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!),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
1
mobile/openapi/doc/ExifResponseDto.md
generated
1
mobile/openapi/doc/ExifResponseDto.md
generated
@ -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)
|
||||||
|
|
||||||
|
1
mobile/openapi/doc/UpdateAssetDto.md
generated
1
mobile/openapi/doc/UpdateAssetDto.md
generated
@ -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)
|
||||||
|
|
||||||
|
17
mobile/openapi/lib/model/exif_response_dto.dart
generated
17
mobile/openapi/lib/model/exif_response_dto.dart
generated
@ -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;
|
||||||
|
23
mobile/openapi/lib/model/update_asset_dto.dart
generated
23
mobile/openapi/lib/model/update_asset_dto.dart
generated
@ -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;
|
||||||
|
5
mobile/openapi/test/exif_response_dto_test.dart
generated
5
mobile/openapi/test/exif_response_dto_test.dart
generated
@ -111,6 +111,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String description
|
||||||
|
test('to test the property `description`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
5
mobile/openapi/test/update_asset_dto_test.dart
generated
5
mobile/openapi/test/update_asset_dto_test.dart
generated
@ -31,6 +31,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String description
|
||||||
|
test('to test the property `description`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -25,4 +25,8 @@ export class UpdateAssetDto {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
||||||
|
12
web/src/api/open-api/api.ts
generated
12
web/src/api/open-api/api.ts
generated
@ -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;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -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}
|
||||||
|
|
||||||
|
@ -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}`}>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user