mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00: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:
@@ -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_map/flutter_map.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/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||
|
||||
class ExifBottomSheet extends HookConsumerWidget {
|
||||
final Asset assetDetail;
|
||||
final Asset asset;
|
||||
|
||||
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||
: super(key: key);
|
||||
const ExifBottomSheet({Key? key, required this.asset}) : super(key: key);
|
||||
|
||||
bool get showMap =>
|
||||
assetDetail.exifInfo?.latitude != null &&
|
||||
assetDetail.exifInfo?.longitude != null;
|
||||
asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null;
|
||||
|
||||
@override
|
||||
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() {
|
||||
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) {
|
||||
String resolution = a.width != null && a.height != null
|
||||
? "${a.height} x ${a.width} "
|
||||
@@ -128,13 +115,39 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
"exif_bottom_sheet_location",
|
||||
style: TextStyle(fontSize: 11, color: textColor),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
buildMap(),
|
||||
if (exifInfo != null &&
|
||||
exifInfo.city != null &&
|
||||
exifInfo.state != null)
|
||||
buildLocationText(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
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(
|
||||
"${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
@@ -146,7 +159,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
buildDate() {
|
||||
final fileCreatedAt = assetDetail.fileCreatedAt.toLocal();
|
||||
final fileCreatedAt = asset.fileCreatedAt.toLocal();
|
||||
final date = DateFormat.yMMMEd().format(fileCreatedAt);
|
||||
final time = DateFormat.jm().format(fileCreatedAt);
|
||||
|
||||
@@ -167,27 +180,37 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"exif_bottom_sheet_details",
|
||||
style: TextStyle(fontSize: 11, color: textColor),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
leading: const Icon(Icons.image),
|
||||
leading: Icon(
|
||||
Icons.image,
|
||||
color: textColor.withAlpha(200),
|
||||
),
|
||||
title: Text(
|
||||
assetDetail.fileName,
|
||||
asset.fileName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
subtitle: buildSizeText(assetDetail),
|
||||
subtitle: buildSizeText(asset),
|
||||
),
|
||||
if (exifInfo?.make != null)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
leading: const Icon(Icons.camera),
|
||||
leading: Icon(
|
||||
Icons.camera,
|
||||
color: textColor.withAlpha(200),
|
||||
),
|
||||
title: Text(
|
||||
"${exifInfo!.make} ${exifInfo.model}",
|
||||
style: TextStyle(
|
||||
@@ -203,80 +226,75 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Card(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(15),
|
||||
topRight: Radius.circular(15),
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Card(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(15),
|
||||
topRight: Radius.circular(15),
|
||||
),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.all(0),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > 600) {
|
||||
// Two column
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildDragHeader(),
|
||||
buildDate(),
|
||||
const SizedBox(height: 32.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: showMap ? 5 : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: buildLocation(),
|
||||
margin: const EdgeInsets.all(0),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > 600) {
|
||||
// Two column
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildDragHeader(),
|
||||
buildDate(),
|
||||
if (asset.isRemote) DescriptionInput(asset: asset),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: showMap ? 5 : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: buildLocation(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: buildDetail(),
|
||||
Flexible(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: buildDetail(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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: 50),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
buildLocation(),
|
||||
const SizedBox(height: 16.0),
|
||||
Divider(
|
||||
thickness: 1,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
buildDetail(),
|
||||
const SizedBox(height: 50),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// One column
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildDragHeader(),
|
||||
buildDate(),
|
||||
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)) {
|
||||
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_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -25,19 +26,25 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
return ExpansionTile(
|
||||
textColor: Theme.of(context).primaryColor,
|
||||
title: const Text(
|
||||
"Advanced",
|
||||
"advanced_settings_tile_title",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
).tr(),
|
||||
subtitle: const Text(
|
||||
"advanced_settings_tile_subtitle",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
),
|
||||
).tr(),
|
||||
children: [
|
||||
SettingsSwitchListTile(
|
||||
enabled: true,
|
||||
appSettingService: appSettingService,
|
||||
valueNotifier: isEnabled,
|
||||
settingsEnum: AppSettingsEnum.advancedTroubleshooting,
|
||||
title: "Troubleshooting",
|
||||
subtitle: "Enable additional features for troubleshooting",
|
||||
title: "advanced_settings_troubleshooting_title".tr(),
|
||||
subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ class ExifInfo {
|
||||
String? city;
|
||||
String? state;
|
||||
String? country;
|
||||
String? description;
|
||||
|
||||
@ignore
|
||||
String get exposureTime {
|
||||
@@ -58,7 +59,8 @@ class ExifInfo {
|
||||
long = dto.longitude?.toDouble(),
|
||||
city = dto.city,
|
||||
state = dto.state,
|
||||
country = dto.country;
|
||||
country = dto.country,
|
||||
description = dto.description;
|
||||
|
||||
ExifInfo({
|
||||
this.fileSize,
|
||||
@@ -74,6 +76,7 @@ class ExifInfo {
|
||||
this.city,
|
||||
this.state,
|
||||
this.country,
|
||||
this.description,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,58 +27,63 @@ const ExifInfoSchema = CollectionSchema(
|
||||
name: r'country',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'exposureSeconds': PropertySchema(
|
||||
r'description': PropertySchema(
|
||||
id: 2,
|
||||
name: r'description',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'exposureSeconds': PropertySchema(
|
||||
id: 3,
|
||||
name: r'exposureSeconds',
|
||||
type: IsarType.float,
|
||||
),
|
||||
r'f': PropertySchema(
|
||||
id: 3,
|
||||
id: 4,
|
||||
name: r'f',
|
||||
type: IsarType.float,
|
||||
),
|
||||
r'fileSize': PropertySchema(
|
||||
id: 4,
|
||||
id: 5,
|
||||
name: r'fileSize',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'iso': PropertySchema(
|
||||
id: 5,
|
||||
id: 6,
|
||||
name: r'iso',
|
||||
type: IsarType.int,
|
||||
),
|
||||
r'lat': PropertySchema(
|
||||
id: 6,
|
||||
id: 7,
|
||||
name: r'lat',
|
||||
type: IsarType.float,
|
||||
),
|
||||
r'lens': PropertySchema(
|
||||
id: 7,
|
||||
id: 8,
|
||||
name: r'lens',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'long': PropertySchema(
|
||||
id: 8,
|
||||
id: 9,
|
||||
name: r'long',
|
||||
type: IsarType.float,
|
||||
),
|
||||
r'make': PropertySchema(
|
||||
id: 9,
|
||||
id: 10,
|
||||
name: r'make',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'mm': PropertySchema(
|
||||
id: 10,
|
||||
id: 11,
|
||||
name: r'mm',
|
||||
type: IsarType.float,
|
||||
),
|
||||
r'model': PropertySchema(
|
||||
id: 11,
|
||||
id: 12,
|
||||
name: r'model',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'state': PropertySchema(
|
||||
id: 12,
|
||||
id: 13,
|
||||
name: r'state',
|
||||
type: IsarType.string,
|
||||
)
|
||||
@@ -115,6 +120,12 @@ int _exifInfoEstimateSize(
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.description;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.lens;
|
||||
if (value != null) {
|
||||
@@ -150,17 +161,18 @@ void _exifInfoSerialize(
|
||||
) {
|
||||
writer.writeString(offsets[0], object.city);
|
||||
writer.writeString(offsets[1], object.country);
|
||||
writer.writeFloat(offsets[2], object.exposureSeconds);
|
||||
writer.writeFloat(offsets[3], object.f);
|
||||
writer.writeLong(offsets[4], object.fileSize);
|
||||
writer.writeInt(offsets[5], object.iso);
|
||||
writer.writeFloat(offsets[6], object.lat);
|
||||
writer.writeString(offsets[7], object.lens);
|
||||
writer.writeFloat(offsets[8], object.long);
|
||||
writer.writeString(offsets[9], object.make);
|
||||
writer.writeFloat(offsets[10], object.mm);
|
||||
writer.writeString(offsets[11], object.model);
|
||||
writer.writeString(offsets[12], object.state);
|
||||
writer.writeString(offsets[2], object.description);
|
||||
writer.writeFloat(offsets[3], object.exposureSeconds);
|
||||
writer.writeFloat(offsets[4], object.f);
|
||||
writer.writeLong(offsets[5], object.fileSize);
|
||||
writer.writeInt(offsets[6], object.iso);
|
||||
writer.writeFloat(offsets[7], object.lat);
|
||||
writer.writeString(offsets[8], object.lens);
|
||||
writer.writeFloat(offsets[9], object.long);
|
||||
writer.writeString(offsets[10], object.make);
|
||||
writer.writeFloat(offsets[11], object.mm);
|
||||
writer.writeString(offsets[12], object.model);
|
||||
writer.writeString(offsets[13], object.state);
|
||||
}
|
||||
|
||||
ExifInfo _exifInfoDeserialize(
|
||||
@@ -172,17 +184,18 @@ ExifInfo _exifInfoDeserialize(
|
||||
final object = ExifInfo(
|
||||
city: reader.readStringOrNull(offsets[0]),
|
||||
country: reader.readStringOrNull(offsets[1]),
|
||||
exposureSeconds: reader.readFloatOrNull(offsets[2]),
|
||||
f: reader.readFloatOrNull(offsets[3]),
|
||||
fileSize: reader.readLongOrNull(offsets[4]),
|
||||
iso: reader.readIntOrNull(offsets[5]),
|
||||
lat: reader.readFloatOrNull(offsets[6]),
|
||||
lens: reader.readStringOrNull(offsets[7]),
|
||||
long: reader.readFloatOrNull(offsets[8]),
|
||||
make: reader.readStringOrNull(offsets[9]),
|
||||
mm: reader.readFloatOrNull(offsets[10]),
|
||||
model: reader.readStringOrNull(offsets[11]),
|
||||
state: reader.readStringOrNull(offsets[12]),
|
||||
description: reader.readStringOrNull(offsets[2]),
|
||||
exposureSeconds: reader.readFloatOrNull(offsets[3]),
|
||||
f: reader.readFloatOrNull(offsets[4]),
|
||||
fileSize: reader.readLongOrNull(offsets[5]),
|
||||
iso: reader.readIntOrNull(offsets[6]),
|
||||
lat: reader.readFloatOrNull(offsets[7]),
|
||||
lens: reader.readStringOrNull(offsets[8]),
|
||||
long: reader.readFloatOrNull(offsets[9]),
|
||||
make: reader.readStringOrNull(offsets[10]),
|
||||
mm: reader.readFloatOrNull(offsets[11]),
|
||||
model: reader.readStringOrNull(offsets[12]),
|
||||
state: reader.readStringOrNull(offsets[13]),
|
||||
);
|
||||
object.id = id;
|
||||
return object;
|
||||
@@ -200,27 +213,29 @@ P _exifInfoDeserializeProp<P>(
|
||||
case 1:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 2:
|
||||
return (reader.readFloatOrNull(offset)) as P;
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 3:
|
||||
return (reader.readFloatOrNull(offset)) as P;
|
||||
case 4:
|
||||
return (reader.readLongOrNull(offset)) as P;
|
||||
return (reader.readFloatOrNull(offset)) as P;
|
||||
case 5:
|
||||
return (reader.readIntOrNull(offset)) as P;
|
||||
return (reader.readLongOrNull(offset)) as P;
|
||||
case 6:
|
||||
return (reader.readFloatOrNull(offset)) as P;
|
||||
return (reader.readIntOrNull(offset)) as P;
|
||||
case 7:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
return (reader.readFloatOrNull(offset)) as P;
|
||||
case 8:
|
||||
return (reader.readFloatOrNull(offset)) as P;
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 9:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 10:
|
||||
return (reader.readFloatOrNull(offset)) as P;
|
||||
case 11:
|
||||
case 10:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readFloatOrNull(offset)) as P;
|
||||
case 12:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 13:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
default:
|
||||
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>
|
||||
exposureSecondsIsNull() {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'exposureSeconds');
|
||||
|
||||
Reference in New Issue
Block a user