use photo_view for the paging, zoom, pan & metadata-extractor for exif
This commit is contained in:
parent
4ee06358b9
commit
55ad742847
9 changed files with 277 additions and 165 deletions
|
@ -54,6 +54,7 @@ flutter {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'com.drewnoakes:metadata-extractor:2.12.0'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||||
|
|
|
@ -16,6 +16,9 @@ import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.load.Key;
|
import com.bumptech.glide.load.Key;
|
||||||
import com.bumptech.glide.request.FutureTarget;
|
import com.bumptech.glide.request.FutureTarget;
|
||||||
import com.bumptech.glide.signature.ObjectKey;
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
|
import com.drew.imaging.ImageMetadataReader;
|
||||||
|
import com.drew.metadata.Metadata;
|
||||||
|
import com.drew.metadata.exif.ExifSubIFDDirectory;
|
||||||
import com.karumi.dexter.Dexter;
|
import com.karumi.dexter.Dexter;
|
||||||
import com.karumi.dexter.PermissionToken;
|
import com.karumi.dexter.PermissionToken;
|
||||||
import com.karumi.dexter.listener.PermissionDeniedResponse;
|
import com.karumi.dexter.listener.PermissionDeniedResponse;
|
||||||
|
@ -24,6 +27,8 @@ import com.karumi.dexter.listener.PermissionRequest;
|
||||||
import com.karumi.dexter.listener.single.PermissionListener;
|
import com.karumi.dexter.listener.single.PermissionListener;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -81,6 +86,10 @@ public class MainActivity extends FlutterActivity {
|
||||||
case "getImageEntries":
|
case "getImageEntries":
|
||||||
getPermissionResult(result, this);
|
getPermissionResult(result, this);
|
||||||
break;
|
break;
|
||||||
|
case "getOverlayMetadata":
|
||||||
|
String path = call.argument("path");
|
||||||
|
getOverlayMetadata(result, path);
|
||||||
|
break;
|
||||||
case "getImageBytes": {
|
case "getImageBytes": {
|
||||||
Map map = call.argument("entry");
|
Map map = call.argument("entry");
|
||||||
Integer width = call.argument("width");
|
Integer width = call.argument("width");
|
||||||
|
@ -153,11 +162,36 @@ public class MainActivity extends FlutterActivity {
|
||||||
}).check();
|
}).check();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Map> fetchAll(Activity activity) {
|
List<Map> fetchAll(Activity activity) {
|
||||||
return new MediaStoreImageProvider().fetchAll(activity).stream()
|
return new MediaStoreImageProvider().fetchAll(activity).stream()
|
||||||
.map(ImageEntry::toMap)
|
.map(ImageEntry::toMap)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void getOverlayMetadata (Result result, String path) {
|
||||||
|
try (InputStream is = new FileInputStream(path)) {
|
||||||
|
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||||
|
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||||
|
Map<String, String> metadataMap = new HashMap<>();
|
||||||
|
if (directory != null) {
|
||||||
|
if (directory.containsTag(ExifSubIFDDirectory.TAG_FNUMBER)) {
|
||||||
|
metadataMap.put("aperture", directory.getDescription(ExifSubIFDDirectory.TAG_FNUMBER));
|
||||||
|
}
|
||||||
|
if (directory.containsTag(ExifSubIFDDirectory.TAG_EXPOSURE_TIME)) {
|
||||||
|
metadataMap.put("exposureTime", directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
|
||||||
|
}
|
||||||
|
if (directory.containsTag(ExifSubIFDDirectory.TAG_FOCAL_LENGTH)) {
|
||||||
|
metadataMap.put("focalLength", directory.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
|
||||||
|
}
|
||||||
|
if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) {
|
||||||
|
metadataMap.put("iso", "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(metadataMap);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, BitmapWorkerTask.MyTaskResult> {
|
class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, BitmapWorkerTask.MyTaskResult> {
|
||||||
|
|
|
@ -1,34 +1,35 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/image_fetcher.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:transparent_image/transparent_image.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
|
|
||||||
class ImageFullscreenPage extends StatefulWidget {
|
class ImageFullscreenPage extends StatefulWidget {
|
||||||
final Map entry;
|
final List<Map> entries;
|
||||||
final Uint8List thumbnail;
|
final String initialUri;
|
||||||
|
|
||||||
ImageFullscreenPage({this.entry, this.thumbnail});
|
ImageFullscreenPage({this.entries, this.initialUri});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageFullscreenPageState createState() => ImageFullscreenPageState();
|
ImageFullscreenPageState createState() => ImageFullscreenPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
||||||
int get imageWidth => widget.entry['width'];
|
int _currentPage;
|
||||||
|
PageController _pageController;
|
||||||
|
|
||||||
int get imageHeight => widget.entry['height'];
|
List<Map> get entries => widget.entries;
|
||||||
|
|
||||||
String get uri => widget.entry['uri'];
|
|
||||||
|
|
||||||
String get path => widget.entry['path'];
|
|
||||||
|
|
||||||
double requestWidth, requestHeight;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
var index = entries.indexWhere((entry) => entry['uri'] == widget.initialUri);
|
||||||
|
_currentPage = max(0, index);
|
||||||
|
_pageController = PageController(initialPage: _currentPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -38,50 +39,126 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (requestWidth == null || requestHeight == null) {
|
|
||||||
var mediaQuery = MediaQuery.of(context);
|
|
||||||
var screenSize = mediaQuery.size;
|
|
||||||
var dpr = mediaQuery.devicePixelRatio;
|
|
||||||
requestWidth = imageWidth * dpr;
|
|
||||||
requestHeight = imageHeight * dpr;
|
|
||||||
if (imageWidth > screenSize.width || imageHeight > screenSize.height) {
|
|
||||||
var ratio = max(imageWidth / screenSize.width, imageHeight / screenSize.height);
|
|
||||||
requestWidth /= ratio;
|
|
||||||
requestHeight /= ratio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return MediaQuery.removeViewInsets(
|
return MediaQuery.removeViewInsets(
|
||||||
context: context,
|
context: context,
|
||||||
// remove bottom view insets to paint underneath the translucent navigation bar
|
// remove bottom view insets to paint underneath the translucent navigation bar
|
||||||
removeBottom: true,
|
removeBottom: true,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Hero(
|
backgroundColor: Colors.black,
|
||||||
tag: uri,
|
body: Stack(
|
||||||
child: Stack(
|
alignment: Alignment.bottomCenter,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
PhotoViewGallery.builder(
|
||||||
child: widget.thumbnail == null
|
itemCount: entries.length,
|
||||||
? CircularProgressIndicator()
|
builder: (context, index) {
|
||||||
: Image.memory(
|
var entry = entries[index];
|
||||||
widget.thumbnail,
|
return PhotoViewGalleryPageOptions(
|
||||||
width: requestWidth,
|
imageProvider: FileImage(File(entry['path'])),
|
||||||
height: requestHeight,
|
heroTag: entry['uri'],
|
||||||
fit: BoxFit.contain,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
),
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loadingChild: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
Center(
|
pageController: _pageController,
|
||||||
child: FadeInImage(
|
onPageChanged: (index) {
|
||||||
placeholder: MemoryImage(kTransparentImage),
|
debugPrint('onPageChanged: index=$index');
|
||||||
image: FileImage(File(path)),
|
setState(() => _currentPage = index);
|
||||||
fadeOutDuration: Duration(milliseconds: 1),
|
},
|
||||||
fadeInDuration: Duration(milliseconds: 200),
|
transitionOnUserGestures: true,
|
||||||
width: requestWidth,
|
scrollPhysics: BouncingScrollPhysics(),
|
||||||
height: requestHeight,
|
),
|
||||||
fit: BoxFit.contain,
|
if (_currentPage != null)
|
||||||
),
|
FullscreenOverlay(
|
||||||
|
entry: entries[_currentPage],
|
||||||
|
index: _currentPage,
|
||||||
|
total: entries.length,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// Hero(
|
||||||
|
// tag: uri,
|
||||||
|
// child: Stack(
|
||||||
|
// children: [
|
||||||
|
// Center(
|
||||||
|
// child: widget.thumbnail == null
|
||||||
|
// ? CircularProgressIndicator()
|
||||||
|
// : Image.memory(
|
||||||
|
// widget.thumbnail,
|
||||||
|
// width: requestWidth,
|
||||||
|
// height: requestHeight,
|
||||||
|
// fit: BoxFit.contain,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// Center(
|
||||||
|
// child: FadeInImage(
|
||||||
|
// placeholder: MemoryImage(kTransparentImage),
|
||||||
|
// image: FileImage(File(path)),
|
||||||
|
// fadeOutDuration: Duration(milliseconds: 1),
|
||||||
|
// fadeInDuration: Duration(milliseconds: 200),
|
||||||
|
// width: requestWidth,
|
||||||
|
// height: requestHeight,
|
||||||
|
// fit: BoxFit.contain,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FullscreenOverlay extends StatelessWidget {
|
||||||
|
final Map entry;
|
||||||
|
final int index, total;
|
||||||
|
|
||||||
|
FullscreenOverlay({this.entry, this.index, this.total});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
debugPrint('FullscreenOverlay MediaQuery.of(context)=${MediaQuery.of(context)}');
|
||||||
|
// TODO TLAD find actual value from MediaQuery before insets removal
|
||||||
|
var viewInsetsBottom = 46.0;
|
||||||
|
var date = ImageEntry.getBestDate(entry);
|
||||||
|
return IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(8.0).add(EdgeInsets.only(bottom: viewInsetsBottom)),
|
||||||
|
color: Colors.black45,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('$index / $total - ${entry['title']}'),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text('${DateFormat.yMMMMd().format(date)} – ${DateFormat.Hm().format(date)}')),
|
||||||
|
Expanded(child: Text('${entry['width']} × ${entry['height']}')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: ImageFetcher.getOverlayMetadata(entry['path']),
|
||||||
|
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done || snapshot.hasError) {
|
||||||
|
return Text('');
|
||||||
|
}
|
||||||
|
var metadata = snapshot.data;
|
||||||
|
if (metadata.isEmpty) {
|
||||||
|
return Text('');
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(metadata['aperture'])),
|
||||||
|
Expanded(child: Text(metadata['exposureTime'])),
|
||||||
|
Expanded(child: Text(metadata['focalLength'])),
|
||||||
|
Expanded(child: Text(metadata['iso'])),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
16
lib/model/image_entry.dart
Normal file
16
lib/model/image_entry.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
class ImageEntry {
|
||||||
|
static DateTime getBestDate(Map entry) {
|
||||||
|
var dateTakenMillis = entry['sourceDateTakenMillis'] as int;
|
||||||
|
if (dateTakenMillis != null && dateTakenMillis > 0) return DateTime.fromMillisecondsSinceEpoch(dateTakenMillis);
|
||||||
|
|
||||||
|
var dateModifiedSecs = entry['dateModifiedSecs'] as int;
|
||||||
|
if (dateModifiedSecs != null && dateModifiedSecs > 0) return DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DateTime getDayTaken(Map entry) {
|
||||||
|
var d = getBestDate(entry);
|
||||||
|
return d == null ? null : DateTime(d.year, d.month, d.day);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ class ImageFetcher {
|
||||||
final result = await platform.invokeMethod('getImageEntries');
|
final result = await platform.invokeMethod('getImageEntries');
|
||||||
return (result as List).cast<Map>();
|
return (result as List).cast<Map>();
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('failed with exception=${e.message}');
|
debugPrint('getImageEntries failed with exception=${e.message}');
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ class ImageFetcher {
|
||||||
});
|
});
|
||||||
return result as Uint8List;
|
return result as Uint8List;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('failed with exception=${e.message}');
|
debugPrint('getImageBytes failed with exception=${e.message}');
|
||||||
}
|
}
|
||||||
return Uint8List(0);
|
return Uint8List(0);
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,20 @@ class ImageFetcher {
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
});
|
});
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('failed with exception=${e.message}');
|
debugPrint('cancelGetImageBytes failed with exception=${e.message}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// return map with: 'aperture' 'exposureTime' 'focalLength' 'iso'
|
||||||
|
static Future<Map> getOverlayMetadata (String path) async {
|
||||||
|
try {
|
||||||
|
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||||
|
'path': path,
|
||||||
|
});
|
||||||
|
return result as Map;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('getOverlayMetadata failed with exception=${e.message}');
|
||||||
|
}
|
||||||
|
return Map();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/image_fullscreen_page.dart';
|
|
||||||
import 'package:aves/model/image_fetcher.dart';
|
import 'package:aves/model/image_fetcher.dart';
|
||||||
import 'package:aves/model/mime_types.dart';
|
import 'package:aves/model/mime_types.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -21,10 +20,6 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
Future<Uint8List> loader;
|
Future<Uint8List> loader;
|
||||||
Uint8List bytes;
|
Uint8List bytes;
|
||||||
|
|
||||||
int get imageWidth => widget.entry['width'];
|
|
||||||
|
|
||||||
int get imageHeight => widget.entry['height'];
|
|
||||||
|
|
||||||
String get mimeType => widget.entry['mimeType'];
|
String get mimeType => widget.entry['mimeType'];
|
||||||
|
|
||||||
String get uri => widget.entry['uri'];
|
String get uri => widget.entry['uri'];
|
||||||
|
@ -50,62 +45,54 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
var isVideo = mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
var isVideo = mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
||||||
var isGif = mimeType == MimeTypes.MIME_GIF;
|
var isGif = mimeType == MimeTypes.MIME_GIF;
|
||||||
var iconSize = widget.extent / 4;
|
var iconSize = widget.extent / 4;
|
||||||
return GestureDetector(
|
return Container(
|
||||||
onTap: () => Navigator.push(
|
decoration: BoxDecoration(
|
||||||
context,
|
border: Border.all(
|
||||||
MaterialPageRoute(
|
color: Colors.grey.shade700,
|
||||||
builder: (context) => ImageFullscreenPage(entry: widget.entry, thumbnail: bytes),
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: FutureBuilder(
|
||||||
decoration: BoxDecoration(
|
future: loader,
|
||||||
border: Border.all(
|
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||||
color: Colors.grey.shade700,
|
if (bytes == null && snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||||
width: 0.5,
|
bytes = snapshot.data;
|
||||||
),
|
}
|
||||||
),
|
return Stack(
|
||||||
child: FutureBuilder(
|
alignment: AlignmentDirectional.bottomStart,
|
||||||
future: loader,
|
children: [
|
||||||
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
Hero(
|
||||||
if (bytes == null && snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
tag: uri,
|
||||||
bytes = snapshot.data;
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
}
|
// during hero animation back from a fullscreen image,
|
||||||
return Stack(
|
// the image covers the whole screen (because of the 'fit' prop and the full screen hero constraints)
|
||||||
alignment: AlignmentDirectional.bottomStart,
|
// so we wrap the image to apply better constraints
|
||||||
children: [
|
var dim = min(constraints.maxWidth, constraints.maxHeight);
|
||||||
Hero(
|
return Container(
|
||||||
tag: uri,
|
alignment: Alignment.center,
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
constraints: BoxConstraints.tight(Size(dim, dim)),
|
||||||
// during hero animation back from a fullscreen image,
|
child: Image.memory(
|
||||||
// the image covers the whole screen (because of the 'fit' prop and the full screen hero constraints)
|
bytes ?? kTransparentImage,
|
||||||
// so we wrap the image to apply better constraints
|
width: dim,
|
||||||
var dim = min(constraints.maxWidth, constraints.maxHeight);
|
height: dim,
|
||||||
return Container(
|
fit: BoxFit.cover,
|
||||||
alignment: Alignment.center,
|
),
|
||||||
constraints: BoxConstraints.tight(Size(dim, dim)),
|
);
|
||||||
child: Image.memory(
|
}),
|
||||||
bytes ?? kTransparentImage,
|
),
|
||||||
width: dim,
|
if (isVideo)
|
||||||
height: dim,
|
Icon(
|
||||||
fit: BoxFit.cover,
|
Icons.play_circle_outline,
|
||||||
),
|
size: iconSize,
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
if (isVideo)
|
if (isGif)
|
||||||
Icon(
|
Icon(
|
||||||
Icons.play_circle_outline,
|
Icons.gif,
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
if (isGif)
|
],
|
||||||
Icon(
|
);
|
||||||
Icons.gif,
|
}),
|
||||||
size: iconSize,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:aves/common/draggable_scrollbar.dart';
|
import 'package:aves/common/draggable_scrollbar.dart';
|
||||||
import 'package:aves/common/outlined_text.dart';
|
import 'package:aves/common/outlined_text.dart';
|
||||||
|
import 'package:aves/image_fullscreen_page.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/thumbnail.dart';
|
import 'package:aves/thumbnail.dart';
|
||||||
import 'package:aves/utils/date_utils.dart';
|
import 'package:aves/utils/date_utils.dart';
|
||||||
import "package:collection/collection.dart";
|
import "package:collection/collection.dart";
|
||||||
|
@ -8,25 +10,11 @@ import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class ThumbnailCollection extends StatelessWidget {
|
class ThumbnailCollection extends StatelessWidget {
|
||||||
|
final List<Map> entries;
|
||||||
final Map<DateTime, List<Map>> sections;
|
final Map<DateTime, List<Map>> sections;
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
|
|
||||||
ThumbnailCollection(List<Map> entries) : sections = groupBy(entries, getDayTaken);
|
ThumbnailCollection(this.entries) : sections = groupBy(entries, ImageEntry.getDayTaken);
|
||||||
|
|
||||||
static DateTime getBestDate(Map entry) {
|
|
||||||
var dateTakenMillis = entry['sourceDateTakenMillis'] as int;
|
|
||||||
if (dateTakenMillis != null && dateTakenMillis > 0) return DateTime.fromMillisecondsSinceEpoch(dateTakenMillis);
|
|
||||||
|
|
||||||
var dateModifiedSecs = entry['dateModifiedSecs'] as int;
|
|
||||||
if (dateModifiedSecs != null && dateModifiedSecs > 0) return DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static DateTime getDayTaken(Map entry) {
|
|
||||||
var d = getBestDate(entry);
|
|
||||||
return d == null ? null : DateTime(d.year, d.month, d.day);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -51,11 +39,23 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
sliver: SliverGrid(
|
sliver: SliverGrid(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, index) {
|
(context, index) {
|
||||||
var entries = sections[sectionKey];
|
var sectionEntries = sections[sectionKey];
|
||||||
if (index >= entries.length) return null;
|
if (index >= sectionEntries.length) return null;
|
||||||
return Thumbnail(
|
var entry = sectionEntries[index];
|
||||||
entry: entries[index],
|
return GestureDetector(
|
||||||
extent: extent,
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ImageFullscreenPage(
|
||||||
|
entries: entries,
|
||||||
|
initialUri: entry['uri'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Thumbnail(
|
||||||
|
entry: entry,
|
||||||
|
extent: extent,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: sections[sectionKey].length,
|
childCount: sections[sectionKey].length,
|
||||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -1,6 +1,13 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
after_layout:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: after_layout
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.7+2"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -81,6 +88,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.7.0"
|
||||||
|
photo_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: photo_view
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.2"
|
||||||
quiver:
|
quiver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
32
pubspec.yaml
32
pubspec.yaml
|
@ -22,6 +22,7 @@ dependencies:
|
||||||
collection:
|
collection:
|
||||||
flutter_sticky_header:
|
flutter_sticky_header:
|
||||||
intl:
|
intl:
|
||||||
|
photo_view:
|
||||||
transparent_image:
|
transparent_image:
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
@ -31,34 +32,3 @@ dev_dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
|
||||||
# assets:
|
|
||||||
# - images/a_dot_burr.jpeg
|
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
|
Loading…
Reference in a new issue