Commit 0532db5a authored by Vanja Matija's avatar Vanja Matija
Browse files

fix: implement to fetch all image from iOS device

parent b28d3b74
Showing with 374 additions and 80 deletions
+374 -80
import UIKit
import Flutter
import Photos
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
var images = [String]()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "io.memri.photos.channel",
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard call.method == "getAllImages" else {
result(FlutterMethodNotImplemented)
return
}
self.images = []
self.fetchAllImages(result: result)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func fetchAllImages(result: @escaping FlutterResult) {
PHPhotoLibrary.requestAuthorization { (status) in
switch status {
case .authorized:
print("Permission is Authorized")
self.getPhotos(result: result)
case .denied, .restricted:
print("Permission is Not allowed")
result(FlutterError(code: "UNAVAILABLE",
message: "Permission is Not allowed",
details: nil))
case .notDetermined:
print("Permission is Not determined yet")
result(FlutterError(code: "UNAVAILABLE",
message: "Permission is Not determined yet",
details: nil))
default:
result(FlutterError(code: "UNAVAILABLE",
message: "Permission is Not determined yet",
details: nil))
}
}
}
fileprivate func getPhotos(result: @escaping FlutterResult) {
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = false
requestOptions.deliveryMode = .highQualityFormat
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let results: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
fetchAsset(i: 0, assets: results, result: result)
// if results.count > 0 {
// var i = 0
// while i < results.count {
// let asset = results.object(at: i)
// asset.requestContentEditingInput(with: PHContentEditingInputRequestOptions()) { input, _ in
// if !(input?.fullSizeImageURL?.absoluteString ?? "").isEmpty {
// self.images.append(input!.fullSizeImageURL!.absoluteString)
// }
// i+=1
// }
// }
// return images
// } else {
// return [];
// }
}
func fetchAsset(i: Int, assets: PHFetchResult<PHAsset>, result: @escaping FlutterResult) {
if (i >= assets.count) {
result(images)
return
}
let asset = assets.object(at: i)
asset.requestContentEditingInput(with: PHContentEditingInputRequestOptions()) { input, _ in
if !(input?.fullSizeImageURL?.path ?? "").isEmpty {
self.images.append(input!.fullSizeImageURL?.path ?? "")
}
self.fetchAsset(i: i+1, assets: assets, result: result)
}
}
}
......@@ -36,34 +36,48 @@ class Item {
List<Item>? getEdgeTargets(String edgeName) => edges[edgeName]?.targets;
dynamic get(String propertyName) {
return properties[propertyName];
if (properties.containsKey(propertyName)) {
return properties[propertyName];
}
return null;
}
dynamic getThumbnail() {
return properties['thumbnail'];
return get('thumbnail');
}
dynamic getFile() {
return get('file');
}
bool isFromDevice() {
return get('type') == 'device';
}
dynamic getFullSize() {
return properties['file'];
return get('file');
}
String? getName() {
return properties['filename'];
return get('filename');
}
DateTime? getCreated() {
return DateTime.fromMillisecondsSinceEpoch(properties['dateCreated']).toLocal();
if (get('dateCreated') != null) {
return DateTime.fromMillisecondsSinceEpoch(get('dateCreated')).toLocal();
}
return null;
}
Size? getSize() {
return Size(properties['width'], properties['height']);
return Size(get('width') ?? 0, get('height') ?? 0);
}
List<ItemLocation> getLocation() {
final values = getEdgeTargets('location');
final List<ItemLocation> locations = [];
values?.forEach((element) {
locations.add(ItemLocation(element.properties['latitude'], element.properties['longitude']));
locations.add(ItemLocation(element.get('latitude'), element.get('longitude')));
});
return locations;
}
......
import 'dart:async';
import 'package:photos_app/constants/app_configs.dart';
import 'dart:io';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:photos_app/constants/app_logger.dart';
import 'package:photos_app/core/models/home_page_tab.dart';
import 'package:photos_app/core/models/item.dart';
......@@ -7,14 +12,8 @@ import 'package:photos_app/core/services/analytics_service.dart';
import 'package:photos_app/core/services/graph_service.dart';
import 'package:photos_app/core/services/pod_service.dart';
import 'package:photos_app/utilities/helpers/app_helper.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:photos_app/widgets/content_widget.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:dio/dio.dart';
import '../configs/routes/route_navigator.dart';
class PhotoProvider with ChangeNotifier {
final GraphService _graphService;
......@@ -31,10 +30,8 @@ class PhotoProvider with ChangeNotifier {
// Controllers
ItemScrollController categoryItemScrollController = ItemScrollController();
ItemScrollController savedCategoryItemScrollController =
ItemScrollController();
late ItemPositionsListener feedPositionsListener =
ItemPositionsListener.create();
ItemScrollController savedCategoryItemScrollController = ItemScrollController();
late ItemPositionsListener feedPositionsListener = ItemPositionsListener.create();
// Animations
late AnimationController subcategoryAnimationController;
......@@ -61,7 +58,9 @@ class PhotoProvider with ChangeNotifier {
];
bool get showAppbar => _showAppbar;
int get homePagesIndex => _homePagesIndex;
HomePageTab get selectedHomePageTab => homePages[_homePagesIndex];
DataSourceStatus status = DataSourceStatus.initial;
......@@ -70,7 +69,10 @@ class PhotoProvider with ChangeNotifier {
/// Getters
///
List<Item> get photoList => _photos.values.toList();
List<Item> devicePhotos = [];
List<Item> get photoList => isFetchFromDevice ? devicePhotos : _photos.values.toList();
List<String> get filter => _filter.toList();
///
......@@ -92,6 +94,7 @@ class PhotoProvider with ChangeNotifier {
/// Methods
///
Future<void> init() async {
isFetchFromDevice = false;
await _fetchPhotos();
}
......@@ -101,8 +104,7 @@ class PhotoProvider with ChangeNotifier {
if (res.isNotEmpty) {
if (filter.isEmpty) {
items = res;
}
else {
} else {
items = res.where((item) => filter.contains(item.id)).toList();
}
}
......@@ -131,6 +133,11 @@ class PhotoProvider with ChangeNotifier {
}
Future<void> onRefresh() async {
if (isFetchFromDevice) {
status = devicePhotos.isEmpty ? DataSourceStatus.empty : DataSourceStatus.success;
notifyListeners();
return;
}
status = DataSourceStatus.refreshing;
notifyListeners();
await _fetchPhotos();
......@@ -142,12 +149,8 @@ class PhotoProvider with ChangeNotifier {
homePagesIndex = 0;
}
Future<dynamic> makePostRequest(
String baseUrl,
String path,
String method,
{Map<String, dynamic> query = const {},
Map<String, dynamic> jsonBody = const {}}) async {
Future<dynamic> makePostRequest(String baseUrl, String path, String method,
{Map<String, dynamic> query = const {}, Map<String, dynamic> jsonBody = const {}}) async {
AppLogger.debug('Making network HTTP Request to $baseUrl $path $method');
BaseOptions options = BaseOptions(baseUrl: baseUrl);
Dio dio = Dio(options);
......@@ -256,7 +259,7 @@ class PhotoProvider with ChangeNotifier {
notifyListeners();
}
Future<bool> uploadPhoto(String sha, List<int> bytes) async {
Future<bool> uploadPhoto(String sha, Uint8List bytes) async {
try {
await _podService.uploadFile(sha, bytes);
return true;
......@@ -265,6 +268,35 @@ class PhotoProvider with ChangeNotifier {
}
}
Future<void> addDevicePhotos(List<File>? files) async {
if ((files ?? []).isEmpty) {
return;
}
isFetchFromDevice = true;
status = files!.isEmpty ? DataSourceStatus.empty : DataSourceStatus.success;
for (final image in files) {
final name = image.path.split('/').last.split('.').first;
if (devicePhotos.any((element) => element.getName() == name)) {
continue;
}
// final bytes = await File(image.path).readAsBytes();
// final sha = sha256.convert(bytes).toString();
// debugPrint('--Sha256: $sha');
final Map<String, dynamic> properties = {};
properties['id'] = Random(1000000).toString();
properties['filename'] = name;
properties['file'] = image;
properties['type'] = 'device';
final item = Item(type: 'Photo', properties: properties);
// final edge = Edge(
// source: item,
// target:
// Item(type: 'file', properties: {'sha256': sha, 'filename': name, 'externalId': name}),
// name: 'file');
devicePhotos.add(item);
}
notifyListeners();
}
void reset() {
_homePagesIndex = 0;
......
......@@ -9,10 +9,14 @@ import 'package:image_picker/image_picker.dart';
import 'package:photos_app/configs/routes/route_navigator.dart';
import 'package:photos_app/constants/app_logger.dart';
import 'package:photos_app/core/models/item.dart';
import 'package:photos_app/providers/app_provider.dart';
import 'package:photos_app/providers/auth_provider.dart';
import 'package:photos_app/providers/photo_provider.dart';
import 'package:photos_app/utilities/helpers/app_helper.dart';
import 'package:photos_app/utilities/helpers/platform_images.dart';
import 'package:photos_app/widgets/importer_card.dart';
import 'package:photos_app/widgets/loading_indicator.dart';
import 'package:photos_app/widgets/platform_image_picker.dart';
import 'package:provider/provider.dart';
import '../../core/models/plugin/plugin.dart';
......@@ -43,7 +47,7 @@ class _ImportersScreenState extends State<ImportersScreen> {
// 'WhatsappPlugin': Plugin('Whatsapp', 'WhatsappPlugin', 'whatsapp.plugin', 'gitlab.memri.io:5050/memri/plugins/whatsapp-multi-device:dev-latest', 'whatsapp'),
'ICloud': Plugin('Icloud Photos', 'ICloud', 'icloud_photos.plugin',
'gitlab.memri.io:5050/memri/plugins/icloud-photos:dev-latest', 'apple'),
// 'DeviceImporter': Plugin('From this device', 'DeviceImporter', 'app_method', 'photos_app', 'device'),
'DeviceImporter': Plugin('From this device', 'DeviceImporter', 'app_method', 'photos_app', 'device'),
// 'ImageIndexer': Plugin('Process Images', 'ImageIndexer', 'image_indexer.plugin', 'image-indexer', 'memri')
};
......@@ -73,26 +77,29 @@ class _ImportersScreenState extends State<ImportersScreen> {
Widget build(BuildContext context) {
void _onPickImages() async {
Navigator.of(context).pop();
final ImagePicker picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
if (image == null) return;
debugPrint('--GOT IMAGE');
final name = image.path.split('/').last.split('.').first;
debugPrint('--Name: $name');
final bytes = await File(image.path).readAsBytes();
final sha = sha256.convert(bytes).toString();
debugPrint('--Sha256: $sha');
// final exif = await readExifFromBytes(bytes);
final Map<String, dynamic> properties = {};
// properties['id'] = name;
properties['name'] = name;
final item = Item(type: 'Photo', properties: properties);
final edge = Edge(
source: item,
target:
Item(type: 'file', properties: {'sha256': sha, 'filename': name, 'externalId': name}),
name: 'file');
debugPrint('--DO BULK ACTION--');
final images = await PlatformImagePicker.showPickMultipleImage(context);
await context.read<PhotoProvider>().addDevicePhotos(images);
Navigator.of(context).pop();
// final ImagePicker picker = ImagePicker();
// final image = await picker.pickImage(source: ImageSource.gallery);
// if (image == null) return;
// debugPrint('--GOT IMAGE');
// final name = image.path.split('/').last.split('.').first;
// debugPrint('--Name: $name');
// final bytes = await File(image.path).readAsBytes();
// final sha = sha256.convert(bytes).toString();
// debugPrint('--Sha256: $sha');
// // final exif = await readExifFromBytes(bytes);
// final Map<String, dynamic> properties = {};
// // properties['id'] = name;
// properties['name'] = name;
// final item = Item(type: 'Photo', properties: properties);
// final edge = Edge(
// source: item,
// target:
// Item(type: 'file', properties: {'sha256': sha, 'filename': name, 'externalId': name}),
// name: 'file');
// debugPrint('--DO BULK ACTION--');
// bool isSuccess = false;
// try {
// // final data = await context.read<AppProvider>().podService.bulkAction(
......@@ -107,11 +114,16 @@ class _ImportersScreenState extends State<ImportersScreen> {
// }
// if (!isSuccess) return;
// debugPrint('--DO UPLOAD PHOTO--');
final isUploaded = await context.read<PhotoProvider>().uploadPhoto(sha, bytes);
debugPrint('--UPLOAD SUCCESS: $isUploaded');
// final isUploaded = await context.read<PhotoProvider>().uploadPhoto(sha, bytes);
// debugPrint('--UPLOAD SUCCESS: $isUploaded');
}
void _onPickAllImages() {
Future<void> _onPickAllImages() async {
Navigator.of(context).pop();
IgnoreLoadingIndicator().show(context);
final images = await PlatformImages.fetchAllDeviceImages();
IgnoreLoadingIndicator().hide(context);
await context.read<PhotoProvider>().addDevicePhotos(images);
Navigator.of(context).pop();
}
......
......@@ -54,12 +54,17 @@ class _PhotoDetailScreenState extends State<PhotoDetailScreen> {
height: 500,
width: double.infinity,
color: AppColors().white,
child: DefaultImageWidget.fromMemory(
photo?.getThumbnail(),
id: photo?.id,
fullSizeImage: photo?.getFullSize(),
fit: BoxFit.fitWidth,
),
child: (photo?.isFromDevice() ?? false)
? DefaultImageWidget.fromFile(
photo?.getFile(),
fit: BoxFit.fitWidth,
)
: DefaultImageWidget.fromMemory(
photo?.getThumbnail(),
id: photo?.id,
fullSizeImage: photo?.getFullSize(),
fit: BoxFit.fitWidth,
),
),
const Spacing(),
_buildInfo(photo),
......@@ -87,7 +92,8 @@ class _PhotoDetailScreenState extends State<PhotoDetailScreen> {
List<Widget> labelWidgets = [];
labelWidgets.add(_buildLabelWidget('File Name: ' + (selectedPhoto?.getName() ?? 'NA')));
labelWidgets.add(_buildLabelWidget('Created: ' + (selectedPhoto?.getCreated()?.toIso8601String() ?? 'NA')));
labelWidgets.add(
_buildLabelWidget('Created: ' + (selectedPhoto?.getCreated()?.toIso8601String() ?? 'NA')));
labelWidgets.add(_buildLabelWidget('OCR: ' + (selectedPhoto?.get('ocrText') ?? 'NA')));
labelWidgets.add(_buildLabelWidget('Label: ' + (selectedPhoto?.getLabels().join(',') ?? 'NA')));
labelWidgets.add(_buildLabelWidget(
......
import 'dart:io';
import 'package:flutter/services.dart';
class PlatformImages {
static const platform = MethodChannel('io.memri.photos.channel');
static Future<List<File>?> fetchAllDeviceImages() async {
try {
final List<Object?> result = await platform.invokeMethod('getAllImages');
final files = result.map((e) => File(e as String)).toList();
return files;
} on PlatformException catch (e) {
return null;
}
}
}
\ No newline at end of file
import 'dart:io';
import 'dart:typed_data';
import 'package:photos_app/constants/app_logger.dart';
import 'package:cached_memory_image/cached_memory_image.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:photos_app/constants/app_images.dart';
import 'package:photos_app/constants/app_logger.dart';
import 'package:photos_app/locator.dart';
import 'package:photos_app/providers/default_image_provider.dart';
import 'package:provider/provider.dart';
......@@ -21,7 +23,8 @@ class DefaultImageWidget extends StatefulWidget {
this.fit,
this.radius,
this.borderColor,
}) : super(key: key);
}) : imageFile = null,
super(key: key);
DefaultImageWidget.fromMemory(
this.image, {
......@@ -34,11 +37,27 @@ class DefaultImageWidget extends StatefulWidget {
this.radius,
this.borderColor,
}) : fromMemory = true,
imageFile = null,
super(key: key);
DefaultImageWidget.fromFile(
this.imageFile, {
Key? key,
this.id,
this.fullSizeImage,
this.width,
this.height,
this.fit,
this.radius,
this.borderColor,
}) : fromFile = true,
image = null,
super(key: key);
// It is for cached memory image with identifier
final String? id;
final String? image;
final File? imageFile;
final String? fullSizeImage;
final double? width;
final double? height;
......@@ -46,6 +65,7 @@ class DefaultImageWidget extends StatefulWidget {
final double? radius;
final Color? borderColor;
bool fromMemory = false;
bool fromFile = false;
@override
State<DefaultImageWidget> createState() => _DefaultImageWidgetState();
......@@ -67,9 +87,7 @@ class _DefaultImageWidgetState extends State<DefaultImageWidget>
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_provider.fetchThumbnail().then((_) => {
_provider.fetchFullsizeImage()
});
_provider.fetchThumbnail().then((_) => {_provider.fetchFullsizeImage()});
});
}
......@@ -87,9 +105,11 @@ class _DefaultImageWidgetState extends State<DefaultImageWidget>
create: (_) => _provider,
child: Consumer<DefaultImageProvider>(
builder: (_, provider, child) {
final imageWidget = widget.fromMemory
? _buildCachedMemoryImage(cacheHeight, cacheWidth)
: _buildCachedNetworkImage(cacheWidth, cacheHeight);
final imageWidget = widget.fromFile
? _buildFileImage(cacheHeight, cacheWidth)
: widget.fromMemory
? _buildCachedMemoryImage(cacheHeight, cacheWidth)
: _buildCachedNetworkImage(cacheWidth, cacheHeight);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.radius ?? 0),
......@@ -122,6 +142,22 @@ class _DefaultImageWidgetState extends State<DefaultImageWidget>
);
}
Widget _buildFileImage(double? cacheHeight, double? cacheWidth) {
AppLogger.info('Getting cached memory image');
final Uint8List? imageData =
_provider.fullSizeFileDownloaded ?? _provider.thumbnailFileDownloaded;
return Image.file(
widget.imageFile!,
width: widget.width,
height: widget.height,
fit: widget.fit ?? BoxFit.cover,
cacheHeight: cacheHeight?.toInt(),
cacheWidth: cacheWidth?.toInt(),
gaplessPlayback: true,
errorBuilder: (_, __, dynamic error) => _imageDefault(),
);
}
Widget _buildCachedMemoryImage(double? cacheHeight, double? cacheWidth) {
AppLogger.info('Getting cached memory image');
final Uint8List? imageData =
......
......@@ -26,12 +26,19 @@ class PhotoTile extends StatelessWidget {
aspectRatio: 1,
child: Container(
color: Colors.white,
child: DefaultImageWidget.fromMemory(
photo.getThumbnail(),
id: photo.id,
// height: 100,
// width: 100,
),
child: photo.isFromDevice()
? DefaultImageWidget.fromFile(
photo.getFile(),
id: photo.id,
// height: 100,
// width: 100,
)
: DefaultImageWidget.fromMemory(
photo.getThumbnail(),
id: photo.id,
// height: 100,
// width: 100,
),
),
),
// Positioned(
......
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
// ignore_for_file: always_specify_types
// ignore: avoid_classes_with_only_static_members
class PlatformImagePicker {
static Future<File?> showPickSingleImage(BuildContext context) async {
final ImageSource? imageSource = await _showImageSourceActionSheet(context);
final ImagePicker picker = ImagePicker();
try {
if (imageSource != null) {
final data = await picker.pickImage(source: imageSource, imageQuality: 20);
if (data != null) {
return File(data.path);
}
return null;
} else {
return null;
}
} on Exception catch (e) {
print(e.toString());
return null;
}
}
static Future<List<File>?> showPickMultipleImage(BuildContext context) async {
final ImageSource? imageSource = await _showImageSourceActionSheet(context);
final ImagePicker picker = ImagePicker();
try {
if (imageSource != null) {
if (imageSource == ImageSource.camera) {
final data = await picker.pickImage(source: imageSource, imageQuality: 20);
return data != null ? [File(data.path)] : [];
}
final dataList = await picker.pickMultiImage(imageQuality: 20);
if (dataList.isNotEmpty) {
return dataList.map((e) => File(e.path)).toList();
}
return [];
} else {
return null;
}
} on Exception catch (e) {
print(e.toString());
return null;
}
}
static Future<ImageSource?> _showImageSourceActionSheet(BuildContext context) async {
if (Platform.isIOS) {
return showCupertinoModalPopup<ImageSource>(
context: context,
builder: (BuildContext context) => CupertinoActionSheet(
actions: <CupertinoActionSheetAction>[
CupertinoActionSheetAction(
child: Text('Camera'),
onPressed: () => Navigator.pop(context, ImageSource.camera),
),
CupertinoActionSheetAction(
child: Text('Gallery'),
onPressed: () => Navigator.pop(context, ImageSource.gallery),
)
],
),
);
} else {
return showModalBottomSheet<ImageSource>(
context: context,
builder: (BuildContext context) => Wrap(
children: <Widget>[
ListTile(
leading: const Icon(Icons.camera_alt),
title: Text('Camera'),
onTap: () => Navigator.pop(context, ImageSource.camera),
),
ListTile(
leading: const Icon(Icons.photo_album),
title: Text('Gallery'),
onTap: () => Navigator.pop(context, ImageSource.gallery),
),
],
),
);
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment