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 {
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.12.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.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.request.FutureTarget;
|
||||
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.PermissionToken;
|
||||
import com.karumi.dexter.listener.PermissionDeniedResponse;
|
||||
|
@ -24,6 +27,8 @@ import com.karumi.dexter.listener.PermissionRequest;
|
|||
import com.karumi.dexter.listener.single.PermissionListener;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -81,6 +86,10 @@ public class MainActivity extends FlutterActivity {
|
|||
case "getImageEntries":
|
||||
getPermissionResult(result, this);
|
||||
break;
|
||||
case "getOverlayMetadata":
|
||||
String path = call.argument("path");
|
||||
getOverlayMetadata(result, path);
|
||||
break;
|
||||
case "getImageBytes": {
|
||||
Map map = call.argument("entry");
|
||||
Integer width = call.argument("width");
|
||||
|
@ -153,11 +162,36 @@ public class MainActivity extends FlutterActivity {
|
|||
}).check();
|
||||
}
|
||||
|
||||
public List<Map> fetchAll(Activity activity) {
|
||||
List<Map> fetchAll(Activity activity) {
|
||||
return new MediaStoreImageProvider().fetchAll(activity).stream()
|
||||
.map(ImageEntry::toMap)
|
||||
.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> {
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
import 'dart:io';
|
||||
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: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 {
|
||||
final Map entry;
|
||||
final Uint8List thumbnail;
|
||||
final List<Map> entries;
|
||||
final String initialUri;
|
||||
|
||||
ImageFullscreenPage({this.entry, this.thumbnail});
|
||||
ImageFullscreenPage({this.entries, this.initialUri});
|
||||
|
||||
@override
|
||||
ImageFullscreenPageState createState() => ImageFullscreenPageState();
|
||||
}
|
||||
|
||||
class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
||||
int get imageWidth => widget.entry['width'];
|
||||
int _currentPage;
|
||||
PageController _pageController;
|
||||
|
||||
int get imageHeight => widget.entry['height'];
|
||||
|
||||
String get uri => widget.entry['uri'];
|
||||
|
||||
String get path => widget.entry['path'];
|
||||
|
||||
double requestWidth, requestHeight;
|
||||
List<Map> get entries => widget.entries;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var index = entries.indexWhere((entry) => entry['uri'] == widget.initialUri);
|
||||
_currentPage = max(0, index);
|
||||
_pageController = PageController(initialPage: _currentPage);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -38,50 +39,126 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> {
|
|||
|
||||
@override
|
||||
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(
|
||||
context: context,
|
||||
// remove bottom view insets to paint underneath the translucent navigation bar
|
||||
removeBottom: true,
|
||||
child: Scaffold(
|
||||
body: Hero(
|
||||
tag: uri,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: widget.thumbnail == null
|
||||
? CircularProgressIndicator()
|
||||
: Image.memory(
|
||||
widget.thumbnail,
|
||||
width: requestWidth,
|
||||
height: requestHeight,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
PhotoViewGallery.builder(
|
||||
itemCount: entries.length,
|
||||
builder: (context, index) {
|
||||
var entry = entries[index];
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: FileImage(File(entry['path'])),
|
||||
heroTag: entry['uri'],
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
);
|
||||
},
|
||||
loadingChild: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
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,
|
||||
),
|
||||
pageController: _pageController,
|
||||
onPageChanged: (index) {
|
||||
debugPrint('onPageChanged: index=$index');
|
||||
setState(() => _currentPage = index);
|
||||
},
|
||||
transitionOnUserGestures: true,
|
||||
scrollPhysics: BouncingScrollPhysics(),
|
||||
),
|
||||
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');
|
||||
return (result as List).cast<Map>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
debugPrint('getImageEntries failed with exception=${e.message}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class ImageFetcher {
|
|||
});
|
||||
return result as Uint8List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('failed with exception=${e.message}');
|
||||
debugPrint('getImageBytes failed with exception=${e.message}');
|
||||
}
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
@ -36,7 +36,20 @@ class ImageFetcher {
|
|||
'uri': uri,
|
||||
});
|
||||
} 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:typed_data';
|
||||
|
||||
import 'package:aves/image_fullscreen_page.dart';
|
||||
import 'package:aves/model/image_fetcher.dart';
|
||||
import 'package:aves/model/mime_types.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -21,10 +20,6 @@ class ThumbnailState extends State<Thumbnail> {
|
|||
Future<Uint8List> loader;
|
||||
Uint8List bytes;
|
||||
|
||||
int get imageWidth => widget.entry['width'];
|
||||
|
||||
int get imageHeight => widget.entry['height'];
|
||||
|
||||
String get mimeType => widget.entry['mimeType'];
|
||||
|
||||
String get uri => widget.entry['uri'];
|
||||
|
@ -50,62 +45,54 @@ class ThumbnailState extends State<Thumbnail> {
|
|||
var isVideo = mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
||||
var isGif = mimeType == MimeTypes.MIME_GIF;
|
||||
var iconSize = widget.extent / 4;
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ImageFullscreenPage(entry: widget.entry, thumbnail: bytes),
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade700,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade700,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: FutureBuilder(
|
||||
future: loader,
|
||||
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||
if (bytes == null && snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||
bytes = snapshot.data;
|
||||
}
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
Hero(
|
||||
tag: uri,
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
// during hero animation back from a fullscreen image,
|
||||
// the image covers the whole screen (because of the 'fit' prop and the full screen hero constraints)
|
||||
// so we wrap the image to apply better constraints
|
||||
var dim = min(constraints.maxWidth, constraints.maxHeight);
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
constraints: BoxConstraints.tight(Size(dim, dim)),
|
||||
child: Image.memory(
|
||||
bytes ?? kTransparentImage,
|
||||
width: dim,
|
||||
height: dim,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}),
|
||||
child: FutureBuilder(
|
||||
future: loader,
|
||||
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||
if (bytes == null && snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||
bytes = snapshot.data;
|
||||
}
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
Hero(
|
||||
tag: uri,
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
// during hero animation back from a fullscreen image,
|
||||
// the image covers the whole screen (because of the 'fit' prop and the full screen hero constraints)
|
||||
// so we wrap the image to apply better constraints
|
||||
var dim = min(constraints.maxWidth, constraints.maxHeight);
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
constraints: BoxConstraints.tight(Size(dim, dim)),
|
||||
child: Image.memory(
|
||||
bytes ?? kTransparentImage,
|
||||
width: dim,
|
||||
height: dim,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
if (isVideo)
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: iconSize,
|
||||
),
|
||||
if (isVideo)
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: iconSize,
|
||||
),
|
||||
if (isGif)
|
||||
Icon(
|
||||
Icons.gif,
|
||||
size: iconSize,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
if (isGif)
|
||||
Icon(
|
||||
Icons.gif,
|
||||
size: iconSize,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:aves/common/draggable_scrollbar.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/utils/date_utils.dart';
|
||||
import "package:collection/collection.dart";
|
||||
|
@ -8,25 +10,11 @@ import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
class ThumbnailCollection extends StatelessWidget {
|
||||
final List<Map> entries;
|
||||
final Map<DateTime, List<Map>> sections;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
ThumbnailCollection(List<Map> entries) : sections = groupBy(entries, 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);
|
||||
}
|
||||
ThumbnailCollection(this.entries) : sections = groupBy(entries, ImageEntry.getDayTaken);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -51,11 +39,23 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
var entries = sections[sectionKey];
|
||||
if (index >= entries.length) return null;
|
||||
return Thumbnail(
|
||||
entry: entries[index],
|
||||
extent: extent,
|
||||
var sectionEntries = sections[sectionKey];
|
||||
if (index >= sectionEntries.length) return null;
|
||||
var entry = sectionEntries[index];
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ImageFullscreenPage(
|
||||
entries: entries,
|
||||
initialUri: entry['uri'],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Thumbnail(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: sections[sectionKey].length,
|
||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -1,6 +1,13 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
after_layout:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: after_layout
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.7+2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -81,6 +88,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
32
pubspec.yaml
32
pubspec.yaml
|
@ -22,6 +22,7 @@ dependencies:
|
|||
collection:
|
||||
flutter_sticky_header:
|
||||
intl:
|
||||
photo_view:
|
||||
transparent_image:
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -31,34 +32,3 @@ dev_dependencies:
|
|||
flutter:
|
||||
|
||||
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