Implemented EXIF store and display (#19)

* Added EXIF extracting in the backend
* Added EXIF displaying on `image_viewer_page.dart`
* Added Icon for backup option not enable
This commit is contained in:
Alex 2022-02-10 20:40:11 -06:00 committed by GitHub
parent d1498506a8
commit de1dbcea9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1092 additions and 847 deletions

View file

@ -0,0 +1,45 @@
import 'dart:convert';
class ImageViewerPageState {
final bool isBottomSheetEnable;
ImageViewerPageState({
required this.isBottomSheetEnable,
});
ImageViewerPageState copyWith({
bool? isBottomSheetEnable,
}) {
return ImageViewerPageState(
isBottomSheetEnable: isBottomSheetEnable ?? this.isBottomSheetEnable,
);
}
Map<String, dynamic> toMap() {
return {
'isBottomSheetEnable': isBottomSheetEnable,
};
}
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
return ImageViewerPageState(
isBottomSheetEnable: map['isBottomSheetEnable'] ?? false,
);
}
String toJson() => json.encode(toMap());
factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
@override
String toString() => 'ImageViewerPageState(isBottomSheetEnable: $isBottomSheetEnable)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ImageViewerPageState && other.isBottomSheetEnable == isBottomSheetEnable;
}
@override
int get hashCode => isBottomSheetEnable.hashCode;
}

View file

@ -0,0 +1,21 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class ImageViewerPageStateNotifier extends StateNotifier<ImageViewerPageState> {
ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false));
void toggleBottomSheet() {
bool isBottomSheetEnable = state.isBottomSheetEnable;
if (isBottomSheetEnable) {
state.copyWith(isBottomSheetEnable: false);
} else {
state.copyWith(isBottomSheetEnable: true);
}
}
}
final homePageStateProvider = StateNotifierProvider<ImageViewerPageStateNotifier, ImageViewerPageState>(
((ref) => ImageViewerPageStateNotifier()));

View file

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
class ExifBottomSheet extends ConsumerWidget {
final ImmichAssetWithExif assetDetail;
const ExifBottomSheet({Key? key, required this.assetDetail}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
child: ListView(
children: [
assetDetail.exifInfo?.dateTimeOriginal != null
? Text(
DateFormat('E, LLL d, y • h:mm a').format(
DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
),
style: TextStyle(
color: Colors.grey[400],
fontWeight: FontWeight.bold,
fontSize: 14,
),
)
: Container(),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(
"Add Description...",
style: TextStyle(
color: Colors.grey[500],
fontSize: 11,
),
),
),
// Location
assetDetail.exifInfo?.latitude != null
? Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
Text(
"LOCATION",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
Text(
"${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
)
],
),
)
: Container(),
// Detail
assetDetail.exifInfo != null
? Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"DETAILS",
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.image),
title: Text(
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"${assetDetail.exifInfo?.exifImageHeight!} x ${assetDetail.exifInfo?.exifImageWidth!} ${assetDetail.exifInfo?.fileSizeInByte!}B "),
),
assetDetail.exifInfo?.make != null
? ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
textColor: Colors.grey[300],
iconColor: Colors.grey[300],
leading: const Icon(Icons.camera),
title: Text(
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
)
: Container()
],
),
)
: Container()
],
),
);
}
}

View file

@ -0,0 +1,57 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
const TopControlAppBar({Key? key, required this.asset, required this.onMoreInfoPressed}) : super(key: key);
final ImmichAsset asset;
final Function onMoreInfoPressed;
@override
Widget build(BuildContext context) {
double iconSize = 18.0;
return AppBar(
foregroundColor: Colors.grey[100],
toolbarHeight: 60,
backgroundColor: Colors.black,
leading: IconButton(
onPressed: () {
AutoRouter.of(context).pop();
},
icon: const Icon(
Icons.arrow_back_ios_new_rounded,
size: 20.0,
),
),
actions: [
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
print("backup");
},
icon: const Icon(Icons.backup_outlined),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
print("favorite");
},
icon: asset.isFavorite ? const Icon(Icons.favorite_rounded) : const Icon(Icons.favorite_border_rounded),
),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onMoreInfoPressed();
},
icon: const Icon(Icons.more_horiz_rounded))
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View file

@ -0,0 +1,85 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:photo_view/photo_view.dart';
// ignore: must_be_immutable
class ImageViewerPage extends HookConsumerWidget {
final String imageUrl;
final String heroTag;
final String thumbnailUrl;
final ImmichAsset asset;
final AssetService _assetService = AssetService();
ImmichAssetWithExif? assetDetail;
ImageViewerPage(
{Key? key, required this.imageUrl, required this.heroTag, required this.thumbnailUrl, required this.asset})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var box = Hive.box(userInfoBox);
getAssetExif() async {
assetDetail = await _assetService.getAssetById(asset.id);
}
useEffect(() {
getAssetExif();
}, []);
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
asset: asset,
onMoreInfoPressed: () {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
});
},
),
body: Center(
child: Hero(
tag: heroTag,
child: CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
errorWidget: (context, url, error) => const Icon(Icons.error),
imageBuilder: (context, imageProvider) {
return PhotoView(imageProvider: imageProvider);
},
placeholder: (context, url) {
return CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: thumbnailUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
placeholderFadeInDuration: const Duration(milliseconds: 0),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
);
},
),
),
),
);
}
}

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
class AssetService {
@ -58,4 +59,21 @@ class AssetService {
return [];
}
}
Future<ImmichAssetWithExif?> getAssetById(String assetId) async {
try {
var res = await _networkService.getRequest(
url: "asset/assetById/$assetId",
);
Map<String, dynamic> decodedData = jsonDecode(res.toString());
ImmichAssetWithExif result = ImmichAssetWithExif.fromMap(decodedData);
print("result $result");
return result;
} catch (e) {
debugPrint("Error getAllAsset ${e.toString()}");
return null;
}
}
}

View file

@ -1,7 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/backup_state.model.dart';
@ -20,79 +23,89 @@ class ImmichSliverAppBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final BackUpState _backupState = ref.watch(backupProvider);
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
sliver: SliverAppBar(
centerTitle: true,
floating: true,
pinned: false,
snap: false,
backgroundColor: Colors.grey[200],
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
leading: Builder(
builder: (BuildContext context) {
return IconButton(
icon: const Icon(Icons.account_circle_rounded),
onPressed: () {
Scaffold.of(context).openDrawer();
},
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
);
},
),
title: Text(
'IMMICH',
style: GoogleFonts.snowburstOne(
textStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Theme.of(context).primaryColor,
),
),
),
actions: [
Stack(
alignment: AlignmentDirectional.center,
children: [
_backupState.backupProgress == BackUpProgressEnum.inProgress
? Positioned(
top: 10,
right: 12,
child: SizedBox(
height: 8,
width: 8,
child: CircularProgressIndicator(
strokeWidth: 1,
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
),
),
)
: Container(),
IconButton(
icon: const Icon(Icons.backup_rounded),
tooltip: 'Backup Controller',
onPressed: () async {
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
if (onPop == true) {
onPopBack!();
}
},
),
_backupState.backupProgress == BackUpProgressEnum.inProgress
? Positioned(
bottom: 5,
child: Text(
_backupState.backingUpAssetCount.toString(),
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
),
)
: Container()
],
),
],
bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
return SliverAppBar(
centerTitle: true,
floating: true,
pinned: false,
snap: false,
backgroundColor: Colors.grey[200],
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
leading: Builder(
builder: (BuildContext context) {
return IconButton(
icon: const Icon(Icons.account_circle_rounded),
onPressed: () {
Scaffold.of(context).openDrawer();
},
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
);
},
),
title: Text(
'IMMICH',
style: GoogleFonts.snowburstOne(
textStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Theme.of(context).primaryColor,
),
),
),
actions: [
Stack(
alignment: AlignmentDirectional.center,
children: [
_backupState.backupProgress == BackUpProgressEnum.inProgress
? Positioned(
top: 10,
right: 12,
child: SizedBox(
height: 8,
width: 8,
child: CircularProgressIndicator(
strokeWidth: 1,
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
),
),
)
: Container(),
IconButton(
splashRadius: 25,
iconSize: 30,
icon: _isEnableAutoBackup
? const Icon(Icons.backup_rounded)
: Badge(
padding: const EdgeInsets.all(4),
elevation: 1,
position: BadgePosition.bottomEnd(bottom: -4, end: -4),
badgeColor: Colors.white,
badgeContent: const Icon(
Icons.cloud_off_rounded,
size: 8,
),
child: const Icon(Icons.backup_rounded)),
tooltip: 'Backup Controller',
onPressed: () async {
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
if (onPop == true) {
onPopBack!();
}
},
),
_backupState.backupProgress == BackUpProgressEnum.inProgress
? Positioned(
bottom: 5,
child: Text(
_backupState.backingUpAssetCount.toString(),
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
),
)
: Container()
],
),
],
);
}
}

View file

@ -56,6 +56,7 @@ class ThumbnailImage extends HookConsumerWidget {
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
heroTag: asset.id,
thumbnailUrl: thumbnailRequestUrl,
asset: asset,
),
);
} else {

View file

@ -15,7 +15,7 @@ class LoginForm extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final usernameController = useTextEditingController(text: 'testuser@email.com');
final passwordController = useTextEditingController(text: 'password');
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283');
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.204:2283');
return Center(
child: ConstrainedBox(

View file

@ -1,7 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/login/ui/login_form.dart';
class LoginPage extends HookConsumerWidget {