feat: Add description (#2237)

* Added dto, logic to insert description and web implementation

* create text field and update on remote database

* Update description and save changes

* styling

* fix web test

* fix server test

* preserve description on metadata extraction job run

* handle exif info is null situation

* pr feedback

* format openapi spec

* update createAssetDto

* refactor logic to service

* move files

* only owner can update description

* Render description correctly in shared album

* Render description correctly in shared link

* disable description edit for not owner of asset on mobile

* localization and clean up

* fix test

* Uses providers for description text (#2244)

* uses providers for description text

* comments

* fixes initial data setting

* fixes notifier

---------

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

View file

@ -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),
],
);
},
),
),
),
),